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:
2026-03-01 22:52:41 +00:00
committed by GitHub
parent 8ca8a0d1d2
commit 96e22e227c
29 changed files with 544 additions and 227 deletions

View File

@@ -1,5 +1,6 @@
import type { Context } from "@aris/core"
import type { ContextKey } from "@aris/core"
import { Context, contextKey } from "@aris/core"
import { describe, expect, test } from "bun:test"
import type { WeatherKitClient, WeatherKitResponse } from "./weatherkit"
@@ -15,14 +16,25 @@ const mockCredentials = {
serviceId: "mock",
}
interface LocationData {
lat: number
lng: number
accuracy: number
}
const LocationKey: ContextKey<LocationData> = contextKey("aris.location", "location")
const createMockClient = (response: WeatherKitResponse): WeatherKitClient => ({
fetch: async () => response,
})
const createMockContext = (location?: { lat: number; lng: number }): Context => ({
time: new Date("2026-01-17T00:00:00Z"),
location: location ? { ...location, accuracy: 10 } : undefined,
})
function createMockContext(location?: { lat: number; lng: number }): Context {
const ctx = new Context(new Date("2026-01-17T00:00:00Z"))
if (location) {
ctx.set([[LocationKey, { ...location, accuracy: 10 }]])
}
return ctx
}
describe("WeatherKitDataSource", () => {
test("returns empty array when location is missing", async () => {

View File

@@ -1,6 +1,6 @@
import type { Context, DataSource, FeedItemSignals } from "@aris/core"
import type { Context, ContextKey, DataSource, FeedItemSignals } from "@aris/core"
import { TimeRelevance } from "@aris/core"
import { TimeRelevance, contextKey } from "@aris/core"
import {
WeatherFeedItemType,
@@ -40,6 +40,13 @@ export interface WeatherKitQueryConfig {
units?: Units
}
interface LocationData {
lat: number
lng: number
}
const LocationKey: ContextKey<LocationData> = contextKey("aris.location", "location")
export class WeatherKitDataSource implements DataSource<WeatherFeedItem, WeatherKitQueryConfig> {
private readonly DEFAULT_HOURLY_LIMIT = 12
private readonly DEFAULT_DAILY_LIMIT = 7
@@ -59,7 +66,8 @@ export class WeatherKitDataSource implements DataSource<WeatherFeedItem, Weather
}
async query(context: Context, config: WeatherKitQueryConfig = {}): Promise<WeatherFeedItem[]> {
if (!context.location) {
const location = context.get(LocationKey)
if (!location) {
return []
}
@@ -67,8 +75,8 @@ export class WeatherKitDataSource implements DataSource<WeatherFeedItem, Weather
const timestamp = context.time
const response = await this.client.fetch({
lat: context.location.lat,
lng: context.location.lng,
lat: location.lat,
lng: location.lng,
})
const items: WeatherFeedItem[] = []