mirror of
https://github.com/kennethnym/aris.git
synced 2026-03-20 00:51:20 +00:00
Add listActions() and executeAction() to FeedSource for write operations back to external services. Actions use arktype schemas for input validation via StandardSchemaV1. - ActionDefinition type with optional input schema - FeedEngine routes actions with existence and ID validation - Source IDs use reverse-domain format (aris.location, aris.tfl) - LocationSource: update-location action with schema validation - TflSource: set-lines-of-interest action with lineId validation - No-op implementations for sources without actions Co-authored-by: Ona <no-reply@ona.com>
99 lines
2.5 KiB
TypeScript
99 lines
2.5 KiB
TypeScript
import type { ActionDefinition, Context, FeedSource } from "@aris/core"
|
|
|
|
import { UnknownActionError, contextKey, type ContextKey } from "@aris/core"
|
|
import { type } from "arktype"
|
|
|
|
import { Location, type LocationSourceOptions } from "./types.ts"
|
|
|
|
export const LocationKey: ContextKey<Location> = contextKey("location")
|
|
|
|
/**
|
|
* A FeedSource that provides location context.
|
|
*
|
|
* This source accepts external location pushes and does not query location itself.
|
|
* Use `pushLocation` to update the location from an external provider (e.g., GPS, network).
|
|
*
|
|
* Does not produce feed items - always returns empty array from `fetchItems`.
|
|
*/
|
|
export class LocationSource implements FeedSource {
|
|
readonly id = "aris.location"
|
|
|
|
private readonly historySize: number
|
|
private locations: Location[] = []
|
|
private listeners = new Set<(update: Partial<Context>) => void>()
|
|
|
|
constructor(options: LocationSourceOptions = {}) {
|
|
this.historySize = options.historySize ?? 1
|
|
}
|
|
|
|
async listActions(): Promise<Record<string, ActionDefinition>> {
|
|
return {
|
|
"update-location": {
|
|
id: "update-location",
|
|
description: "Push a new location update",
|
|
input: Location,
|
|
},
|
|
}
|
|
}
|
|
|
|
async executeAction(actionId: string, params: unknown): Promise<void> {
|
|
switch (actionId) {
|
|
case "update-location": {
|
|
const result = Location(params)
|
|
if (result instanceof type.errors) {
|
|
throw new Error(result.summary)
|
|
}
|
|
this.pushLocation(result)
|
|
return
|
|
}
|
|
default:
|
|
throw new UnknownActionError(actionId)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Push a new location update. Notifies all context listeners.
|
|
*/
|
|
pushLocation(location: Location): void {
|
|
this.locations.push(location)
|
|
if (this.locations.length > this.historySize) {
|
|
this.locations.shift()
|
|
}
|
|
this.listeners.forEach((listener) => {
|
|
listener({ [LocationKey]: location })
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Most recent location, or null if none pushed.
|
|
*/
|
|
get lastLocation(): Location | null {
|
|
return this.locations[this.locations.length - 1] ?? null
|
|
}
|
|
|
|
/**
|
|
* Location history, oldest first. Length limited by `historySize`.
|
|
*/
|
|
get locationHistory(): readonly Location[] {
|
|
return this.locations
|
|
}
|
|
|
|
onContextUpdate(callback: (update: Partial<Context>) => void): () => void {
|
|
this.listeners.add(callback)
|
|
return () => {
|
|
this.listeners.delete(callback)
|
|
}
|
|
}
|
|
|
|
async fetchContext(): Promise<Partial<Context> | null> {
|
|
if (this.lastLocation) {
|
|
return { [LocationKey]: this.lastLocation }
|
|
}
|
|
return null
|
|
}
|
|
|
|
async fetchItems(): Promise<[]> {
|
|
return []
|
|
}
|
|
}
|