mirror of
https://github.com/kennethnym/aris.git
synced 2026-03-20 00:51:20 +00:00
feat: replace flat context with tuple-keyed store (#50)
Context keys are now tuples instead of strings, inspired by
React Query's query keys. This prevents context collisions
when multiple instances of the same source type are registered.
Sources write to structured keys like
["aris.google-calendar", "nextEvent", { account: "work" }]
and consumers can query by prefix via context.find().
Co-authored-by: Ona <no-reply@ona.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import type { Context } from "@aris/core"
|
||||
import type { ContextEntry } from "@aris/core"
|
||||
|
||||
import { TimeRelevance, contextValue } from "@aris/core"
|
||||
import { Context, TimeRelevance } from "@aris/core"
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { readFileSync } from "node:fs"
|
||||
import { join } from "node:path"
|
||||
@@ -13,14 +13,21 @@ import type {
|
||||
} from "./types.ts"
|
||||
|
||||
import { CalDavSource, computeSignals } from "./caldav-source.ts"
|
||||
import { CalDavCalendarKey } from "./calendar-context.ts"
|
||||
import { CalDavCalendarKey, type CalendarContext } from "./calendar-context.ts"
|
||||
|
||||
function loadFixture(name: string): string {
|
||||
return readFileSync(join(import.meta.dir, "..", "fixtures", name), "utf-8")
|
||||
}
|
||||
|
||||
function createContext(time: Date): Context {
|
||||
return { time }
|
||||
return new Context(time)
|
||||
}
|
||||
|
||||
/** Extract the CalendarContext value from fetchContext entries. */
|
||||
function extractCalendar(entries: readonly ContextEntry[] | null): CalendarContext | undefined {
|
||||
if (!entries) return undefined
|
||||
const entry = entries.find(([key]) => key === CalDavCalendarKey)
|
||||
return entry?.[1] as CalendarContext | undefined
|
||||
}
|
||||
|
||||
class MockDAVClient implements CalDavDAVClient {
|
||||
@@ -302,8 +309,8 @@ describe("CalDavSource.fetchContext", () => {
|
||||
test("returns empty context when no calendars exist", async () => {
|
||||
const client = new MockDAVClient([], {})
|
||||
const source = createSource(client)
|
||||
const ctx = await source.fetchContext(createContext(new Date("2026-01-15T12:00:00Z")))
|
||||
const calendar = contextValue(ctx as Context, CalDavCalendarKey)
|
||||
const entries = await source.fetchContext(createContext(new Date("2026-01-15T12:00:00Z")))
|
||||
const calendar = extractCalendar(entries)
|
||||
|
||||
expect(calendar).toBeDefined()
|
||||
expect(calendar!.inProgress).toEqual([])
|
||||
@@ -320,8 +327,8 @@ describe("CalDavSource.fetchContext", () => {
|
||||
const source = createSource(client)
|
||||
|
||||
// 14:30 is during the 14:00-15:00 event
|
||||
const ctx = await source.fetchContext(createContext(new Date("2026-01-15T14:30:00Z")))
|
||||
const calendar = contextValue(ctx as Context, CalDavCalendarKey)
|
||||
const entries = await source.fetchContext(createContext(new Date("2026-01-15T14:30:00Z")))
|
||||
const calendar = extractCalendar(entries)
|
||||
|
||||
expect(calendar!.inProgress).toHaveLength(1)
|
||||
expect(calendar!.inProgress[0]!.title).toBe("Team Standup")
|
||||
@@ -335,8 +342,8 @@ describe("CalDavSource.fetchContext", () => {
|
||||
const source = createSource(client)
|
||||
|
||||
// 12:00 is before the 14:00 event
|
||||
const ctx = await source.fetchContext(createContext(new Date("2026-01-15T12:00:00Z")))
|
||||
const calendar = contextValue(ctx as Context, CalDavCalendarKey)
|
||||
const entries = await source.fetchContext(createContext(new Date("2026-01-15T12:00:00Z")))
|
||||
const calendar = extractCalendar(entries)
|
||||
|
||||
expect(calendar!.inProgress).toHaveLength(0)
|
||||
expect(calendar!.nextEvent).not.toBeNull()
|
||||
@@ -350,8 +357,8 @@ describe("CalDavSource.fetchContext", () => {
|
||||
const client = new MockDAVClient([{ url: "/cal/work", displayName: "Work" }], objects)
|
||||
const source = createSource(client)
|
||||
|
||||
const ctx = await source.fetchContext(createContext(new Date("2026-01-15T12:00:00Z")))
|
||||
const calendar = contextValue(ctx as Context, CalDavCalendarKey)
|
||||
const entries = await source.fetchContext(createContext(new Date("2026-01-15T12:00:00Z")))
|
||||
const calendar = extractCalendar(entries)
|
||||
|
||||
expect(calendar!.inProgress).toHaveLength(0)
|
||||
expect(calendar!.nextEvent).toBeNull()
|
||||
@@ -369,8 +376,8 @@ describe("CalDavSource.fetchContext", () => {
|
||||
const client = new MockDAVClient([{ url: "/cal/work", displayName: "Work" }], objects)
|
||||
const source = createSource(client)
|
||||
|
||||
const ctx = await source.fetchContext(createContext(new Date("2026-01-15T12:00:00Z")))
|
||||
const calendar = contextValue(ctx as Context, CalDavCalendarKey)
|
||||
const entries = await source.fetchContext(createContext(new Date("2026-01-15T12:00:00Z")))
|
||||
const calendar = extractCalendar(entries)
|
||||
|
||||
expect(calendar!.todayEventCount).toBe(2)
|
||||
expect(calendar!.hasTodayEvents).toBe(true)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ActionDefinition, Context, FeedItemSignals, FeedSource } from "@aris/core"
|
||||
import type { ActionDefinition, ContextEntry, FeedItemSignals, FeedSource } from "@aris/core"
|
||||
|
||||
import { TimeRelevance, UnknownActionError } from "@aris/core"
|
||||
import { Context, TimeRelevance, UnknownActionError } from "@aris/core"
|
||||
import { DAVClient } from "tsdav"
|
||||
|
||||
import type { CalDavDAVClient, CalDavEventData, CalDavFeedItem } from "./types.ts"
|
||||
@@ -93,17 +93,20 @@ export class CalDavSource implements FeedSource<CalDavFeedItem> {
|
||||
throw new UnknownActionError(actionId)
|
||||
}
|
||||
|
||||
async fetchContext(context: Context): Promise<Partial<Context> | null> {
|
||||
async fetchContext(context: Context): Promise<readonly ContextEntry[] | null> {
|
||||
const events = await this.fetchEvents(context)
|
||||
if (events.length === 0) {
|
||||
return {
|
||||
[CalDavCalendarKey]: {
|
||||
inProgress: [],
|
||||
nextEvent: null,
|
||||
hasTodayEvents: false,
|
||||
todayEventCount: 0,
|
||||
},
|
||||
}
|
||||
return [
|
||||
[
|
||||
CalDavCalendarKey,
|
||||
{
|
||||
inProgress: [],
|
||||
nextEvent: null,
|
||||
hasTodayEvents: false,
|
||||
todayEventCount: 0,
|
||||
},
|
||||
],
|
||||
]
|
||||
}
|
||||
|
||||
const now = context.time
|
||||
@@ -121,7 +124,7 @@ export class CalDavSource implements FeedSource<CalDavFeedItem> {
|
||||
todayEventCount: events.length,
|
||||
}
|
||||
|
||||
return { [CalDavCalendarKey]: calendarContext }
|
||||
return [[CalDavCalendarKey, calendarContext]]
|
||||
}
|
||||
|
||||
async fetchItems(context: Context): Promise<CalDavFeedItem[]> {
|
||||
|
||||
@@ -21,4 +21,4 @@ export interface CalendarContext {
|
||||
todayEventCount: number
|
||||
}
|
||||
|
||||
export const CalDavCalendarKey: ContextKey<CalendarContext> = contextKey("caldavCalendar")
|
||||
export const CalDavCalendarKey: ContextKey<CalendarContext> = contextKey("aris.caldav", "calendar")
|
||||
|
||||
Reference in New Issue
Block a user