iterate 0.2.6 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/README.md +86 -76
  2. package/THIRD_PARTY_NOTICES.md +55 -0
  3. package/bin/iterate.js +18 -3
  4. package/dist/api-url-B6404M82.mjs +17 -0
  5. package/dist/api-url-B6404M82.mjs.map +1 -0
  6. package/dist/app-ref-BipL0feU.mjs +35 -0
  7. package/dist/app-ref-BipL0feU.mjs.map +1 -0
  8. package/dist/app-ref-C1CrgXqX.mjs +7 -0
  9. package/dist/app-ref-C1CrgXqX.mjs.map +1 -0
  10. package/dist/app-ref-DYai_om1.mjs +7 -0
  11. package/dist/app-ref-DYai_om1.mjs.map +1 -0
  12. package/dist/cli-D0c-pDL_.mjs +1010 -0
  13. package/dist/cli-D0c-pDL_.mjs.map +1 -0
  14. package/dist/client.d.ts +3 -0
  15. package/dist/client.mjs +4 -0
  16. package/dist/cloudflare-BTm90gQ4.mjs +951 -0
  17. package/dist/cloudflare-BTm90gQ4.mjs.map +1 -0
  18. package/dist/contract-s4FW4eES.mjs +309 -0
  19. package/dist/contract-s4FW4eES.mjs.map +1 -0
  20. package/dist/document-review/index.d.ts +5 -0
  21. package/dist/document-review/types.d.ts +107 -0
  22. package/dist/document-review.mjs +7015 -0
  23. package/dist/document-review.mjs.map +1 -0
  24. package/dist/durable-object-processor-durability-CNsTjAJS.mjs +205 -0
  25. package/dist/durable-object-processor-durability-CNsTjAJS.mjs.map +1 -0
  26. package/dist/idempotency-DleloJNt.mjs +28 -0
  27. package/dist/idempotency-DleloJNt.mjs.map +1 -0
  28. package/dist/index.mjs +1 -1
  29. package/dist/itx/api-url.d.ts +6 -0
  30. package/dist/itx/itx-node-client.d.ts +65 -0
  31. package/dist/itx/itx-session.d.ts +215 -0
  32. package/dist/itx/owned-rpc-session.d.ts +14 -0
  33. package/dist/itx/query-client.d.ts +10 -0
  34. package/dist/itx-api.generated.d.ts +6195 -0
  35. package/dist/itx-session-sjud8GiT.mjs +534 -0
  36. package/dist/itx-session-sjud8GiT.mjs.map +1 -0
  37. package/dist/live-state-BJNqOwFw.mjs +299 -0
  38. package/dist/live-state-BJNqOwFw.mjs.map +1 -0
  39. package/dist/next/api.d.ts +479 -0
  40. package/dist/next/api.mjs +0 -0
  41. package/dist/next/app-server.d.ts +44 -0
  42. package/dist/next/app-server.mjs +479 -0
  43. package/dist/next/app-server.mjs.map +1 -0
  44. package/dist/next/app-session.d.ts +49 -0
  45. package/dist/next/app-session.mjs +238 -0
  46. package/dist/next/app-session.mjs.map +1 -0
  47. package/dist/next/app.d.ts +29 -0
  48. package/dist/next/app.mjs +141 -0
  49. package/dist/next/app.mjs.map +1 -0
  50. package/dist/next/client/live-state.d.ts +63 -0
  51. package/dist/next/client/oauth.d.ts +12 -0
  52. package/dist/next/client/react.d.ts +109 -0
  53. package/dist/next/client/socket.d.ts +6 -0
  54. package/dist/next/client.mjs +156 -0
  55. package/dist/next/client.mjs.map +1 -0
  56. package/dist/next/expression.d.ts +146 -0
  57. package/dist/next/expression.mjs +399 -0
  58. package/dist/next/expression.mjs.map +1 -0
  59. package/dist/next/lib.d.ts +56 -0
  60. package/dist/next/lib.mjs +199 -0
  61. package/dist/next/lib.mjs.map +1 -0
  62. package/dist/next/oauth-scopes.d.ts +32 -0
  63. package/dist/next/oauth-scopes.mjs +40 -0
  64. package/dist/next/oauth-scopes.mjs.map +1 -0
  65. package/dist/next/oauth.mjs +29 -0
  66. package/dist/next/oauth.mjs.map +1 -0
  67. package/dist/next/principal.d.ts +64 -0
  68. package/dist/next/principal.mjs +98 -0
  69. package/dist/next/principal.mjs.map +1 -0
  70. package/dist/next/project-ingress.d.ts +37 -0
  71. package/dist/next/project-ingress.mjs +75 -0
  72. package/dist/next/project-ingress.mjs.map +1 -0
  73. package/dist/next/react.mjs +285 -0
  74. package/dist/next/react.mjs.map +1 -0
  75. package/dist/next/sdk/auth.d.ts +5 -0
  76. package/dist/next/sdk/index.d.ts +112 -0
  77. package/dist/next/sdk.mjs +139 -0
  78. package/dist/next/sdk.mjs.map +1 -0
  79. package/dist/next/stream/processor.d.ts +378 -0
  80. package/dist/next/stream/processor.mjs +582 -0
  81. package/dist/next/stream/processor.mjs.map +1 -0
  82. package/dist/next/stream/run.d.ts +58 -0
  83. package/dist/next/stream/run.mjs +40 -0
  84. package/dist/next/stream/run.mjs.map +1 -0
  85. package/dist/next-node.d.ts +15 -0
  86. package/dist/next-node.mjs +51 -0
  87. package/dist/next-node.mjs.map +1 -0
  88. package/dist/node.d.ts +3 -0
  89. package/dist/node.mjs +185 -0
  90. package/dist/node.mjs.map +1 -0
  91. package/dist/processor-host-capabilities-BMFH3KTM.mjs +56 -0
  92. package/dist/processor-host-capabilities-BMFH3KTM.mjs.map +1 -0
  93. package/dist/processors/cloudflare.d.ts +3 -0
  94. package/dist/processors/durable-object-processor-durability.d.ts +79 -0
  95. package/dist/processors/event-consumption-metrics.d.ts +82 -0
  96. package/dist/processors/idempotency.d.ts +13 -0
  97. package/dist/processors/index.d.ts +12 -0
  98. package/dist/processors/processor-contracts.d.ts +342 -0
  99. package/dist/processors/processor-facet.d.ts +186 -0
  100. package/dist/processors/processor-host-capabilities.d.ts +60 -0
  101. package/dist/processors/prompt-sections.d.ts +17 -0
  102. package/dist/processors/rpc-types.d.ts +515 -0
  103. package/dist/processors/schemas.d.ts +102 -0
  104. package/dist/processors/stream-handle.d.ts +45 -0
  105. package/dist/processors/stream-processor-keepalive.d.ts +95 -0
  106. package/dist/processors/stream-processor-registry.d.ts +233 -0
  107. package/dist/processors/stream-processor-runner.d.ts +289 -0
  108. package/dist/processors/stream-processor.d.ts +339 -0
  109. package/dist/processors/stream-runtime-metrics.d.ts +107 -0
  110. package/dist/processors/testing.d.ts +302 -0
  111. package/dist/processors-BoNyeBfQ.mjs +10 -0
  112. package/dist/processors-BoNyeBfQ.mjs.map +1 -0
  113. package/dist/processors-cloudflare.mjs +3 -0
  114. package/dist/processors-testing.mjs +435 -0
  115. package/dist/processors-testing.mjs.map +1 -0
  116. package/dist/processors.mjs +52 -0
  117. package/dist/processors.mjs.map +1 -0
  118. package/dist/protocol-DnK_f2m6.mjs +251 -0
  119. package/dist/protocol-DnK_f2m6.mjs.map +1 -0
  120. package/dist/sdk/capnweb/index.d.ts +2 -0
  121. package/dist/sdk/capnweb/live-state/compact.d.ts +5 -0
  122. package/dist/sdk/capnweb/live-state/diff.d.ts +41 -0
  123. package/dist/sdk/capnweb/live-state/engine.d.ts +44 -0
  124. package/dist/sdk/capnweb/live-state/index.d.ts +41 -0
  125. package/dist/sdk/capnweb/live-state/protocol.d.ts +87 -0
  126. package/dist/sdk/capnweb/live-state/retain.d.ts +23 -0
  127. package/dist/sdk/capnweb/live-state/store.d.ts +20 -0
  128. package/dist/sdk/capnweb/live-state/types.d.ts +11 -0
  129. package/dist/sdk/capnweb/react.d.ts +45 -0
  130. package/dist/sdk/capnweb/react.mjs +316 -0
  131. package/dist/sdk/capnweb/react.mjs.map +1 -0
  132. package/dist/sdk/capnweb.mjs +4 -0
  133. package/dist/sdk/itx/react.d.ts +191 -0
  134. package/dist/sdk/itx/react.mjs +383 -0
  135. package/dist/sdk/itx/react.mjs.map +1 -0
  136. package/dist/sdk-DMB-IM11.mjs +933 -0
  137. package/dist/sdk-DMB-IM11.mjs.map +1 -0
  138. package/dist/sdk.d.ts +339 -0
  139. package/dist/sdk.mjs +2 -0
  140. package/dist/serve-itx.d.ts +46 -0
  141. package/dist/starter-apps/flake-dashboard/app-ref.d.ts +31 -0
  142. package/dist/starter-apps/flake-dashboard/configured-worker.mjs +1055 -0
  143. package/dist/starter-apps/flake-dashboard/configured-worker.mjs.map +1 -0
  144. package/dist/starter-apps/flake-dashboard/contract.d.ts +4839 -0
  145. package/dist/starter-apps/flake-dashboard/contract.mjs +2 -0
  146. package/dist/starter-apps/flake-dashboard/index.d.ts +17 -0
  147. package/dist/starter-apps/flake-dashboard/index.mjs +56 -0
  148. package/dist/starter-apps/flake-dashboard/index.mjs.map +1 -0
  149. package/dist/starter-apps/flake-dashboard/worker.d.ts +4607 -0
  150. package/dist/starter-apps/github-ai-linter/ai-linter.d.ts +8914 -0
  151. package/dist/starter-apps/github-ai-linter/configured-worker.mjs +17987 -0
  152. package/dist/starter-apps/github-ai-linter/configured-worker.mjs.map +1 -0
  153. package/dist/starter-apps/github-ai-linter/contract.d.ts +9193 -0
  154. package/dist/starter-apps/github-ai-linter/index.d.ts +10 -0
  155. package/dist/starter-apps/github-ai-linter/index.mjs +36 -0
  156. package/dist/starter-apps/github-ai-linter/index.mjs.map +1 -0
  157. package/dist/starter-apps/github-ai-linter/prompt.d.ts +13 -0
  158. package/dist/starter-apps/github-ai-linter/review-bot.d.ts +808 -0
  159. package/dist/starter-apps/github-ai-linter/rules.d.ts +34 -0
  160. package/dist/starter-apps/github-ai-linter/worker-ref.d.ts +19 -0
  161. package/dist/starter-apps/github-ai-linter/worker.d.ts +19 -0
  162. package/dist/starter-apps/github-ai-linter/worker.mjs +947 -0
  163. package/dist/starter-apps/github-ai-linter/worker.mjs.map +1 -0
  164. package/dist/starter-apps/guestbook/app-ref.d.ts +27 -0
  165. package/dist/starter-apps/guestbook/client.d.ts +7 -0
  166. package/dist/starter-apps/guestbook/client.mjs +59 -0
  167. package/dist/starter-apps/guestbook/configured-worker.mjs +205 -0
  168. package/dist/starter-apps/guestbook/configured-worker.mjs.map +1 -0
  169. package/dist/starter-apps/guestbook/index.d.ts +9 -0
  170. package/dist/starter-apps/guestbook/index.mjs +31 -0
  171. package/dist/starter-apps/guestbook/index.mjs.map +1 -0
  172. package/dist/starter-apps/guestbook/processor.d.ts +2267 -0
  173. package/dist/starter-apps/guestbook/worker.d.ts +26 -0
  174. package/dist/starter-apps/guestbook/worker.mjs +191 -0
  175. package/dist/starter-apps/guestbook/worker.mjs.map +1 -0
  176. package/dist/starter-apps/media/configured-worker.mjs +577 -0
  177. package/dist/starter-apps/media/configured-worker.mjs.map +1 -0
  178. package/dist/starter-apps/media/index.mjs +36 -0
  179. package/dist/starter-apps/media/index.mjs.map +1 -0
  180. package/dist/starter-apps/media/ref.mjs +20 -0
  181. package/dist/starter-apps/media/ref.mjs.map +1 -0
  182. package/dist/starter-apps/media/worker.mjs +579 -0
  183. package/dist/starter-apps/media/worker.mjs.map +1 -0
  184. package/dist/starter-apps/notes/configured-worker.mjs +6134 -0
  185. package/dist/starter-apps/notes/configured-worker.mjs.map +1 -0
  186. package/dist/starter-apps/notes/index.mjs +23 -0
  187. package/dist/starter-apps/notes/index.mjs.map +1 -0
  188. package/dist/starter-apps/notes/ref.mjs +21 -0
  189. package/dist/starter-apps/notes/ref.mjs.map +1 -0
  190. package/dist/starter-apps/notes/worker.mjs +427 -0
  191. package/dist/starter-apps/notes/worker.mjs.map +1 -0
  192. package/dist/starter-apps/todo/client.mjs +59 -0
  193. package/dist/starter-apps/todo/configured-worker.mjs +2864 -0
  194. package/dist/starter-apps/todo/configured-worker.mjs.map +1 -0
  195. package/dist/starter-apps/todo/index.d.ts +8 -0
  196. package/dist/starter-apps/todo/index.mjs +29 -0
  197. package/dist/starter-apps/todo/index.mjs.map +1 -0
  198. package/dist/stream-processor-keepalive-DAQTP6m3.mjs +2082 -0
  199. package/dist/stream-processor-keepalive-DAQTP6m3.mjs.map +1 -0
  200. package/dist/usingCtx-inzbY1Qz.mjs +57 -0
  201. package/dist/usingCtx-mZx5nsAW.mjs +11800 -0
  202. package/dist/usingCtx-mZx5nsAW.mjs.map +1 -0
  203. package/dist/worker-ref-DZxPDmb_.mjs +390 -0
  204. package/dist/worker-ref-DZxPDmb_.mjs.map +1 -0
  205. package/menubar/Iterate.entitlements +12 -0
  206. package/menubar/Iterate.swift +914 -0
  207. package/menubar/IterateIcon.swift +145 -0
  208. package/menubar/README.md +28 -0
  209. package/menubar/build-menubar-app.sh +59 -0
  210. package/package.json +235 -18
  211. package/dist/cli-DMS4kJph.mjs +0 -868
  212. package/dist/cli-DMS4kJph.mjs.map +0 -1
  213. package/dist/config-DtnR7Lv7.mjs +0 -170
  214. package/dist/config-DtnR7Lv7.mjs.map +0 -1
  215. package/dist/index.d.mts.map +0 -1
  216. package/dist/stream-tui/agent-chat-terminal.d.mts +0 -1
  217. package/dist/stream-tui/agent-chat-terminal.mjs +0 -933
  218. package/dist/stream-tui/agent-chat-terminal.mjs.map +0 -1
@@ -0,0 +1,582 @@
1
+ import { codedError, diff, jsonEqual, reportIssue } from "../lib.mjs";
2
+ import "zod";
3
+ //#region src/next/stream/processor.ts
4
+ /** How long after an attempt starts a dead host is revived: the recovery bound. A revive that finds
5
+ * the attempt still in flight claims again with the delay doubled, up to `REVIVE_AFTER_MAX_MS`. */
6
+ const REVIVE_AFTER_MS = 2e4;
7
+ const REVIVE_AFTER_MAX_MS = 30 * 6e4;
8
+ /** THE ONE consumes rule — the processor engine, the subscription delivery loop, and the inline
9
+ * reduces all call this; there is no second copy to drift. `consumes` undefined = every durable event
10
+ * (a subscriber's default). "*" = every durable event. A NAMED type opts that type in, INCLUDING
11
+ * ephemerals ("*" NEVER sweeps ephemerals) — so a live-state watcher spells
12
+ * `consumes: ["events.iterate.com/live-state/changed"]` and filters `payload.key` itself. The wake
13
+ * record (`stream/woken`) is a durable event like any other: a "*" row receives every incarnation's. */
14
+ function consumesEvent(consumes, event) {
15
+ if (event.ephemeral) return consumes?.includes(event.type) ?? false;
16
+ return !consumes || consumes.includes("*") || consumes.includes(event.type);
17
+ }
18
+ /** What the ENGINE reduces: the contract's consumes, minus the one type no processor may ever reduce or
19
+ * react to — a live-state delta. Deltas are notifications ABOUT state; letting one feed a reduce is
20
+ * the feedback-loop class, made unspellable here rather than discouraged. */
21
+ const reducesEvent = (consumes, event) => event.type !== "events.iterate.com/live-state/changed" && consumesEvent(consumes, event);
22
+ /** THE AUTHOR CLASS: a contract, three hooks and one helper. Deps an effect needs arrive through
23
+ * the subclass's own constructor, as for any class. One instance lives as long as its host; a field
24
+ * on it is RUNTIME state (gone with the host), which `projectLiveState` may reduce into the live view. */
25
+ var StreamProcessor = class {
26
+ /** Pure reduce. Return the NEXT state (a new object) — or null/undefined to keep the current. The
27
+ * `Event` type param — a discriminated union of the events the contract consumes — narrows
28
+ * `event.payload` per `event.type` inside the body, so no cast is needed; it defaults to the
29
+ * untyped `StreamEvent` for processors that don't declare one. */
30
+ reduce(_args) {}
31
+ /** Side-effect hook. Synchronous by design: register async work via the two helpers on args.
32
+ * `append`/`appendTo` take what THIS class's `contract` emits (`EmittedEventInput<this["contract"]>`:
33
+ * a subclass whose `contract` is a defined one gets each emitted type's payload as its catalog
34
+ * spells it; the base, and a hand-built contract, take any input). */
35
+ processEvent(_args) {}
36
+ /** The live-state PROJECTION — the shape clients see and the diffs are computed over. DEFAULT: the
37
+ * reduced state verbatim, so every processor is live out of the box; that is deliberate — the
38
+ * delta is an EPHEMERAL event, so "always live" costs an offset and a cheap diff, nothing durable.
39
+ * Override to redact, or to REDUCE IN RUNTIME FIELDS (`return { ...state, lastSeenMs: this.lastSeenMs }`);
40
+ * the engine re-projects after EVERY batch, and a field changed outside a batch needs the host's
41
+ * `publishLiveState()`. */
42
+ projectLiveState(state) {
43
+ return state;
44
+ }
45
+ /** Stable idempotency key namespaced by slug; pass the event being processed for a per-event key. */
46
+ idempotencyKey(key, event) {
47
+ return event ? `${this.contract.slug}/${key}@${event.offset}` : `${this.contract.slug}/${key}`;
48
+ }
49
+ };
50
+ /** THE ENGINE: everything below the author's three hooks — the serial chain, the checkpoint, gap
51
+ * repair, the at-head pass, version re-reduces, live-state publishing. Constructed by the host
52
+ * (`StreamProcessorDurableObject`; a test with the stand-ins in test-support.ts). */
53
+ var ProcessorEngine = class {
54
+ processor;
55
+ #contract;
56
+ #stream;
57
+ #storage;
58
+ /** Rule 1: every batch runs on this chain, one after another. */
59
+ #serialBatchChain = Promise.resolve();
60
+ /** The reduced state and the durable offset it was reduced through — checkpointed on the batches
61
+ * that carried a durable. */
62
+ #reducedState;
63
+ #reducedThroughOffset;
64
+ /** A checkpoint found under ANOTHER contract version: the input to the one re-reduce the chain
65
+ * runs before anything else; cleared once it ran. */
66
+ #staleCheckpoint;
67
+ /** The highest `range.through` ever SHOWN to this processor (see processEventBatch). */
68
+ #pushedThroughOffset;
69
+ /** A refusal that can only repeat — the checkpoint over its cell (stamped `retryable: false`):
70
+ * LATCHED for this incarnation, so every later batch, catch-up and read verb rejects with it at
71
+ * once instead of re-reducing into the same wall on every push and wake. A fresh incarnation
72
+ * tries once more. */
73
+ #latchedRefusal;
74
+ /** waitUntilProcessed's waiting callers, resolved as the cursor advances. */
75
+ #waitUntilProcessedWaiters = [];
76
+ /** Born with the engine, so its epoch is minted once per incarnation. */
77
+ #liveState;
78
+ /** Rule 3's claim: attempts in flight, and how many revives found one still in flight (the
79
+ * backoff of the next claim; reset when the last attempt settles). The claim calls ride ONE
80
+ * chain, so a release never overtakes the claim of the attempt that followed it. */
81
+ #backgroundWorkInFlight = 0;
82
+ #revivesWhileBusy = 0;
83
+ #claimChain = Promise.resolve();
84
+ /** Whether the last batch this engine ran carried the at-head pass (rule 5) — what `revive()`
85
+ * reads to know if its catch-up already ran one. */
86
+ #lastBatchAtHead = false;
87
+ constructor(processor, deps) {
88
+ this.processor = processor;
89
+ this.#contract = processor.contract;
90
+ this.#stream = deps.stream;
91
+ this.#storage = deps.storage;
92
+ const { slug, version } = this.#contract;
93
+ const checkpoint = this.#storage.read(slug);
94
+ if (checkpoint?.reducerVersion === version) {
95
+ this.#reducedState = checkpoint.state ?? this.#contract.initialState();
96
+ this.#reducedThroughOffset = checkpoint.reducedThroughOffset;
97
+ } else {
98
+ this.#reducedState = this.#contract.initialState();
99
+ this.#reducedThroughOffset = 0;
100
+ if (checkpoint) this.#staleCheckpoint = {
101
+ reducedThroughOffset: checkpoint.reducedThroughOffset,
102
+ state: checkpoint.state ?? this.#reducedState
103
+ };
104
+ }
105
+ let seed;
106
+ try {
107
+ seed = processor.projectLiveState(this.#staleCheckpoint ? this.#staleCheckpoint.state : this.#reducedState);
108
+ } catch (error) {
109
+ reportIssue("processor.live-state", error, { slug });
110
+ seed = void 0;
111
+ }
112
+ this.#liveState = new LiveState(this.#stream, slug, seed);
113
+ }
114
+ /** THE SEED DOOR for live-state clients (LiveState.snapshot), caught up first. */
115
+ async liveSnapshot() {
116
+ if (!this.#reducedThroughPushedHead()) await this.catchUpFromLog();
117
+ return this.#liveState.snapshot();
118
+ }
119
+ /** Emit a delta for the CURRENT projection (reduced + any runtime fields) if it changed. The engine
120
+ * calls this after every batch; the host calls it after a runtime field moved outside a batch. A
121
+ * throwing projection loses only its notification (the client re-seeds on the chain gap). */
122
+ publishLiveState() {
123
+ let projection;
124
+ try {
125
+ projection = this.processor.projectLiveState(this.#reducedState);
126
+ } catch (error) {
127
+ reportIssue("processor.live-state", error, { slug: this.#contract.slug });
128
+ return;
129
+ }
130
+ this.#liveState.set(projection);
131
+ }
132
+ /** THE push door: contiguous → reduce it directly (no read); anything else → gap repair from the
133
+ * own cursor first. Fire-and-forget safe: enqueues on the serial chain. */
134
+ processEventBatch(events, range) {
135
+ this.#pushedThroughOffset = Math.max(this.#pushedThroughOffset ?? 0, range.through);
136
+ return this.#runOnSerialChain(async () => {
137
+ await this.#rereduceIfVersionChanged();
138
+ while (this.#reducedThroughOffset < range.after) {
139
+ const after = this.#reducedThroughOffset;
140
+ const page = await this.#stream.read(after, 500);
141
+ if (page.scannedThroughOffset <= after) break;
142
+ await this.#reduceAndCommitEventBatch(page.events.filter((event) => event.offset <= range.after), {
143
+ after,
144
+ through: Math.min(page.scannedThroughOffset, range.after)
145
+ }, false);
146
+ }
147
+ await this.#reduceAndCommitEventBatch(events, range, range.through >= this.#pushedThroughOffset);
148
+ });
149
+ }
150
+ /** Catch up from the own checkpoint (a cold boot, the read verbs, the barrier), page by page — a
151
+ * failed batch, a missed push, or a fresh incarnation can never skip a durable event. */
152
+ catchUpFromLog() {
153
+ return this.#runOnSerialChain(async () => {
154
+ await this.#rereduceIfVersionChanged();
155
+ for (;;) {
156
+ const after = this.#reducedThroughOffset;
157
+ const page = await this.#stream.read(after, 500);
158
+ if (page.scannedThroughOffset <= after) return;
159
+ await this.#reduceAndCommitEventBatch(page.events, {
160
+ after,
161
+ through: page.scannedThroughOffset
162
+ }, page.atHead);
163
+ if (page.atHead) return;
164
+ }
165
+ });
166
+ }
167
+ /** Reduce-and-effects caught up through the log, then `{ offset, state }`. */
168
+ async snapshot() {
169
+ if (!this.#reducedThroughPushedHead()) await this.catchUpFromLog();
170
+ return {
171
+ offset: this.#reducedThroughOffset,
172
+ state: this.#reducedState
173
+ };
174
+ }
175
+ /** Provably reduced through the head SHOWN so far → the read verbs skip their catch-up read. */
176
+ #reducedThroughPushedHead() {
177
+ return this.#pushedThroughOffset !== void 0 && this.#reducedThroughOffset >= this.#pushedThroughOffset;
178
+ }
179
+ /** THE barrier verb (read-your-writes): resolves once processed AT LEAST through `offset`. An
180
+ * offset ABOVE the durable mark (an ephemeral's) is reached only if this processor was pushed it —
181
+ * the log cannot prove past the mark, so a wake alone never advances there. */
182
+ waitUntilProcessed(input) {
183
+ const { offset, timeoutMs = 1e4 } = input;
184
+ return new Promise((resolve, reject) => {
185
+ if (this.#reducedThroughOffset >= offset) return resolve();
186
+ const waiter = {
187
+ offset,
188
+ resolve: () => {
189
+ clearTimeout(timer);
190
+ resolve();
191
+ }
192
+ };
193
+ const timer = setTimeout(() => {
194
+ this.#waitUntilProcessedWaiters.splice(this.#waitUntilProcessedWaiters.indexOf(waiter), 1);
195
+ reject(/* @__PURE__ */ new Error(`processor "${this.#contract.slug}" did not reach offset ${offset} in ${timeoutMs}ms`));
196
+ }, timeoutMs);
197
+ this.#waitUntilProcessedWaiters.push(waiter);
198
+ this.catchUpFromLog().catch((error) => {
199
+ const i = this.#waitUntilProcessedWaiters.indexOf(waiter);
200
+ if (i === -1) return;
201
+ this.#waitUntilProcessedWaiters.splice(i, 1);
202
+ clearTimeout(timer);
203
+ reject(error instanceof Error ? error : new Error(String(error)));
204
+ });
205
+ });
206
+ }
207
+ /** Serialize on the chain. THE RULE: never await your own chain from inside a batch — a
208
+ * processor that appends during its batch would deadlock, which is why every append→drive
209
+ * caller is fire-and-forget. */
210
+ #runOnSerialChain(work) {
211
+ const run = this.#serialBatchChain.then(() => {
212
+ if (this.#latchedRefusal) throw this.#latchedRefusal;
213
+ return work();
214
+ });
215
+ this.#serialBatchChain = run.catch(() => {});
216
+ return run;
217
+ }
218
+ /** The one-time cost of a contract version bump: re-reduce the durable log from offset 0 through
219
+ * the OLD cursor (`reduce` only — those effects already ran) and checkpoint under the new
220
+ * version. Never past the old cursor: re-reducing to the head instead would judge an
221
+ * already-queued in-flight push stale and swallow its effects. */
222
+ async #rereduceIfVersionChanged() {
223
+ if (!this.#staleCheckpoint) return;
224
+ const target = this.#staleCheckpoint.reducedThroughOffset;
225
+ let state = this.#contract.initialState();
226
+ let reducedThroughOffset = 0;
227
+ while (reducedThroughOffset < target) {
228
+ const page = await this.#stream.read(reducedThroughOffset, 500);
229
+ for (const event of page.events) if (event.offset <= target && reducesEvent(this.#contract.consumes, event)) state = this.#validateNormalizeAndReduce(event, state).state;
230
+ if (page.scannedThroughOffset <= reducedThroughOffset) break;
231
+ reducedThroughOffset = Math.min(page.scannedThroughOffset, target);
232
+ }
233
+ this.#writeCheckpointOrLatch(this.#contract.slug, {
234
+ reducerVersion: this.#contract.version,
235
+ reducedThroughOffset: target
236
+ }, state, true);
237
+ this.#reducedState = state;
238
+ this.#reducedThroughOffset = target;
239
+ this.#staleCheckpoint = void 0;
240
+ this.publishLiveState();
241
+ this.#resolveWaitUntilProcessedWaiters(target);
242
+ }
243
+ /** Rules 2–5 over one range (the caller has healed any durable prefix gap first). DURABLES reduce
244
+ * at-most-once (`offset > cursor`); EPHEMERALS ALWAYS deliver — each rides exactly one push and
245
+ * can never be a redelivery, so a durable-only wake that clamped the cursor PAST an ephemeral
246
+ * offset must not suppress it. The cursor is a DURABLE-reduce watermark and never regresses. */
247
+ async #reduceAndCommitEventBatch(events, range, atHead) {
248
+ const reducedThroughOffsetBefore = this.#reducedThroughOffset;
249
+ const stateBefore = this.#reducedState;
250
+ let state = stateBefore;
251
+ const consumableEvents = events.filter((event) => reducesEvent(this.#contract.consumes, event) && (event.ephemeral || event.offset > reducedThroughOffsetBefore));
252
+ let caughtUpDelivered = false;
253
+ for (let i = 0; i < consumableEvents.length; i++) {
254
+ const last = i === consumableEvents.length - 1;
255
+ const r = await this.#reduceAndProcessEvent(consumableEvents[i], state, atHead && last);
256
+ state = r.state;
257
+ if (atHead && last && r.processed) caughtUpDelivered = true;
258
+ }
259
+ if (atHead && !caughtUpDelivered) state = (await this.#reduceAndProcessEvent(null, state, true)).state;
260
+ const reducedThroughOffset = Math.max(reducedThroughOffsetBefore, range.through);
261
+ const advanced = reducedThroughOffset > reducedThroughOffsetBefore;
262
+ if (events.some((event) => !event.ephemeral) && advanced) this.#writeCheckpointOrLatch(this.#contract.slug, {
263
+ reducerVersion: this.#contract.version,
264
+ reducedThroughOffset
265
+ }, state, state !== stateBefore);
266
+ this.#reducedState = state;
267
+ this.#reducedThroughOffset = reducedThroughOffset;
268
+ this.#lastBatchAtHead = atHead;
269
+ this.#resolveWaitUntilProcessedWaiters(reducedThroughOffset);
270
+ this.publishLiveState();
271
+ }
272
+ /** Start an attempt: the first in flight claims the alarm (not awaited — a claim that has not
273
+ * landed when the host dies revives nothing either way, and the attempt must not wait on it);
274
+ * the last to settle releases the claim. */
275
+ #runInBackground(work) {
276
+ this.#backgroundWorkInFlight += 1;
277
+ if (this.#backgroundWorkInFlight === 1) this.#claim(REVIVE_AFTER_MS);
278
+ work().catch((error) => reportIssue("processor.background", error, { slug: this.#contract.slug })).finally(() => {
279
+ this.#backgroundWorkInFlight -= 1;
280
+ if (this.#backgroundWorkInFlight === 0) {
281
+ this.#revivesWhileBusy = 0;
282
+ this.#claim(null);
283
+ }
284
+ });
285
+ }
286
+ #claim(afterMs) {
287
+ const at = afterMs === null ? null : Date.now() + afterMs;
288
+ this.#claimChain = this.#claimChain.then(() => this.#stream.claim(at)).catch((error) => reportIssue("processor.claim", error, { slug: this.#contract.slug }));
289
+ }
290
+ /** THE REVIVE — the context's alarm pass calls this for a due claim (spent by then): catch up
291
+ * from the log and run the at-head pass, so a processor restarts what state says is still owed
292
+ * (rule 3). A fresh incarnation finds nothing in flight and starts it; an attempt still in flight
293
+ * here claims again, later each time (20 s, 40 s, … `REVIVE_AFTER_MAX_MS`). */
294
+ async revive() {
295
+ this.#lastBatchAtHead = false;
296
+ await this.catchUpFromLog();
297
+ if (!this.#lastBatchAtHead) await this.#runOnSerialChain(async () => {
298
+ this.#reducedState = (await this.#reduceAndProcessEvent(null, this.#reducedState, true)).state;
299
+ this.publishLiveState();
300
+ });
301
+ if (this.#backgroundWorkInFlight === 0) return;
302
+ this.#revivesWhileBusy += 1;
303
+ this.#claim(Math.min(REVIVE_AFTER_MS * 2 ** this.#revivesWhileBusy, REVIVE_AFTER_MAX_MS));
304
+ await this.#claimChain;
305
+ }
306
+ /** THE GUARDED REDUCE, shared by the live flow and the version replay. A reducer that throws on an
307
+ * event (malformed, or one an OLDER version accepted) must never wedge the processor: on a version
308
+ * replay it would fail the catch-up before the new checkpoint is written, every incarnation. */
309
+ #reduceOrKeep(event, state) {
310
+ try {
311
+ return this.processor.reduce({
312
+ event,
313
+ state
314
+ }) ?? state;
315
+ } catch (error) {
316
+ reportIssue("processor.reduce", error, {
317
+ slug: this.#contract.slug,
318
+ offset: event.offset
319
+ });
320
+ return state;
321
+ }
322
+ }
323
+ /** Validate a consumed event's payload against the contract's declared schema, then reduce it — or,
324
+ * for a malformed payload, skip the fold and report (it must never corrupt reduced state, the
325
+ * exported view a live client parses). Returns the next state AND the event to carry onward,
326
+ * NORMALIZED to the schema's `z.output` (coercions/defaults applied) when it validated — so the
327
+ * reducer, the effect hook, and the version replay all see exactly what `ConsumedEvent<Contract>`
328
+ * promises. SHARED by the live flow and `#rereduceIfVersionChanged`, so the two can never diverge
329
+ * (a coercion applied live but not on replay would make a version bump rewrite state). A payload-less
330
+ * event validates as `{}` (the "empty defaults" convention the contract requires of its stateSchema);
331
+ * a contract with no `events` catalog (the kernel-generic processors) folds unvalidated, as before. */
332
+ #validateNormalizeAndReduce(event, state) {
333
+ const parsed = this.#contract.payloadSchemaFor?.(event.type)?.safeParse(event.payload ?? {});
334
+ if (parsed && !parsed.success) {
335
+ reportIssue("processor.reduce.payload", parsed.error, {
336
+ slug: this.#contract.slug,
337
+ offset: event.offset,
338
+ type: event.type
339
+ });
340
+ return {
341
+ state,
342
+ event,
343
+ valid: false
344
+ };
345
+ }
346
+ const normalized = parsed ? {
347
+ ...event,
348
+ payload: parsed.data
349
+ } : event;
350
+ return {
351
+ state: this.#reduceOrKeep(normalized, state),
352
+ event: normalized,
353
+ valid: true
354
+ };
355
+ }
356
+ /** THE per-event primitive (rules 2–3) — the batch loop and the eventless at-head pass both come
357
+ * here: a GUARDED reduce, then `processEvent` with a FIFO blocker chain drained to a FIXED POINT.
358
+ * Returns the next state and whether the effect ran — a malformed payload for a KNOWN event is
359
+ * SKIPPED for BOTH reduce and effect (the effect hook is typed against `ConsumedEvent`'s `z.output`,
360
+ * so handing it garbage would throw and wedge the batch — checkpoints never advance, catch-up
361
+ * refails the same row); `processed: false` lets the batch fall back to the eventless caught-up pass.
362
+ * Owns NO cursor / persist / waiter — the caller does. */
363
+ async #reduceAndProcessEvent(event, state, caughtUp) {
364
+ const { slug, version, emits } = this.#contract;
365
+ const previousState = state;
366
+ if (event) {
367
+ const reduced = this.#validateNormalizeAndReduce(event, state);
368
+ if (!reduced.valid) return {
369
+ state: reduced.state,
370
+ processed: false
371
+ };
372
+ state = reduced.state;
373
+ event = reduced.event;
374
+ }
375
+ let blockers = Promise.resolve();
376
+ const stamped = (emittedEvents) => {
377
+ for (const emitted of emittedEvents) {
378
+ if (!emits.includes(emitted.type)) throw new Error(`processor "${slug}" emits ${JSON.stringify(emitted.type)} without declaring it`);
379
+ emitted.source = { processor: {
380
+ slug,
381
+ version,
382
+ ...event && { whileProcessing: {
383
+ offset: event.offset,
384
+ type: event.type
385
+ } }
386
+ } };
387
+ }
388
+ return emittedEvents;
389
+ };
390
+ this.processor.processEvent({
391
+ event,
392
+ state,
393
+ previousState,
394
+ append: async (...emittedEvents) => await this.#stream.append(...stamped(emittedEvents)),
395
+ appendTo: async (path, ...emittedEvents) => {
396
+ if (!this.#stream.appendTo) throw new Error(`processor "${slug}": this host reaches no other context (appendTo)`);
397
+ return await this.#stream.appendTo(path, ...stamped(emittedEvents));
398
+ },
399
+ blockProcessorWhile: (work) => {
400
+ blockers = blockers.then(() => work());
401
+ },
402
+ runInBackground: (work) => this.#runInBackground(work),
403
+ delivery: { caughtUp }
404
+ });
405
+ for (let awaited; awaited !== blockers;) {
406
+ awaited = blockers;
407
+ await awaited;
408
+ }
409
+ return {
410
+ state,
411
+ processed: true
412
+ };
413
+ }
414
+ /** The checkpoint write, with the latch: a refusal stamped `retryable: false` can only repeat. */
415
+ #writeCheckpointOrLatch(slug, cursor, state, stateChanged) {
416
+ try {
417
+ this.#storage.write(slug, cursor, state, stateChanged);
418
+ } catch (error) {
419
+ if (error?.retryable === false) this.#latchedRefusal = error instanceof Error ? error : new Error(String(error));
420
+ throw error;
421
+ }
422
+ }
423
+ /** Resolve the waiters a cursor advance satisfies; keep the rest. */
424
+ #resolveWaitUntilProcessedWaiters(reducedThroughOffset) {
425
+ for (const w of this.#waitUntilProcessedWaiters.splice(0)) if (reducedThroughOffset >= w.offset) w.resolve();
426
+ else this.#waitUntilProcessedWaiters.push(w);
427
+ }
428
+ };
429
+ function idempotencyConflictMessage(idempotencyKey, existingOffset) {
430
+ return `idempotency key "${idempotencyKey}" already names a different event at offset ${existingOffset}`;
431
+ }
432
+ /** Structural equality of the parts an idempotent retry must not change. */
433
+ function sameIdempotentEvent(existingEvent, requestedEvent) {
434
+ return existingEvent.type === requestedEvent.type && jsonEqual(existingEvent.payload, requestedEvent.payload) && jsonEqual(existingEvent.metadata, requestedEvent.metadata);
435
+ }
436
+ /** Under the 2 MB cell, with room for the row's other columns. */
437
+ const REDUCE_CHECKPOINT_STATE_MAX_CHARS = 2 * 1024 * 1024 - 4096;
438
+ /** What BOTH hosts read and write their checkpoints through — the stream's storage and a facet's
439
+ * own (the unit lane drives it over node:sqlite, stream/test-support.ts). */
440
+ var ReduceCheckpointTable = class ReduceCheckpointTable {
441
+ #sql;
442
+ /** `createTable: false` when the caller knows the table exists (the stream's storage skips every
443
+ * CREATE on a re-wake); a facet host constructs one per incarnation and lets it create. */
444
+ constructor(sql, options = { createTable: true }) {
445
+ this.#sql = sql;
446
+ if (options.createTable) ReduceCheckpointTable.createTable(sql);
447
+ }
448
+ static createTable(sql) {
449
+ sql.exec(`CREATE TABLE IF NOT EXISTS reduce_checkpoints (
450
+ slug TEXT PRIMARY KEY,
451
+ reducer_version TEXT NOT NULL,
452
+ reduced_through_offset INTEGER NOT NULL,
453
+ state TEXT
454
+ )`);
455
+ }
456
+ read(slug) {
457
+ const row = this.#sql.exec("SELECT reducer_version, reduced_through_offset, state FROM reduce_checkpoints WHERE slug = ?", slug).toArray()[0];
458
+ if (!row) return void 0;
459
+ return {
460
+ reducerVersion: String(row.reducer_version),
461
+ reducedThroughOffset: Number(row.reduced_through_offset),
462
+ state: row.state ? JSON.parse(String(row.state)) : void 0
463
+ };
464
+ }
465
+ /** ALWAYS the cursor; the state ONLY when `stateChanged` — one write either way. */
466
+ write(slug, cursor, state, stateChanged) {
467
+ const serializedState = stateChanged ? JSON.stringify(state) ?? null : null;
468
+ if (serializedState && serializedState.length > REDUCE_CHECKPOINT_STATE_MAX_CHARS) throw Object.assign(codedError("REDUCE_CHECKPOINT_TOO_LARGE", `checkpoint "${slug}": the reduced state serializes to ${serializedState.length} chars, over the ${REDUCE_CHECKPOINT_STATE_MAX_CHARS}-char ceiling of one storage cell (2 MB) — a reduce must keep a summary, not the events; nothing was written`, {
469
+ slug,
470
+ chars: serializedState.length,
471
+ maxChars: REDUCE_CHECKPOINT_STATE_MAX_CHARS
472
+ }), { retryable: false });
473
+ this.#sql.exec(`INSERT INTO reduce_checkpoints (slug, reducer_version, reduced_through_offset, state)
474
+ VALUES (?, ?, ?, ?)
475
+ ON CONFLICT(slug) DO UPDATE SET
476
+ reducer_version = excluded.reducer_version,
477
+ reduced_through_offset = excluded.reduced_through_offset,
478
+ state = COALESCE(excluded.state, reduce_checkpoints.state)`, slug, cursor.reducerVersion, cursor.reducedThroughOffset, serializedState);
479
+ }
480
+ };
481
+ /** A delta whose patch is over this many chars is not sent: a whole-array replace of a large
482
+ * projection would cost every watcher the projection per set, and past the event ceiling the door
483
+ * would refuse it outright. The delta rides with `patch: null` instead — the rev moved, re-seed. */
484
+ const LIVE_STATE_PATCH_MAX_CHARS = 1024 * 1024;
485
+ var LiveState = class {
486
+ #liveStateSink;
487
+ #liveStateKey;
488
+ #state;
489
+ /** The DIFF BASE: the last value that serialized — what a client that applied every delta holds.
490
+ * Kept apart from `#state` so a value the wire cannot carry, adopted without an emit, never
491
+ * becomes the base every later diff would throw against. */
492
+ #lastSerializedState;
493
+ #liveStateRev;
494
+ /** THE DELTA APPEND CHAIN — at most one delta append in flight, so commit order = mint order for a
495
+ * CROSS-HOP sink: `env.ITX.get().append(e)` mints a FRESH capability per call, so two deltas
496
+ * issued in different turns race across the hop and the second can commit first — ~14% of rapid
497
+ * pairs on the deployed edge (never locally, the hop is sub-ms). Nothing is dropped by a reorder,
498
+ * but it costs every watcher the full door re-read the deltas exist to avoid. A lone delta (the
499
+ * chain idle) is issued synchronously; only when an append is already in flight does the next
500
+ * queue behind it. Nobody waits on this. */
501
+ #liveStateDeltaAppendChain = Promise.resolve();
502
+ constructor(sink, key, initial) {
503
+ this.#liveStateSink = sink;
504
+ this.#liveStateKey = key;
505
+ this.#state = initial;
506
+ this.#lastSerializedState = initial;
507
+ this.#liveStateRev = Date.now() * 4096 + Math.floor(Math.random() * 4096);
508
+ }
509
+ /** The current value (reflects every `set`). */
510
+ get() {
511
+ return this.#state;
512
+ }
513
+ /** THE seed door: `{rev, state}` read together (single-threaded ⇒ atomically), which is what lets
514
+ * a client chain patches exactly instead of guessing which changes its snapshot already contains. */
515
+ snapshot() {
516
+ return {
517
+ rev: this.#liveStateRev,
518
+ state: this.#state
519
+ };
520
+ }
521
+ /** Replace the value: diff the last serialized base → next; on a real change bump the revision
522
+ * and append the delta. Build a NEW value (don't mutate `next` in place) — the diff is over JSON.
523
+ * A diff/append failure degrades to a LOST notification (the client re-seeds on the chain gap),
524
+ * never a throw the caller sees. */
525
+ set(next) {
526
+ if (next === this.#lastSerializedState) return;
527
+ let patch;
528
+ try {
529
+ patch = diff(this.#lastSerializedState, next);
530
+ } catch {
531
+ this.#state = next;
532
+ this.#liveStateRev += 1;
533
+ return;
534
+ }
535
+ this.#state = next;
536
+ this.#lastSerializedState = next;
537
+ if (!patch) return;
538
+ const from = this.#liveStateRev;
539
+ const to = from + 1;
540
+ this.#liveStateRev = to;
541
+ const wirePatch = JSON.stringify(patch).length > LIVE_STATE_PATCH_MAX_CHARS ? null : patch;
542
+ const emitDelta = () => this.#liveStateSink.append({
543
+ type: "events.iterate.com/live-state/changed",
544
+ ephemeral: true,
545
+ payload: {
546
+ key: this.#liveStateKey,
547
+ from,
548
+ to,
549
+ patch: wirePatch
550
+ }
551
+ });
552
+ this.#liveStateDeltaAppendChain = this.#liveStateDeltaAppendChain.then(emitDelta).catch(() => {});
553
+ }
554
+ };
555
+ function defineProcessorContract(contract) {
556
+ if (!contract.stateSchema.safeParse({}).success) throw new Error(`contract "${contract.slug}": stateSchema must parse {} (default every field)`);
557
+ const events = contract.events ?? {};
558
+ const processorDeps = contract.processorDeps ?? [];
559
+ const depEventTypes = /* @__PURE__ */ new Set();
560
+ for (const dep of processorDeps) for (const type of Object.keys(dep.events)) {
561
+ if (type in events) throw new Error(`contract "${contract.slug}": event "${type}" is already owned by a dep`);
562
+ if (depEventTypes.has(type)) throw new Error(`contract "${contract.slug}": event "${type}" is declared by two deps`);
563
+ depEventTypes.add(type);
564
+ }
565
+ const resolve = (type) => events[type] ?? processorDeps.map((dep) => dep.events[type]).find(Boolean);
566
+ return {
567
+ slug: contract.slug,
568
+ version: contract.version,
569
+ description: contract.description,
570
+ consumes: contract.consumes,
571
+ emits: contract.emits,
572
+ stateSchema: contract.stateSchema,
573
+ events,
574
+ processorDeps,
575
+ initialState: () => contract.stateSchema.parse({}),
576
+ payloadSchemaFor: (type) => resolve(type)?.payloadSchema
577
+ };
578
+ }
579
+ //#endregion
580
+ export { LiveState, ProcessorEngine, REVIVE_AFTER_MAX_MS, REVIVE_AFTER_MS, ReduceCheckpointTable, StreamProcessor, consumesEvent, defineProcessorContract, idempotencyConflictMessage, jsonEqual, sameIdempotentEvent };
581
+
582
+ //# sourceMappingURL=processor.mjs.map