system-context.ts 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. export * as SystemContext from "./system-context"
  2. import { Effect, Schema } from "effect"
  3. import { Hash } from "./util/hash"
  4. export const Key = Schema.String.check(Schema.isPattern(/^[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._/-]*$/)).pipe(
  5. Schema.brand("SystemContext.Key"),
  6. )
  7. export type Key = typeof Key.Type
  8. export const unavailable = Symbol.for("@opencode/SystemContext.Unavailable")
  9. export type Unavailable = typeof unavailable
  10. export interface Value {
  11. /** Full component text rendered into a new epoch baseline. */
  12. readonly baseline: string
  13. /** Absolute current-state text emitted when this component changes. */
  14. readonly update: string
  15. }
  16. export interface Component<out E = never, out R = never> {
  17. readonly key: Key
  18. readonly load: Effect.Effect<Value | Unavailable, E, R>
  19. }
  20. export interface SystemContext<out E = never, out R = never> {
  21. readonly components: ReadonlyArray<Component<E, R>>
  22. }
  23. export interface AvailableEntry extends Value {
  24. readonly _tag: "Available"
  25. readonly key: Key
  26. readonly hash: string
  27. }
  28. export interface UnavailableEntry {
  29. readonly _tag: "Unavailable"
  30. readonly key: Key
  31. }
  32. export type Entry = AvailableEntry | UnavailableEntry
  33. export interface Snapshot {
  34. readonly entries: ReadonlyArray<Entry>
  35. }
  36. export interface Part {
  37. readonly key: Key
  38. readonly text: string
  39. }
  40. export type Checkpoint = Readonly<Record<string, string>>
  41. export interface Initialized {
  42. readonly baseline: ReadonlyArray<Part>
  43. readonly checkpoint: Checkpoint
  44. }
  45. export interface Refreshed {
  46. readonly changes: ReadonlyArray<Part>
  47. readonly checkpoint: Checkpoint
  48. }
  49. export class DuplicateKeyError extends Schema.TaggedErrorClass<DuplicateKeyError>()("SystemContext.DuplicateKeyError", {
  50. key: Key,
  51. }) {
  52. override get message() {
  53. return `Duplicate system context key: ${this.key}`
  54. }
  55. }
  56. export const value = <E, R>(component: Component<E, R>): Component<E, R> => component
  57. export function struct<E, R>(components: Readonly<Record<string, Component<E, R>>>): SystemContext<E, R> {
  58. const values = Object.values(components)
  59. assertUniqueKeys(values)
  60. return { components: values }
  61. }
  62. export const load = <E, R>(context: SystemContext<E, R>) =>
  63. Effect.sync(() => assertUniqueKeys(context.components)).pipe(
  64. Effect.andThen(
  65. Effect.forEach(context.components, (component) =>
  66. component.load.pipe(
  67. Effect.map(
  68. (result): Entry =>
  69. result === unavailable
  70. ? { _tag: "Unavailable", key: component.key }
  71. : { _tag: "Available", key: component.key, ...result, hash: Hash.sha256(result.update) },
  72. ),
  73. ),
  74. ),
  75. ),
  76. Effect.map((entries): Snapshot => ({ entries })),
  77. )
  78. export function initialize(snapshot: Snapshot): Initialized {
  79. return {
  80. baseline: snapshot.entries.flatMap((entry) =>
  81. entry._tag === "Available" ? [{ key: entry.key, text: entry.baseline }] : [],
  82. ),
  83. checkpoint: nextCheckpoint(snapshot, {}),
  84. }
  85. }
  86. export function refresh(snapshot: Snapshot, previous: Checkpoint): Refreshed {
  87. return {
  88. changes: snapshot.entries.flatMap((entry) =>
  89. entry._tag === "Available" && getCheckpoint(previous, entry.key) !== entry.hash
  90. ? [{ key: entry.key, text: entry.update }]
  91. : [],
  92. ),
  93. checkpoint: nextCheckpoint(snapshot, previous),
  94. }
  95. }
  96. export function render(parts: ReadonlyArray<Part>) {
  97. return parts.map((part) => part.text).join("\n\n")
  98. }
  99. function nextCheckpoint(snapshot: Snapshot, previous: Checkpoint) {
  100. return Object.fromEntries(
  101. snapshot.entries.flatMap((entry) => {
  102. if (entry._tag === "Available") return [[entry.key, entry.hash]]
  103. const hash = getCheckpoint(previous, entry.key)
  104. return hash === undefined ? [] : [[entry.key, hash]]
  105. }),
  106. )
  107. }
  108. function getCheckpoint(checkpoint: Checkpoint, key: Key) {
  109. return Object.hasOwn(checkpoint, key) ? checkpoint[key] : undefined
  110. }
  111. function assertUniqueKeys(components: ReadonlyArray<Component<unknown, unknown>>) {
  112. const keys = new Set<Key>()
  113. for (const component of components) {
  114. if (keys.has(component.key)) throw new DuplicateKeyError({ key: component.key })
  115. keys.add(component.key)
  116. }
  117. }