| export * as SystemContext from "./index" |
|
|
| import { Effect, Option, Schema } from "effect" |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| export const Key = Schema.String.check(Schema.isPattern(/^[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._/-]*$/)).pipe( |
| Schema.brand("SystemContext.Key"), |
| ) |
| export type Key = typeof Key.Type |
|
|
| |
| export const unavailable = Symbol.for("@opencode/SystemContext.Unavailable") |
| export type Unavailable = typeof unavailable |
|
|
| |
| export interface Source<A> { |
| readonly key: Key |
| readonly codec: Schema.Codec<A, Schema.Json, never, never> |
| readonly load: Effect.Effect<A | Unavailable> |
| readonly baseline: (current: A) => string |
| readonly update: (previous: A, current: A) => string |
| readonly removed?: (previous: A) => string |
| } |
|
|
| const ContextTypeId: unique symbol = Symbol.for("@opencode/SystemContext") |
|
|
| |
| export interface SystemContext { |
| readonly [ContextTypeId]: ReadonlyArray<PackedSource> |
| } |
|
|
| |
| export const SourceSnapshot = Schema.Struct({ |
| value: Schema.Json, |
| removed: Schema.optional(Schema.NonEmptyString), |
| }) |
| export type SourceSnapshot = typeof SourceSnapshot.Type |
|
|
| |
| export const Snapshot = Schema.Record(Key, SourceSnapshot) |
| export type Snapshot = Readonly<Record<string, SourceSnapshot>> |
|
|
| export interface Generation { |
| readonly baseline: string |
| readonly snapshot: Snapshot |
| } |
|
|
| export interface Updated { |
| readonly _tag: "Updated" |
| readonly text: string |
| readonly snapshot: Snapshot |
| } |
|
|
| export interface ReplacementReady { |
| readonly _tag: "ReplacementReady" |
| readonly generation: Generation |
| } |
|
|
| export interface ReplacementBlocked { |
| readonly _tag: "ReplacementBlocked" |
| } |
|
|
| export type ReplacementResult = ReplacementReady | ReplacementBlocked |
| export type ReconcileResult = { readonly _tag: "Unchanged" } | Updated | ReplacementResult |
|
|
| export class InitializationBlocked extends Schema.TaggedErrorClass<InitializationBlocked>()( |
| "SystemContext.InitializationBlocked", |
| { keys: Schema.Array(Key) }, |
| ) { |
| override get message() { |
| return `System context initialization blocked by unavailable sources: ${this.keys.join(", ")}` |
| } |
| } |
|
|
| export class DuplicateKeyError extends Schema.TaggedErrorClass<DuplicateKeyError>()("SystemContext.DuplicateKeyError", { |
| key: Key, |
| }) { |
| override get message() { |
| return `Duplicate system context key: ${this.key}` |
| } |
| } |
|
|
| interface PackedSource { |
| readonly key: Key |
| readonly load: Effect.Effect<Loaded | Unavailable> |
| } |
|
|
| interface Loaded { |
| readonly baseline: () => Rendered |
| readonly compare: (previous: Schema.Json) => Compared |
| } |
|
|
| interface Rendered { |
| readonly text: string |
| readonly snapshot: SourceSnapshot |
| } |
|
|
| type Compared = |
| | { readonly _tag: "Incompatible" } |
| | { readonly _tag: "Unchanged" } |
| | { readonly _tag: "Updated"; readonly render: () => Rendered } |
|
|
| interface AvailableEntry extends Loaded { |
| readonly _tag: "Available" |
| readonly key: Key |
| } |
|
|
| interface UnavailableEntry { |
| readonly _tag: "Unavailable" |
| readonly key: Key |
| } |
|
|
| type Entry = AvailableEntry | UnavailableEntry |
|
|
| |
| export const empty = context([]) |
|
|
| |
| export function make<A>(source: Source<A>): SystemContext { |
| const decode = Schema.decodeUnknownOption(source.codec) |
| const encode = Schema.encodeSync(source.codec) |
| const equivalent = Schema.toEquivalence(source.codec) |
| return context([ |
| { |
| key: source.key, |
| load: source.load.pipe( |
| Effect.map((value) => { |
| if (isUnavailable(value)) return value |
| const snapshot = (): SourceSnapshot => ({ |
| value: encode(value), |
| ...(source.removed ? { removed: requireText(source.key, "removal", source.removed(value)) } : {}), |
| }) |
| return { |
| baseline: (): Rendered => ({ |
| text: requireText(source.key, "baseline", source.baseline(value)), |
| snapshot: snapshot(), |
| }), |
| compare: (previous): Compared => |
| Option.match(decode(previous), { |
| onNone: (): Compared => ({ _tag: "Incompatible" }), |
| onSome: (decoded): Compared => |
| equivalent(decoded, value) |
| ? { _tag: "Unchanged" } |
| : { |
| _tag: "Updated", |
| render: () => ({ |
| text: requireText(source.key, "update", source.update(decoded, value)), |
| snapshot: snapshot(), |
| }), |
| }, |
| }), |
| } |
| }), |
| ), |
| }, |
| ]) |
| } |
|
|
| |
| export function combine(values: ReadonlyArray<SystemContext>): SystemContext { |
| const sources = values.flatMap((value) => value[ContextTypeId]) |
| assertUniqueKeys(sources) |
| return context(sources) |
| } |
|
|
| const observe = (value: SystemContext) => |
| Effect.forEach( |
| value[ContextTypeId], |
| (source) => |
| source.load.pipe( |
| Effect.map( |
| (result): Entry => |
| result === unavailable |
| ? { _tag: "Unavailable", key: source.key } |
| : { _tag: "Available", key: source.key, ...result }, |
| ), |
| ), |
| { concurrency: "unbounded" }, |
| ) |
|
|
| |
| export function initialize(value: SystemContext): Effect.Effect<Generation, InitializationBlocked> { |
| return observe(value).pipe( |
| Effect.flatMap((entries) => { |
| const unavailable = entries.flatMap((entry) => (entry._tag === "Unavailable" ? [entry.key] : [])) |
| if (unavailable.length > 0) return new InitializationBlocked({ keys: unavailable }) |
| return Effect.succeed(initializeObservation(entries)) |
| }), |
| ) |
| } |
|
|
| function initializeObservation(entries: ReadonlyArray<Entry>): Generation { |
| const available = entries.filter((entry): entry is AvailableEntry => entry._tag === "Available") |
| const rendered = available.map((entry) => [entry.key, entry.baseline()] as const) |
| return { |
| baseline: render(rendered.map(([, result]) => result.text)), |
| snapshot: Object.fromEntries(rendered.map(([key, result]) => [key, result.snapshot])), |
| } |
| } |
|
|
| |
| export function reconcile(value: SystemContext, previous: Snapshot): Effect.Effect<ReconcileResult> { |
| return observe(value).pipe( |
| Effect.map((entries): ReconcileResult => { |
| const result = reconcileObservation(entries, previous) |
| if (result._tag === "Unchanged" || result._tag === "Updated") return result |
| return replaceObservation(entries, previous) |
| }), |
| ) |
| } |
|
|
| function reconcileObservation( |
| entries: ReadonlyArray<Entry>, |
| previous: Snapshot, |
| ): { readonly _tag: "Unchanged" } | Updated | { readonly _tag: "Replace" } { |
| const keys = new Set(entries.map((entry) => entry.key)) |
| const comparisons = new Map<Key, Compared>() |
| for (const entry of entries) { |
| if (entry._tag === "Unavailable") continue |
| const stored = getSnapshot(previous, entry.key) |
| if (!stored) continue |
| const compared = entry.compare(stored.value) |
| if (compared._tag === "Incompatible") return { _tag: "Replace" } |
| comparisons.set(entry.key, compared) |
| } |
| for (const key of Object.keys(previous).sort()) { |
| if (keys.has(Key.make(key))) continue |
| if (previous[key].removed === undefined) return { _tag: "Replace" } |
| } |
|
|
| const snapshot: Record<string, SourceSnapshot> = {} |
| const updates: string[] = [] |
| for (const entry of entries) { |
| const stored = getSnapshot(previous, entry.key) |
| if (entry._tag === "Unavailable") { |
| if (stored) snapshot[entry.key] = stored |
| continue |
| } |
| if (!stored) { |
| const rendered = entry.baseline() |
| updates.push(rendered.text) |
| snapshot[entry.key] = rendered.snapshot |
| continue |
| } |
| const compared = comparisons.get(entry.key) |
| if (!compared || compared._tag === "Incompatible") |
| throw new Error(`Missing comparison for system context source ${entry.key}`) |
| if (compared._tag === "Unchanged") { |
| snapshot[entry.key] = stored |
| continue |
| } |
| const rendered = compared.render() |
| updates.push(rendered.text) |
| snapshot[entry.key] = rendered.snapshot |
| } |
| for (const key of Object.keys(previous).sort()) { |
| if (keys.has(Key.make(key))) continue |
| const removed = previous[key].removed |
| if (removed === undefined) throw new Error(`Missing removal rendering for system context source ${key}`) |
| updates.push(removed) |
| } |
| if (updates.length === 0) return { _tag: "Unchanged" } |
| return { _tag: "Updated", text: render(updates), snapshot } |
| } |
|
|
| |
| export function replace(value: SystemContext, previous: Snapshot): Effect.Effect<ReplacementResult> { |
| return observe(value).pipe(Effect.map((entries) => replaceObservation(entries, previous))) |
| } |
|
|
| function replaceObservation(entries: ReadonlyArray<Entry>, previous: Snapshot): ReplacementResult { |
| if (entries.some((entry) => entry._tag === "Unavailable" && getSnapshot(previous, entry.key) !== undefined)) |
| return { _tag: "ReplacementBlocked" } |
| return { _tag: "ReplacementReady", generation: initializeObservation(entries) } |
| } |
|
|
| function context(sources: ReadonlyArray<PackedSource>): SystemContext { |
| return { [ContextTypeId]: sources } |
| } |
|
|
| function render(parts: ReadonlyArray<string>) { |
| return parts.join("\n\n") |
| } |
|
|
| function getSnapshot(snapshot: Snapshot, key: Key) { |
| return Object.hasOwn(snapshot, key) ? snapshot[key] : undefined |
| } |
|
|
| function isUnavailable(value: unknown): value is Unavailable { |
| return value === unavailable |
| } |
|
|
| function requireText(key: Key, kind: string, text: string) { |
| if (text.length === 0) throw new Error(`System context source ${key} rendered an empty ${kind}`) |
| return text |
| } |
|
|
| function assertUniqueKeys(sources: ReadonlyArray<PackedSource>) { |
| const keys = new Set<Key>() |
| for (const source of sources) { |
| if (keys.has(source.key)) throw new DuplicateKeyError({ key: source.key }) |
| keys.add(source.key) |
| } |
| } |
|
|