mirror of
https://github.com/kennethnym/aris.git
synced 2026-03-20 09:01:19 +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:
@@ -10,4 +10,4 @@ export interface NextEvent {
|
||||
location: string | null
|
||||
}
|
||||
|
||||
export const NextEventKey: ContextKey<NextEvent> = contextKey("nextEvent")
|
||||
export const NextEventKey: ContextKey<NextEvent> = contextKey("aris.google-calendar", "nextEvent")
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { TimeRelevance, contextValue, type Context } from "@aris/core"
|
||||
import { Context, TimeRelevance } from "@aris/core"
|
||||
import { describe, expect, test } from "bun:test"
|
||||
|
||||
import type { ApiCalendarEvent, GoogleCalendarClient, ListEventsOptions } from "./types"
|
||||
|
||||
import fixture from "../fixtures/events.json"
|
||||
import { NextEventKey } from "./calendar-context"
|
||||
import { NextEventKey, type NextEvent } from "./calendar-context"
|
||||
import { CalendarFeedItemType } from "./feed-items"
|
||||
import { GoogleCalendarSource } from "./google-calendar-source"
|
||||
|
||||
@@ -38,7 +38,7 @@ function defaultMockClient(): GoogleCalendarClient {
|
||||
}
|
||||
|
||||
function createContext(time?: Date): Context {
|
||||
return { time: time ?? NOW }
|
||||
return new Context(time ?? NOW)
|
||||
}
|
||||
|
||||
describe("GoogleCalendarSource", () => {
|
||||
@@ -229,15 +229,16 @@ describe("GoogleCalendarSource", () => {
|
||||
|
||||
test("returns next upcoming timed event (not ongoing)", async () => {
|
||||
const source = new GoogleCalendarSource({ client: defaultMockClient() })
|
||||
const result = await source.fetchContext(createContext())
|
||||
const entries = await source.fetchContext(createContext())
|
||||
|
||||
expect(result).not.toBeNull()
|
||||
const nextEvent = contextValue(result! as Context, NextEventKey)
|
||||
expect(nextEvent).toBeDefined()
|
||||
expect(entries).not.toBeNull()
|
||||
expect(entries).toHaveLength(1)
|
||||
const [key, nextEvent] = entries![0]! as [typeof NextEventKey, NextEvent]
|
||||
expect(key).toEqual(NextEventKey)
|
||||
// evt-soon starts at 10:10, which is the nearest future timed event
|
||||
expect(nextEvent!.title).toBe("1:1 with Manager")
|
||||
expect(nextEvent!.minutesUntilStart).toBe(10)
|
||||
expect(nextEvent!.location).toBeNull()
|
||||
expect(nextEvent.title).toBe("1:1 with Manager")
|
||||
expect(nextEvent.minutesUntilStart).toBe(10)
|
||||
expect(nextEvent.location).toBeNull()
|
||||
})
|
||||
|
||||
test("includes location when available", async () => {
|
||||
@@ -255,12 +256,11 @@ describe("GoogleCalendarSource", () => {
|
||||
const source = new GoogleCalendarSource({
|
||||
client: createMockClient({ primary: events }),
|
||||
})
|
||||
const result = await source.fetchContext(createContext())
|
||||
const entries = await source.fetchContext(createContext())
|
||||
|
||||
expect(result).not.toBeNull()
|
||||
const nextEvent = contextValue(result! as Context, NextEventKey)
|
||||
expect(nextEvent).toBeDefined()
|
||||
expect(nextEvent!.location).toBe("123 Main St")
|
||||
expect(entries).not.toBeNull()
|
||||
const [, nextEvent] = entries![0]! as [typeof NextEventKey, NextEvent]
|
||||
expect(nextEvent.location).toBe("123 Main St")
|
||||
})
|
||||
|
||||
test("skips ongoing events for next-event context", async () => {
|
||||
|
||||
@@ -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 type {
|
||||
ApiCalendarEvent,
|
||||
@@ -58,7 +58,7 @@ const URGENCY_ALL_DAY = 0.4
|
||||
* .register(calendarSource)
|
||||
*
|
||||
* // Access next-event context in downstream sources
|
||||
* const next = contextValue(context, NextEventKey)
|
||||
* const next = context.get(NextEventKey)
|
||||
* if (next && next.minutesUntilStart < 15) {
|
||||
* // remind user
|
||||
* }
|
||||
@@ -85,7 +85,7 @@ export class GoogleCalendarSource implements FeedSource<CalendarFeedItem> {
|
||||
throw new UnknownActionError(actionId)
|
||||
}
|
||||
|
||||
async fetchContext(context: Context): Promise<Partial<Context> | null> {
|
||||
async fetchContext(context: Context): Promise<readonly ContextEntry[] | null> {
|
||||
const events = await this.fetchAllEvents(context.time)
|
||||
|
||||
const now = context.time.getTime()
|
||||
@@ -105,7 +105,7 @@ export class GoogleCalendarSource implements FeedSource<CalendarFeedItem> {
|
||||
location: nextTimedEvent.location,
|
||||
}
|
||||
|
||||
return { [NextEventKey]: nextEvent }
|
||||
return [[NextEventKey, nextEvent]]
|
||||
}
|
||||
|
||||
async fetchItems(context: Context): Promise<CalendarFeedItem[]> {
|
||||
|
||||
Reference in New Issue
Block a user