feat: add actions to FeedSource interface

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>
This commit is contained in:
2026-02-15 12:26:23 +00:00
parent 4d6cac7ec8
commit 699155e0d8
29 changed files with 1169 additions and 116 deletions

View File

@@ -1,22 +1,9 @@
import type { Context, FeedSource } from "@aris/core"
import type { ActionDefinition, Context, FeedSource } from "@aris/core"
import { contextKey, type ContextKey } from "@aris/core"
import { UnknownActionError, contextKey, type ContextKey } from "@aris/core"
import { type } from "arktype"
/**
* Geographic coordinates with accuracy and timestamp.
*/
export interface Location {
lat: number
lng: number
/** Accuracy in meters */
accuracy: number
timestamp: Date
}
export interface LocationSourceOptions {
/** Number of locations to retain in history. Defaults to 1. */
historySize?: number
}
import { Location, type LocationSourceOptions } from "./types.ts"
export const LocationKey: ContextKey<Location> = contextKey("location")
@@ -29,7 +16,7 @@ export const LocationKey: ContextKey<Location> = contextKey("location")
* Does not produce feed items - always returns empty array from `fetchItems`.
*/
export class LocationSource implements FeedSource {
readonly id = "location"
readonly id = "aris.location"
private readonly historySize: number
private locations: Location[] = []
@@ -39,6 +26,31 @@ export class LocationSource implements FeedSource {
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.
*/