feat(core): add FeedController orchestration layer

Adds orchestration for feed reconciliation with context-driven updates:

- FeedController: holds context, debounces updates, reconciles sources
- ContextBridge: bridges context providers to controller
- ContextProvider: reactive + on-demand context value interface
- Branded ContextKey<T> for type-safe context keys

Moves source files to src/ directory and consolidates tests into
integration test.

Co-authored-by: Ona <no-reply@ona.com>
This commit is contained in:
2026-01-18 00:58:29 +00:00
parent 2eff7b49dc
commit 3c16dd4275
15 changed files with 965 additions and 272 deletions

View File

@@ -0,0 +1,79 @@
import type { Context } from "./context"
import type { ContextProvider } from "./context-provider"
interface ContextUpdatable {
pushContextUpdate(update: Partial<Context>): void
}
/**
* Bridges context providers to a feed controller.
*
* Subscribes to provider updates and forwards them to the controller.
* Supports manual refresh to gather current values from all providers.
*
* @example
* ```ts
* const controller = new FeedController()
* .addDataSource(new WeatherDataSource())
* .addDataSource(new TflDataSource())
*
* const bridge = new ContextBridge(controller)
* .addProvider(new LocationProvider())
* .addProvider(new MusicProvider())
*
* // Manual refresh gathers from all providers
* await bridge.refresh()
*
* // Cleanup
* bridge.stop()
* controller.stop()
* ```
*/
export class ContextBridge {
private controller: ContextUpdatable
private providers = new Map<string, ContextProvider>()
private cleanups: Array<() => void> = []
constructor(controller: ContextUpdatable) {
this.controller = controller
}
/**
* Registers a context provider. Immediately subscribes to updates.
*/
addProvider<T>(provider: ContextProvider<T>): this {
this.providers.set(provider.key, provider as ContextProvider)
const cleanup = provider.onUpdate((value) => {
this.controller.pushContextUpdate({ [provider.key]: value })
})
this.cleanups.push(cleanup)
return this
}
/**
* Gathers current values from all providers and pushes to controller.
* Use for manual refresh when user pulls to refresh.
*/
async refresh(): Promise<void> {
const updates: Partial<Context> = {}
const entries = Array.from(this.providers.entries())
const values = await Promise.all(entries.map(([_, provider]) => provider.getCurrentValue()))
entries.forEach(([key], i) => {
updates[key] = values[i]
})
this.controller.pushContextUpdate(updates)
}
/**
* Unsubscribes from all providers.
*/
stop(): void {
this.cleanups.forEach((cleanup) => cleanup())
this.cleanups = []
}
}