iterate 0.3.0 → 0.4.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.
- package/README.md +164 -82
- package/dist/{next/api.d.ts → api.d.ts} +197 -33
- package/dist/{next/app-server.d.ts → app-server.d.ts} +7 -0
- package/dist/{next/app-server.mjs → app-server.mjs} +17 -15
- package/dist/app-server.mjs.map +1 -0
- package/dist/{next/app-session.mjs → app-session.mjs} +12 -15
- package/dist/app-session.mjs.map +1 -0
- package/dist/{next/app.mjs → app.mjs} +53 -14
- package/dist/app.mjs.map +1 -0
- package/dist/{next/client → client}/live-state.d.ts +11 -11
- package/dist/client/oauth.d.ts +17 -0
- package/dist/{next/client → client}/react.d.ts +10 -42
- package/dist/{next/client → client}/socket.d.ts +1 -0
- package/dist/client.mjs +156 -4
- package/dist/client.mjs.map +1 -0
- package/dist/{next/expression.d.ts → expression.d.ts} +11 -69
- package/dist/{next/expression.mjs → expression.mjs} +11 -109
- package/dist/expression.mjs.map +1 -0
- package/dist/lib-BWr-5mFO.mjs +36 -0
- package/dist/lib-BWr-5mFO.mjs.map +1 -0
- package/dist/{next/lib.d.ts → lib.d.ts} +16 -2
- package/dist/{next/lib.mjs → lib.mjs} +32 -3
- package/dist/lib.mjs.map +1 -0
- package/dist/node.d.ts +15 -3
- package/dist/node.mjs +36 -174
- package/dist/node.mjs.map +1 -1
- package/dist/{next/oauth-scopes.mjs → oauth-scopes.mjs} +1 -1
- package/dist/oauth-scopes.mjs.map +1 -0
- package/dist/{next/oauth.mjs → oauth.mjs} +14 -2
- package/dist/oauth.mjs.map +1 -0
- package/dist/principal.d.ts +8 -0
- package/dist/principal.mjs +8 -0
- package/dist/principal.mjs.map +1 -0
- package/dist/project-ingress.d.ts +58 -0
- package/dist/project-ingress.mjs +104 -0
- package/dist/project-ingress.mjs.map +1 -0
- package/dist/{next/react.mjs → react.mjs} +12 -12
- package/dist/react.mjs.map +1 -0
- package/dist/sdk/auth.d.ts +25 -0
- package/dist/sdk/index.d.ts +155 -0
- package/dist/sdk/record-pipelined-steps.d.ts +19 -0
- package/dist/sdk.mjs +245 -2
- package/dist/sdk.mjs.map +1 -0
- package/dist/{next/stream → stream}/processor.d.ts +28 -23
- package/dist/{next/stream → stream}/processor.mjs +48 -25
- package/dist/stream/processor.mjs.map +1 -0
- package/dist/{next/stream → stream}/run.d.ts +9 -6
- package/dist/{next/stream → stream}/run.mjs +13 -8
- package/dist/stream/run.mjs.map +1 -0
- package/dist/stream/test-support.d.ts +45 -0
- package/dist/stream/test-support.mjs +196 -0
- package/dist/stream/test-support.mjs.map +1 -0
- package/package.json +65 -219
- package/THIRD_PARTY_NOTICES.md +0 -55
- package/bin/iterate.js +0 -94
- package/dist/api-url-B6404M82.mjs +0 -17
- package/dist/api-url-B6404M82.mjs.map +0 -1
- package/dist/app-ref-BipL0feU.mjs +0 -35
- package/dist/app-ref-BipL0feU.mjs.map +0 -1
- package/dist/app-ref-C1CrgXqX.mjs +0 -7
- package/dist/app-ref-C1CrgXqX.mjs.map +0 -1
- package/dist/app-ref-DYai_om1.mjs +0 -7
- package/dist/app-ref-DYai_om1.mjs.map +0 -1
- package/dist/cli-D0c-pDL_.mjs +0 -1010
- package/dist/cli-D0c-pDL_.mjs.map +0 -1
- package/dist/client.d.ts +0 -3
- package/dist/cloudflare-BTm90gQ4.mjs +0 -951
- package/dist/cloudflare-BTm90gQ4.mjs.map +0 -1
- package/dist/contract-s4FW4eES.mjs +0 -309
- package/dist/contract-s4FW4eES.mjs.map +0 -1
- package/dist/document-review/index.d.ts +0 -5
- package/dist/document-review/types.d.ts +0 -107
- package/dist/document-review.mjs +0 -7015
- package/dist/document-review.mjs.map +0 -1
- package/dist/durable-object-processor-durability-CNsTjAJS.mjs +0 -205
- package/dist/durable-object-processor-durability-CNsTjAJS.mjs.map +0 -1
- package/dist/idempotency-DleloJNt.mjs +0 -28
- package/dist/idempotency-DleloJNt.mjs.map +0 -1
- package/dist/index.d.mts +0 -5
- package/dist/index.mjs +0 -8
- package/dist/index.mjs.map +0 -1
- package/dist/itx/api-url.d.ts +0 -6
- package/dist/itx/itx-node-client.d.ts +0 -65
- package/dist/itx/itx-session.d.ts +0 -215
- package/dist/itx/owned-rpc-session.d.ts +0 -14
- package/dist/itx/query-client.d.ts +0 -10
- package/dist/itx-api.generated.d.ts +0 -6195
- package/dist/itx-session-sjud8GiT.mjs +0 -534
- package/dist/itx-session-sjud8GiT.mjs.map +0 -1
- package/dist/live-state-BJNqOwFw.mjs +0 -299
- package/dist/live-state-BJNqOwFw.mjs.map +0 -1
- package/dist/next/app-server.mjs.map +0 -1
- package/dist/next/app-session.mjs.map +0 -1
- package/dist/next/app.mjs.map +0 -1
- package/dist/next/client/oauth.d.ts +0 -12
- package/dist/next/client.mjs +0 -156
- package/dist/next/client.mjs.map +0 -1
- package/dist/next/expression.mjs.map +0 -1
- package/dist/next/lib.mjs.map +0 -1
- package/dist/next/oauth-scopes.mjs.map +0 -1
- package/dist/next/oauth.mjs.map +0 -1
- package/dist/next/principal.d.ts +0 -64
- package/dist/next/principal.mjs +0 -98
- package/dist/next/principal.mjs.map +0 -1
- package/dist/next/project-ingress.d.ts +0 -37
- package/dist/next/project-ingress.mjs +0 -75
- package/dist/next/project-ingress.mjs.map +0 -1
- package/dist/next/react.mjs.map +0 -1
- package/dist/next/sdk/auth.d.ts +0 -5
- package/dist/next/sdk/index.d.ts +0 -112
- package/dist/next/sdk.mjs +0 -139
- package/dist/next/sdk.mjs.map +0 -1
- package/dist/next/stream/processor.mjs.map +0 -1
- package/dist/next/stream/run.mjs.map +0 -1
- package/dist/next-node.d.ts +0 -15
- package/dist/next-node.mjs +0 -51
- package/dist/next-node.mjs.map +0 -1
- package/dist/processor-host-capabilities-BMFH3KTM.mjs +0 -56
- package/dist/processor-host-capabilities-BMFH3KTM.mjs.map +0 -1
- package/dist/processors/cloudflare.d.ts +0 -3
- package/dist/processors/durable-object-processor-durability.d.ts +0 -79
- package/dist/processors/event-consumption-metrics.d.ts +0 -82
- package/dist/processors/idempotency.d.ts +0 -13
- package/dist/processors/index.d.ts +0 -12
- package/dist/processors/processor-contracts.d.ts +0 -342
- package/dist/processors/processor-facet.d.ts +0 -186
- package/dist/processors/processor-host-capabilities.d.ts +0 -60
- package/dist/processors/prompt-sections.d.ts +0 -17
- package/dist/processors/rpc-types.d.ts +0 -515
- package/dist/processors/schemas.d.ts +0 -102
- package/dist/processors/stream-handle.d.ts +0 -45
- package/dist/processors/stream-processor-keepalive.d.ts +0 -95
- package/dist/processors/stream-processor-registry.d.ts +0 -233
- package/dist/processors/stream-processor-runner.d.ts +0 -289
- package/dist/processors/stream-processor.d.ts +0 -339
- package/dist/processors/stream-runtime-metrics.d.ts +0 -107
- package/dist/processors/testing.d.ts +0 -302
- package/dist/processors-BoNyeBfQ.mjs +0 -10
- package/dist/processors-BoNyeBfQ.mjs.map +0 -1
- package/dist/processors-cloudflare.mjs +0 -3
- package/dist/processors-testing.mjs +0 -435
- package/dist/processors-testing.mjs.map +0 -1
- package/dist/processors.mjs +0 -52
- package/dist/processors.mjs.map +0 -1
- package/dist/protocol-DnK_f2m6.mjs +0 -251
- package/dist/protocol-DnK_f2m6.mjs.map +0 -1
- package/dist/sdk/capnweb/index.d.ts +0 -2
- package/dist/sdk/capnweb/live-state/compact.d.ts +0 -5
- package/dist/sdk/capnweb/live-state/diff.d.ts +0 -41
- package/dist/sdk/capnweb/live-state/engine.d.ts +0 -44
- package/dist/sdk/capnweb/live-state/index.d.ts +0 -41
- package/dist/sdk/capnweb/live-state/protocol.d.ts +0 -87
- package/dist/sdk/capnweb/live-state/retain.d.ts +0 -23
- package/dist/sdk/capnweb/live-state/store.d.ts +0 -20
- package/dist/sdk/capnweb/live-state/types.d.ts +0 -11
- package/dist/sdk/capnweb/react.d.ts +0 -45
- package/dist/sdk/capnweb/react.mjs +0 -316
- package/dist/sdk/capnweb/react.mjs.map +0 -1
- package/dist/sdk/capnweb.mjs +0 -4
- package/dist/sdk/itx/react.d.ts +0 -191
- package/dist/sdk/itx/react.mjs +0 -383
- package/dist/sdk/itx/react.mjs.map +0 -1
- package/dist/sdk-DMB-IM11.mjs +0 -933
- package/dist/sdk-DMB-IM11.mjs.map +0 -1
- package/dist/sdk.d.ts +0 -339
- package/dist/serve-itx.d.ts +0 -46
- package/dist/starter-apps/flake-dashboard/app-ref.d.ts +0 -31
- package/dist/starter-apps/flake-dashboard/configured-worker.mjs +0 -1055
- package/dist/starter-apps/flake-dashboard/configured-worker.mjs.map +0 -1
- package/dist/starter-apps/flake-dashboard/contract.d.ts +0 -4839
- package/dist/starter-apps/flake-dashboard/contract.mjs +0 -2
- package/dist/starter-apps/flake-dashboard/index.d.ts +0 -17
- package/dist/starter-apps/flake-dashboard/index.mjs +0 -56
- package/dist/starter-apps/flake-dashboard/index.mjs.map +0 -1
- package/dist/starter-apps/flake-dashboard/worker.d.ts +0 -4607
- package/dist/starter-apps/github-ai-linter/ai-linter.d.ts +0 -8914
- package/dist/starter-apps/github-ai-linter/configured-worker.mjs +0 -17987
- package/dist/starter-apps/github-ai-linter/configured-worker.mjs.map +0 -1
- package/dist/starter-apps/github-ai-linter/contract.d.ts +0 -9193
- package/dist/starter-apps/github-ai-linter/index.d.ts +0 -10
- package/dist/starter-apps/github-ai-linter/index.mjs +0 -36
- package/dist/starter-apps/github-ai-linter/index.mjs.map +0 -1
- package/dist/starter-apps/github-ai-linter/prompt.d.ts +0 -13
- package/dist/starter-apps/github-ai-linter/review-bot.d.ts +0 -808
- package/dist/starter-apps/github-ai-linter/rules.d.ts +0 -34
- package/dist/starter-apps/github-ai-linter/worker-ref.d.ts +0 -19
- package/dist/starter-apps/github-ai-linter/worker.d.ts +0 -19
- package/dist/starter-apps/github-ai-linter/worker.mjs +0 -947
- package/dist/starter-apps/github-ai-linter/worker.mjs.map +0 -1
- package/dist/starter-apps/guestbook/app-ref.d.ts +0 -27
- package/dist/starter-apps/guestbook/client.d.ts +0 -7
- package/dist/starter-apps/guestbook/client.mjs +0 -59
- package/dist/starter-apps/guestbook/configured-worker.mjs +0 -205
- package/dist/starter-apps/guestbook/configured-worker.mjs.map +0 -1
- package/dist/starter-apps/guestbook/index.d.ts +0 -9
- package/dist/starter-apps/guestbook/index.mjs +0 -31
- package/dist/starter-apps/guestbook/index.mjs.map +0 -1
- package/dist/starter-apps/guestbook/processor.d.ts +0 -2267
- package/dist/starter-apps/guestbook/worker.d.ts +0 -26
- package/dist/starter-apps/guestbook/worker.mjs +0 -191
- package/dist/starter-apps/guestbook/worker.mjs.map +0 -1
- package/dist/starter-apps/media/configured-worker.mjs +0 -577
- package/dist/starter-apps/media/configured-worker.mjs.map +0 -1
- package/dist/starter-apps/media/index.mjs +0 -36
- package/dist/starter-apps/media/index.mjs.map +0 -1
- package/dist/starter-apps/media/ref.mjs +0 -20
- package/dist/starter-apps/media/ref.mjs.map +0 -1
- package/dist/starter-apps/media/worker.mjs +0 -579
- package/dist/starter-apps/media/worker.mjs.map +0 -1
- package/dist/starter-apps/notes/configured-worker.mjs +0 -6134
- package/dist/starter-apps/notes/configured-worker.mjs.map +0 -1
- package/dist/starter-apps/notes/index.mjs +0 -23
- package/dist/starter-apps/notes/index.mjs.map +0 -1
- package/dist/starter-apps/notes/ref.mjs +0 -21
- package/dist/starter-apps/notes/ref.mjs.map +0 -1
- package/dist/starter-apps/notes/worker.mjs +0 -427
- package/dist/starter-apps/notes/worker.mjs.map +0 -1
- package/dist/starter-apps/todo/client.mjs +0 -59
- package/dist/starter-apps/todo/configured-worker.mjs +0 -2864
- package/dist/starter-apps/todo/configured-worker.mjs.map +0 -1
- package/dist/starter-apps/todo/index.d.ts +0 -8
- package/dist/starter-apps/todo/index.mjs +0 -29
- package/dist/starter-apps/todo/index.mjs.map +0 -1
- package/dist/stream-processor-keepalive-DAQTP6m3.mjs +0 -2082
- package/dist/stream-processor-keepalive-DAQTP6m3.mjs.map +0 -1
- package/dist/usingCtx-mZx5nsAW.mjs +0 -11800
- package/dist/usingCtx-mZx5nsAW.mjs.map +0 -1
- package/dist/worker-ref-DZxPDmb_.mjs +0 -390
- package/dist/worker-ref-DZxPDmb_.mjs.map +0 -1
- package/dist/worker.d.mts +0 -33
- package/dist/worker.mjs +0 -18
- package/dist/worker.mjs.map +0 -1
- package/menubar/Iterate.entitlements +0 -12
- package/menubar/Iterate.swift +0 -914
- package/menubar/IterateIcon.swift +0 -145
- package/menubar/README.md +0 -28
- package/menubar/build-menubar-app.sh +0 -59
- /package/dist/{next/api.mjs → api.mjs} +0 -0
- /package/dist/{next/app-session.d.ts → app-session.d.ts} +0 -0
- /package/dist/{next/app.d.ts → app.d.ts} +0 -0
- /package/dist/{next/oauth-scopes.d.ts → oauth-scopes.d.ts} +0 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { n as jsonEqual, t as codedError } from "../lib-BWr-5mFO.mjs";
|
|
2
|
+
import { DatabaseSync } from "node:sqlite";
|
|
3
|
+
import "zod";
|
|
4
|
+
//#region src/stream/processor.ts
|
|
5
|
+
function idempotencyConflictMessage(idempotencyKey, existingOffset) {
|
|
6
|
+
return `idempotency key "${idempotencyKey}" already names a different event at offset ${existingOffset}`;
|
|
7
|
+
}
|
|
8
|
+
/** Structural equality of the parts an idempotent retry must not change. */
|
|
9
|
+
function sameIdempotentEvent(existingEvent, requestedEvent) {
|
|
10
|
+
return existingEvent.type === requestedEvent.type && jsonEqual(existingEvent.payload, requestedEvent.payload) && jsonEqual(existingEvent.metadata, requestedEvent.metadata);
|
|
11
|
+
}
|
|
12
|
+
/** Under the 2 MB cell, with room for the row's other columns. */
|
|
13
|
+
const REDUCE_CHECKPOINT_STATE_MAX_CHARS = 2 * 1024 * 1024 - 4096;
|
|
14
|
+
/** What BOTH hosts read and write their checkpoints through — the stream's storage and a facet's
|
|
15
|
+
* own (the Node unit tests drive it over node:sqlite, stream/test-support.ts). */
|
|
16
|
+
var ReduceCheckpointTable = class ReduceCheckpointTable {
|
|
17
|
+
#sql;
|
|
18
|
+
/** `createTable: false` when the caller knows the table exists (the stream's storage skips every
|
|
19
|
+
* CREATE on a re-wake); a facet host constructs one per incarnation and lets it create. */
|
|
20
|
+
constructor(sql, options = { createTable: true }) {
|
|
21
|
+
this.#sql = sql;
|
|
22
|
+
if (options.createTable) ReduceCheckpointTable.createTable(sql);
|
|
23
|
+
}
|
|
24
|
+
static createTable(sql) {
|
|
25
|
+
sql.exec(`CREATE TABLE IF NOT EXISTS reduce_checkpoints (
|
|
26
|
+
slug TEXT PRIMARY KEY,
|
|
27
|
+
reducer_version TEXT NOT NULL,
|
|
28
|
+
reduced_through_offset INTEGER NOT NULL,
|
|
29
|
+
state TEXT
|
|
30
|
+
)`);
|
|
31
|
+
}
|
|
32
|
+
read(slug) {
|
|
33
|
+
const row = this.#sql.exec("SELECT reducer_version, reduced_through_offset, state FROM reduce_checkpoints WHERE slug = ?", slug).toArray()[0];
|
|
34
|
+
if (!row) return void 0;
|
|
35
|
+
return {
|
|
36
|
+
reducerVersion: String(row.reducer_version),
|
|
37
|
+
reducedThroughOffset: Number(row.reduced_through_offset),
|
|
38
|
+
state: row.state ? JSON.parse(String(row.state)) : void 0
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** ALWAYS the cursor; the state ONLY when `stateChanged` — one write either way. */
|
|
42
|
+
write(slug, cursor, state, stateChanged) {
|
|
43
|
+
const serializedState = stateChanged ? JSON.stringify(state) ?? null : null;
|
|
44
|
+
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`, {
|
|
45
|
+
slug,
|
|
46
|
+
chars: serializedState.length,
|
|
47
|
+
maxChars: REDUCE_CHECKPOINT_STATE_MAX_CHARS
|
|
48
|
+
}), { retryable: false });
|
|
49
|
+
this.#sql.exec(`INSERT INTO reduce_checkpoints (slug, reducer_version, reduced_through_offset, state)
|
|
50
|
+
VALUES (?, ?, ?, ?)
|
|
51
|
+
ON CONFLICT(slug) DO UPDATE SET
|
|
52
|
+
reducer_version = excluded.reducer_version,
|
|
53
|
+
reduced_through_offset = excluded.reduced_through_offset,
|
|
54
|
+
state = COALESCE(excluded.state, reduce_checkpoints.state)`, slug, cursor.reducerVersion, cursor.reducedThroughOffset, serializedState);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
//#endregion
|
|
58
|
+
//#region src/stream/test-support.ts
|
|
59
|
+
/** THE PROCESSOR HARNESS: fold `inputs` through a processor's pure `reduce`, exactly as the engine
|
|
60
|
+
* does — start from the contract's initial state, validate each payload against the contract (a
|
|
61
|
+
* malformed KNOWN payload is SKIPPED, never reduced), reduce, thread the state — for a declarative
|
|
62
|
+
* `{ events → state }` processor spec with no engine, storage, or effects. Construct the
|
|
63
|
+
* processor with `new` and hand it the events; the offsets are the input order. Ephemeral inputs are
|
|
64
|
+
* reduced like any other — the reduce decides what it folds (presence's `poke` returns undefined). */
|
|
65
|
+
function reduceProcessor(processor, inputs) {
|
|
66
|
+
let state = processor.contract.initialState();
|
|
67
|
+
inputs.forEach((input, index) => {
|
|
68
|
+
const parsed = processor.contract.payloadSchemaFor?.(input.type)?.safeParse(input.payload ?? {});
|
|
69
|
+
if (parsed && !parsed.success) return;
|
|
70
|
+
const event = {
|
|
71
|
+
type: input.type,
|
|
72
|
+
payload: parsed?.success ? parsed.data : input.payload,
|
|
73
|
+
source: input.source,
|
|
74
|
+
offset: index + 1,
|
|
75
|
+
createdAt: (/* @__PURE__ */ new Date((index + 1) * 1e3)).toISOString(),
|
|
76
|
+
path: "/"
|
|
77
|
+
};
|
|
78
|
+
state = processor.reduce({
|
|
79
|
+
event,
|
|
80
|
+
state
|
|
81
|
+
}) ?? state;
|
|
82
|
+
});
|
|
83
|
+
return state;
|
|
84
|
+
}
|
|
85
|
+
function memoryStream(path = "/") {
|
|
86
|
+
const durableEvents = [];
|
|
87
|
+
const pushedEvents = [];
|
|
88
|
+
const eventsByIdempotencyKey = /* @__PURE__ */ new Map();
|
|
89
|
+
const engines = [];
|
|
90
|
+
const claims = [];
|
|
91
|
+
let maxAssigned = 0;
|
|
92
|
+
let reads = 0;
|
|
93
|
+
return {
|
|
94
|
+
stream: {
|
|
95
|
+
claim: (at) => {
|
|
96
|
+
claims.push(at);
|
|
97
|
+
return Promise.resolve();
|
|
98
|
+
},
|
|
99
|
+
append: (...events) => {
|
|
100
|
+
const scannedAfterOffset = maxAssigned;
|
|
101
|
+
const committedEvents = events.map((event) => {
|
|
102
|
+
if (event.idempotencyKey) {
|
|
103
|
+
const existingEvent = eventsByIdempotencyKey.get(event.idempotencyKey);
|
|
104
|
+
if (existingEvent) {
|
|
105
|
+
if (sameIdempotentEvent(existingEvent, event)) return existingEvent;
|
|
106
|
+
throw new Error(idempotencyConflictMessage(event.idempotencyKey, existingEvent.offset));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
maxAssigned += 1;
|
|
110
|
+
const committedEvent = {
|
|
111
|
+
...event,
|
|
112
|
+
offset: maxAssigned,
|
|
113
|
+
createdAt: (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
114
|
+
path
|
|
115
|
+
};
|
|
116
|
+
if (!event.ephemeral) {
|
|
117
|
+
durableEvents.push(committedEvent);
|
|
118
|
+
if (event.idempotencyKey) eventsByIdempotencyKey.set(event.idempotencyKey, committedEvent);
|
|
119
|
+
}
|
|
120
|
+
return committedEvent;
|
|
121
|
+
});
|
|
122
|
+
pushedEvents.push(...committedEvents);
|
|
123
|
+
if (maxAssigned > scannedAfterOffset) {
|
|
124
|
+
const scannedOffsetRange = {
|
|
125
|
+
after: scannedAfterOffset,
|
|
126
|
+
through: maxAssigned
|
|
127
|
+
};
|
|
128
|
+
for (const engine of engines) engine.processEventBatch(committedEvents, scannedOffsetRange).catch(() => {});
|
|
129
|
+
}
|
|
130
|
+
return committedEvents;
|
|
131
|
+
},
|
|
132
|
+
read: (afterOffset = 0, limit = 500) => {
|
|
133
|
+
reads += 1;
|
|
134
|
+
const page = durableEvents.filter((event) => event.offset > afterOffset).slice(0, limit);
|
|
135
|
+
return Promise.resolve({
|
|
136
|
+
events: page,
|
|
137
|
+
scannedThroughOffset: page.length === limit ? page[page.length - 1].offset : Math.max(afterOffset, maxAssigned),
|
|
138
|
+
atHead: page.length < limit || page[page.length - 1].offset === durableEvents[durableEvents.length - 1].offset
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
events: durableEvents,
|
|
143
|
+
pushedEvents,
|
|
144
|
+
engines,
|
|
145
|
+
claims,
|
|
146
|
+
get reads() {
|
|
147
|
+
return reads;
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
/** A facet's checkpoint table (processor.ts `ReduceCheckpointTable`) over an in-memory
|
|
152
|
+
* node:sqlite database — the real table, so the unit tests checkpoint exactly as a facet does —
|
|
153
|
+
* with `writes` counting every write: rule 4 ("one durable commit per batch") and the ephemeral
|
|
154
|
+
* zero-write rule are pinned by counting it. */
|
|
155
|
+
var WriteCountingReduceCheckpointTable = class extends ReduceCheckpointTable {
|
|
156
|
+
writes = 0;
|
|
157
|
+
write(slug, cursor, state, stateChanged) {
|
|
158
|
+
this.writes++;
|
|
159
|
+
super.write(slug, cursor, state, stateChanged);
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
function memoryStorage() {
|
|
163
|
+
return new WriteCountingReduceCheckpointTable(nodeSqliteDurableObjectStorage().sql);
|
|
164
|
+
}
|
|
165
|
+
/** Wait `ms` on a timer — long enough, by default, for fire-and-forget pushes to land. */
|
|
166
|
+
const settle = (ms = 25) => new Promise((r) => setTimeout(r, ms));
|
|
167
|
+
function nodeSqliteDurableObjectStorage() {
|
|
168
|
+
const db = new DatabaseSync(":memory:");
|
|
169
|
+
return {
|
|
170
|
+
sql: { exec(query, ...bindings) {
|
|
171
|
+
const statement = db.prepare(query);
|
|
172
|
+
const bound = bindings;
|
|
173
|
+
if (statement.columns().length === 0) {
|
|
174
|
+
statement.run(...bound);
|
|
175
|
+
return Object.assign([], { toArray: () => [] });
|
|
176
|
+
}
|
|
177
|
+
const rows = statement.iterate(...bound);
|
|
178
|
+
return Object.assign(rows, { toArray: () => [...rows] });
|
|
179
|
+
} },
|
|
180
|
+
transactionSync: (closure) => {
|
|
181
|
+
db.exec("BEGIN");
|
|
182
|
+
try {
|
|
183
|
+
const result = closure();
|
|
184
|
+
db.exec("COMMIT");
|
|
185
|
+
return result;
|
|
186
|
+
} catch (error) {
|
|
187
|
+
db.exec("ROLLBACK");
|
|
188
|
+
throw error;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
//#endregion
|
|
194
|
+
export { memoryStorage, memoryStream, nodeSqliteDurableObjectStorage, reduceProcessor, settle };
|
|
195
|
+
|
|
196
|
+
//# sourceMappingURL=test-support.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-support.mjs","names":["#sql"],"sources":["../../src/stream/processor.ts","../../src/stream/test-support.ts"],"sourcesContent":["// stream/processor.ts — THE PROCESSOR: `StreamProcessor`, the PURE class an author writes (a\n// contract and three hooks, no constructor arguments, no storage, no stream — a unit test constructs\n// it with `new`), and `ProcessorEngine`, which drives ONE such instance against a stream and a\n// storage; the SDK's `StreamProcessorDurableObject` builds one per hosted facet. The author surface\n// is Node-testable and bundled into every loaded isolate as\n// `processor.js` (sdk/index.ts), so nothing here imports cloudflare:workers. Four concepts ride with it:\n// events — `StreamEventInput` / `StreamEvent`, the envelope, and the idempotency rules\n// reduce checkpoint — `ReduceCheckpointTable`, THE ONE spelling of a persisted reduce checkpoint\n// live state — `LiveState`, one value, its revision chain and the diff→emit delta\n// processor contract — `defineProcessorContract`, the zod contract helper (zod rides the SDK bundle)\n//\n// THE CONCURRENCY CONTRACT:\n// 1. ONE SERIAL CHAIN per processor — batches never interleave.\n// 2. ONE EVENT AT A TIME inside a batch: this event's `blockProcessorWhile` work completes\n// before the next event's `processEvent` starts (a FIFO chain, awaited per event).\n// 3. `runInBackground` work is deliberately NOT awaited — it may overtake later events; it is\n// a droppable attempt whose outcome must be recoverable from state at the next at-head pass —\n// and that pass is OWED: while any attempt is in flight the engine holds a CLAIM on its\n// context's alarm (\"come back by T\"), released when the last attempt settles; a host that dies\n// mid-attempt is revived by the alarm (`revive()`: catch up, run the at-head pass), and an\n// attempt still in flight at a revive claims again, later each time, so a hung attempt costs a\n// few wakes an hour and never a loop.\n// 4. ONE DURABLE COMMIT PER BATCH, after every event's blocking work settled — persist BEFORE\n// advancing past the last DURABLE offset. A failed batch persists nothing, retried whole.\n// 5. The at-head pass: the last consumable event of a batch that reaches the stream head\n// carries `delivery.caughtUp: true`; a batch that reaches the head without one gets a single\n// extra `processEvent({ event: null, delivery: { caughtUp: true } })` call.\n//\n// DELIVERY IS PUSH-FIRST with a SCANNED-RANGE PROOF: the cursor advances on the RANGE a push\n// carries, never by counting events, so ephemeral holes, consumes-filters and reboot gaps are all\n// the same non-event. A non-contiguous push triggers GAP REPAIR: read durable rows from the own\n// cursor up to the push start, then process the push. Ephemeral events ride pushes ONLY (an\n// ephemeral missed while a facet rebuilds is gone by design) and NEVER trigger a checkpoint write,\n// so a pure-ephemeral flood costs this class ZERO storage writes.\n//\n// THE READ VERBS (`snapshot`, `liveSnapshot`) read the log only while the reduce has not provably\n// reached the head SHOWN so far: the highest a push showed or — for a processor FED BY PUSHES (its\n// host's word: a row of its context pushes it every commit it consumes) — a catch-up read from the\n// log. So an idle processor a row pushes reads its log once per incarnation, not on every read; one\n// nothing pushes learns of a new event only by reading, and reads every time.\n//\n// `reduce` is a PURE reduce (new object out, its arguments immutable), CHECKPOINTED\n// (`ReduceCheckpointTable` below) with the offset and contract version it was reduced under; bumping\n// `contract.version` re-reduces from offset 0 through `reduce` only, never re-running side effects —\n// over durable rows only, which is why durable product truth must never derive from an ephemeral.\n\nimport type { SqlStorageValue } from \"@cloudflare/workers-types\";\nimport { z } from \"zod\";\nimport { reportIssue, jsonEqual, codedError, diff } from \"../lib.ts\";\n\n/** What a processor declares: its checkpoint slug and reducer version, what it consumes and emits,\n * and its initial state (`defineProcessorContract` below builds one from zod schemas). */\nexport type ProcessorContract<State = unknown> = {\n slug: string;\n /** Bumping this re-reduces state from offset 0 (reduce only — side effects never re-run). */\n version: string;\n description?: string;\n /** What it reacts to: type strings, or \"*\" for every DURABLE event. Ephemeral events are\n * delivered ONLY when their type is named here — `\"*\"` never sweeps them. */\n consumes: readonly string[];\n /** What its `append` is allowed to emit. */\n emits: readonly string[];\n /** The schema-initial state (\"{} with every field defaulted\" for zod contracts). */\n initialState: () => State;\n /** The zod payload schema for a consumed event type (owned or a dep's), or undefined if the type\n * is unknown or the contract declares no `events` catalog. The engine validates a consumed event's\n * payload against it before reducing (a malformed payload for a KNOWN event is skipped, never\n * folded). Present on `defineProcessorContract` contracts; a hand-built core contract omits it and\n * reduces unvalidated. */\n payloadSchemaFor?: (type: string) => z.ZodType | undefined;\n};\n\n/** The stream a processor reduces. `read` answers durable rows plus the proof: `scannedThroughOffset`\n * is how far the read is CONTIGUOUSLY known (never past the durable mark — stream.ts), and `atHead`\n * says whether the page was cut; its length says nothing (a budget cut is short of `limit`). */\nexport type ProcessorStream = {\n append(...events: StreamEventInput[]): Promise<StreamEvent[]> | StreamEvent[];\n read(\n afterOffset?: number,\n limit?: number,\n ): Promise<{ events: StreamEvent[]; scannedThroughOffset: number; atHead: boolean }>;\n /** This processor's claim on its context's alarm: \"come back by `at`\" (the context's alarm pass\n * then calls `revive()`), or `null` to release it. Durable on the context, never a log event. */\n claim(at: number | null): Promise<unknown>;\n};\n\n/** How long after an attempt starts a dead host is revived: the recovery bound. A revive that finds\n * the attempt still in flight claims again with the delay doubled, up to `REVIVE_AFTER_MAX_MS`. */\nexport const REVIVE_AFTER_MS = 20_000;\nexport const REVIVE_AFTER_MAX_MS = 30 * 60_000;\n\n/** The contiguity proof a delivery carries: the half-open offset window `(after, through]`. A chain\n * of these (each `after` === the previous `through`) is how a subscriber proves it missed nothing. */\nexport type ScannedRange = { after: number; through: number };\n\nexport type ReduceArgs<State, Event = StreamEvent> = { event: Event; state: State };\n\nexport type ProcessEventArgs<\n State,\n Event = StreamEvent,\n /** What `append` takes: `EmittedEventInput<typeof Contract>` for a processor that declares one —\n * each type the contract `emits`, its payload as the catalog spells it. */\n Emitted extends StreamEventInput = StreamEventInput,\n> = {\n /** The consumed event — or `null` for the eventless at-head pass. */\n event: Event | null;\n state: State;\n previousState: State;\n /** Emit (validated against `emits`, provenance-stamped) onto this processor's own stream. Declared\n * as METHODS (not arrow-typed properties) on purpose: a subclass that narrows `Emitted` must stay\n * assignable to `StreamProcessor<State>` (the host's field), and only method parameters are\n * compared bivariantly. */\n append(...events: Emitted[]): Promise<StreamEvent[]>;\n /** Hold the cursor until `work` settles; FIFO with other blockers of the SAME event. */\n blockProcessorWhile: (work: () => Promise<unknown>) => void;\n /** Fire-and-forget attempt; may overtake later events; outcome must be state-recoverable. */\n runInBackground: (work: () => Promise<unknown>) => void;\n delivery: { caughtUp: boolean };\n};\n\n/** THE ONE consumes rule — the processor engine, the subscription delivery loop, and the inline\n * reduces all call this; there is no second copy to drift. `consumes` undefined = every durable event\n * (a subscriber's default). \"*\" = every durable event. A NAMED type opts that type in, INCLUDING\n * ephemerals (\"*\" NEVER sweeps ephemerals) — so a live-state watcher spells\n * `consumes: [\"events.iterate.com/itx/live-state-changed\"]` and filters `payload.key` itself. The wake\n * record (`itx/woken`) is a durable event like any other: a \"*\" row receives every incarnation's. */\nexport function consumesEvent(\n consumes: readonly string[] | undefined,\n event: { type: string; ephemeral?: boolean },\n): boolean {\n if (event.ephemeral) return consumes?.includes(event.type) ?? false;\n return !consumes || consumes.includes(\"*\") || consumes.includes(event.type);\n}\n\n/** What the ENGINE reduces: the contract's consumes, minus the one type no processor may ever reduce or\n * react to — a live-state delta. Deltas are notifications ABOUT state; letting one feed a reduce is\n * the feedback-loop class, made unspellable here rather than discouraged. */\nconst reducesEvent = (consumes: readonly string[], event: { type: string; ephemeral?: boolean }) =>\n event.type !== \"events.iterate.com/itx/live-state-changed\" && consumesEvent(consumes, event);\n\n/** THE AUTHOR CLASS: a contract, three hooks and one helper. Deps an effect needs arrive through\n * the subclass's own constructor, as for any class. One instance lives as long as its host; a field\n * on it is RUNTIME state (gone with the host), which `projectLiveState` may reduce into the live view. */\nexport abstract class StreamProcessor<State, Event extends StreamEvent = StreamEvent> {\n abstract readonly contract: ProcessorContract<State>;\n\n /** Pure reduce. Return the NEXT state (a new object) — or null/undefined to keep the current. The\n * `Event` type param — a discriminated union of the events the contract consumes — narrows\n * `event.payload` per `event.type` inside the body, so no cast is needed; it defaults to the\n * untyped `StreamEvent` for processors that don't declare one. */\n reduce(_args: ReduceArgs<State, Event>): State | null | undefined {\n return undefined;\n }\n\n /** Side-effect hook. Synchronous by design: register async work via the two helpers on args.\n * `append` takes what THIS class's `contract` emits (`EmittedEventInput<this[\"contract\"]>`:\n * a subclass whose `contract` is a defined one gets each emitted type's payload as its catalog\n * spells it; the base, and a hand-built contract, take any input). */\n processEvent(\n _args: ProcessEventArgs<State, Event, EmittedEventInput<this[\"contract\"]>>,\n ): undefined {}\n\n /** The live-state PROJECTION — the shape clients see and the diffs are computed over. DEFAULT: the\n * reduced state verbatim, so every processor is live out of the box; that is deliberate — the\n * delta is an EPHEMERAL event, so \"always live\" costs an offset and a cheap diff, nothing durable.\n * Override to redact, or to REDUCE IN RUNTIME FIELDS (`return { ...state, lastSeenMs: this.lastSeenMs }`);\n * the engine re-projects after EVERY batch, and a field changed outside a batch needs the host's\n * `publishLiveState()`. */\n projectLiveState(state: State): unknown {\n return state;\n }\n\n /** Stable idempotency key namespaced by slug; pass the event being processed for a per-event key. */\n idempotencyKey(key: string, event?: StreamEvent): string {\n return event ? `${this.contract.slug}/${key}@${event.offset}` : `${this.contract.slug}/${key}`;\n }\n}\n\n/** THE ENGINE: everything below the author's three hooks — the serial chain, the checkpoint, gap\n * repair, the at-head pass, version re-reduces, live-state publishing. Constructed by the host\n * (`StreamProcessorDurableObject`; a test with the stand-ins in test-support.ts). */\nexport class ProcessorEngine<State> {\n readonly processor: StreamProcessor<State>;\n readonly #contract: ProcessorContract<State>;\n readonly #stream: ProcessorStream;\n readonly #storage: ReduceCheckpointTable;\n\n /** Rule 1: every batch runs on this chain, one after another. */\n #serialBatchChain: Promise<void> = Promise.resolve();\n /** The reduced state and the durable offset it was reduced through — checkpointed on the batches\n * that carried a durable. */\n #reducedState: State;\n #reducedThroughOffset: number;\n /** A checkpoint found under ANOTHER contract version: the input to the one re-reduce the chain\n * runs before anything else; cleared once it ran. */\n #staleCheckpoint?: { reducedThroughOffset: number; state: State };\n /** The highest `range.through` ever SHOWN to this processor (see processEventBatch). */\n #pushedThroughOffset?: number;\n /** The host's word that a row pushes this processor every commit it consumes (the constructor's\n * `fedByPushes`) — what lets a head read from the log count as shown. */\n readonly #fedByPushes: boolean;\n /** The highest head a catch-up of a processor FED BY PUSHES reduced through (`#showHeadReadFromLog`):\n * every commit past it that the processor consumes reaches it as a push, so the read verbs trust it\n * as a push's head. In memory only — a fresh incarnation catches up once before it trusts any. */\n #headReadFromLogOffset?: number;\n /** A refusal that can only repeat — the checkpoint over its cell (stamped `retryable: false`):\n * LATCHED for this incarnation, so every later batch, catch-up and read verb rejects with it at\n * once instead of re-reducing into the same wall on every push and wake. A fresh incarnation\n * tries once more. */\n #latchedRefusal?: Error;\n /** waitUntilProcessed's waiting callers, resolved as the cursor advances. */\n readonly #waitUntilProcessedWaiters: { offset: number; resolve: () => void }[] = [];\n /** Born with the engine, so its epoch is minted once per incarnation. */\n readonly #liveState: LiveState<unknown>;\n /** Rule 3's claim: attempts in flight, and how many revives found one still in flight (the\n * backoff of the next claim; reset when the last attempt settles). The claim calls ride ONE\n * chain, so a release never overtakes the claim of the attempt that followed it. */\n #backgroundWorkInFlight = 0;\n #revivesWhileBusy = 0;\n #claimChain: Promise<unknown> = Promise.resolve();\n /** Whether the last batch this engine ran carried the at-head pass (rule 5) — what `revive()`\n * reads to know if its catch-up already ran one. */\n #lastBatchAtHead = false;\n\n constructor(\n processor: StreamProcessor<State>,\n deps: {\n stream: ProcessorStream;\n storage: ReduceCheckpointTable;\n /** The host's word that a subscription row pushes this processor every commit it consumes\n * (`processEventBatch`). The read verbs then trust the head a catch-up read until a push shows\n * a later one; absent, only a push's head is trusted, so an unpushed processor reads each time. */\n fedByPushes?: boolean;\n },\n ) {\n this.processor = processor;\n this.#contract = processor.contract;\n this.#stream = deps.stream;\n this.#storage = deps.storage;\n this.#fedByPushes = deps.fedByPushes === true;\n // ONE row, so cursor and state never disagree; one written under another contract version is\n // kept as #staleCheckpoint for the chain's first work.\n const { slug, version } = this.#contract;\n const checkpoint = this.#storage.read<State>(slug);\n if (checkpoint?.reducerVersion === version) {\n this.#reducedState = checkpoint.state ?? this.#contract.initialState();\n this.#reducedThroughOffset = checkpoint.reducedThroughOffset;\n } else {\n this.#reducedState = this.#contract.initialState();\n this.#reducedThroughOffset = 0;\n if (checkpoint)\n this.#staleCheckpoint = {\n reducedThroughOffset: checkpoint.reducedThroughOffset,\n state: checkpoint.state ?? this.#reducedState,\n };\n }\n // Seeded with the projection of the state this incarnation starts from — after a version bump\n // the OLD version's, so the publish that follows the re-reduce emits the one heal delta clients\n // synced to the old state need. The projection is the author's code: a throw here costs the\n // seed, never the engine.\n let seed: unknown;\n try {\n seed = processor.projectLiveState(\n this.#staleCheckpoint ? this.#staleCheckpoint.state : this.#reducedState,\n );\n } catch (error) {\n reportIssue(\"processor.live-state\", error, { slug });\n seed = undefined;\n }\n this.#liveState = new LiveState(this.#stream, slug, seed);\n }\n\n /** THE SEED READ for live-state clients (LiveState.snapshot), caught up first. */\n async liveSnapshot(): Promise<{ rev: number; state: unknown }> {\n if (!this.#reducedThroughPushedHead()) await this.catchUpFromLog();\n return this.#liveState.snapshot();\n }\n\n /** Emit a delta for the CURRENT projection (reduced + any runtime fields) if it changed. The engine\n * calls this after every batch; the host calls it after a runtime field moved outside a batch. A\n * throwing projection loses only its notification (the client re-seeds on the chain gap). */\n publishLiveState(): void {\n let projection: unknown;\n try {\n projection = this.processor.projectLiveState(this.#reducedState);\n } catch (error) {\n reportIssue(\"processor.live-state\", error, { slug: this.#contract.slug });\n return;\n }\n this.#liveState.set(projection);\n }\n\n // ── driving the reduce ──\n\n /** THE push method: contiguous → reduce it directly (no read); anything else → gap repair from the\n * own cursor first. Fire-and-forget safe: enqueues on the serial chain. */\n processEventBatch(events: StreamEvent[], range: ScannedRange): Promise<void> {\n // Recorded SYNCHRONOUSLY: the head this processor has been SHOWN. Read verbs skip their catch-up\n // when the reduce has provably reached it — the fast path that deletes one parent read RPC from\n // every capability dispatch once caught up.\n this.#pushedThroughOffset = Math.max(this.#pushedThroughOffset ?? 0, range.through);\n return this.#runOnSerialChain(async () => {\n await this.#rereduceIfVersionChanged();\n // GAP REPAIR heals the durable prefix from the log FIRST — up to the push start and no\n // further, because the push carries fresh ephemerals the log cannot return, so the push\n // itself is processed afterwards, never replaced by a catch-up. The repair never delivers\n // caughtUp: the push decides at-head. The log can run out below `range.after` only when that\n // offset was handed to an ephemeral — then there is nothing durable left to heal.\n while (this.#reducedThroughOffset < range.after) {\n const after = this.#reducedThroughOffset;\n const page = await this.#stream.read(after, 500);\n if (page.scannedThroughOffset <= after) break;\n await this.#reduceAndCommitEventBatch(\n page.events.filter((event) => event.offset <= range.after),\n { after, through: Math.min(page.scannedThroughOffset, range.after) },\n false,\n );\n }\n // ALWAYS process the push — no push is ever discarded (a wholly-behind one reduces nothing and\n // just delivers its ephemerals). At head iff this push reaches the head shown so far.\n await this.#reduceAndCommitEventBatch(\n events,\n range,\n range.through >= this.#pushedThroughOffset!,\n );\n });\n }\n\n /** Catch up from the own checkpoint (a cold boot, the read verbs, the barrier), page by page — a\n * failed batch, a missed push, or a fresh incarnation can never skip a durable event. */\n catchUpFromLog(): Promise<void> {\n return this.#runOnSerialChain(async () => {\n await this.#rereduceIfVersionChanged();\n for (;;) {\n const after = this.#reducedThroughOffset;\n const page = await this.#stream.read(after, 500);\n if (page.scannedThroughOffset <= after) {\n // Nothing beyond the cursor: at head already.\n if (page.atHead) this.#showHeadReadFromLog(after);\n return;\n }\n // The page says whether it reached the head — never judge by its length (rule 5's caught-up\n // pass rides the last page).\n await this.#reduceAndCommitEventBatch(\n page.events,\n { after, through: page.scannedThroughOffset },\n page.atHead,\n );\n if (page.atHead) {\n this.#showHeadReadFromLog(page.scannedThroughOffset);\n return;\n }\n }\n });\n }\n\n /** A catch-up reduced through `reducedThroughOffset`, the head its last page reached: for a\n * processor FED BY PUSHES, the head is SHOWN. Every later commit it consumes reaches it as a push,\n * whose `range.through` is recorded the moment the push arrives, so a read that follows the push\n * catches up again; while none arrives, nothing it consumes has landed and its reads stop re-reading\n * the log. A processor nothing pushes records nothing: it learns of a new event only by reading. */\n #showHeadReadFromLog(reducedThroughOffset: number): void {\n if (!this.#fedByPushes) return;\n this.#headReadFromLogOffset = Math.max(this.#headReadFromLogOffset ?? 0, reducedThroughOffset);\n }\n\n // ── the read surface ──\n\n /** Reduce-and-effects caught up through the log, then `{ offset, state }`. */\n async snapshot(): Promise<{ offset: number; state: State }> {\n if (!this.#reducedThroughPushedHead()) await this.catchUpFromLog();\n return { offset: this.#reducedThroughOffset, state: this.#reducedState };\n }\n\n /** Provably reduced through the head SHOWN so far — the highest a push showed or, fed by pushes, a\n * catch-up read — → the read verbs skip their catch-up read. Nothing shown yet (a fresh\n * incarnation, or an unpushed processor) → they read. Whether a PUSH reaches the head (rule 5)\n * is still judged against pushes alone (processEventBatch). */\n #reducedThroughPushedHead(): boolean {\n const shownHeadOffset =\n this.#headReadFromLogOffset === undefined\n ? this.#pushedThroughOffset\n : Math.max(this.#pushedThroughOffset ?? 0, this.#headReadFromLogOffset);\n return shownHeadOffset !== undefined && this.#reducedThroughOffset >= shownHeadOffset;\n }\n\n /** THE barrier verb (read-your-writes): resolves once processed AT LEAST through `offset`. An\n * offset ABOVE the durable mark (an ephemeral's) is reached only if this processor was pushed it —\n * the log cannot prove past the mark, so a wake alone never advances there. */\n waitUntilProcessed(input: { offset: number; timeoutMs?: number }): Promise<void> {\n const { offset, timeoutMs = 10_000 } = input;\n return new Promise<void>((resolve, reject) => {\n if (this.#reducedThroughOffset >= offset) return resolve();\n const waiter = {\n offset,\n resolve: () => {\n clearTimeout(timer);\n resolve();\n },\n };\n const timer = setTimeout(() => {\n // A timed-out waiter LEAVES the list — otherwise every later batch re-scans it forever.\n this.#waitUntilProcessedWaiters.splice(this.#waitUntilProcessedWaiters.indexOf(waiter), 1);\n reject(\n new Error(\n `processor \"${this.#contract.slug}\" did not reach offset ${offset} in ${timeoutMs}ms`,\n ),\n );\n }, timeoutMs);\n this.#waitUntilProcessedWaiters.push(waiter);\n // A rejecting catch-up (read threw) rejects THIS waiter promptly with the real error, not a\n // wait-until-timeout with a generic message.\n void this.catchUpFromLog().catch((error) => {\n const i = this.#waitUntilProcessedWaiters.indexOf(waiter);\n if (i === -1) return; // already resolved/timed-out\n this.#waitUntilProcessedWaiters.splice(i, 1);\n clearTimeout(timer);\n reject(error instanceof Error ? error : new Error(String(error)));\n });\n });\n }\n\n // ── the engine (private) ──\n\n /** Serialize on the chain. THE RULE: never await your own chain from inside a batch — a\n * processor that appends during its batch would deadlock, which is why every append→drive\n * caller is fire-and-forget. */\n #runOnSerialChain(work: () => Promise<void>): Promise<void> {\n const run = this.#serialBatchChain.then(() => {\n if (this.#latchedRefusal) throw this.#latchedRefusal;\n return work();\n });\n this.#serialBatchChain = run.catch(() => {}); // a failed batch never wedges the chain; retry via wake\n return run;\n }\n\n /** The one-time cost of a contract version bump: re-reduce the durable log from offset 0 through\n * the OLD cursor (`reduce` only — those effects already ran) and checkpoint under the new\n * version. Never past the old cursor: re-reducing to the head instead would judge an\n * already-queued in-flight push stale and swallow its effects. */\n async #rereduceIfVersionChanged(): Promise<void> {\n if (!this.#staleCheckpoint) return;\n const target = this.#staleCheckpoint.reducedThroughOffset;\n let state = this.#contract.initialState();\n let reducedThroughOffset = 0;\n while (reducedThroughOffset < target) {\n const page = await this.#stream.read(reducedThroughOffset, 500);\n for (const event of page.events)\n if (event.offset <= target && reducesEvent(this.#contract.consumes, event))\n // The SAME validate+normalize+fold as the live flow — a replay must reproduce it exactly,\n // else a coercion applied live but not here would make a version bump rewrite state.\n state = this.#validateNormalizeAndReduce(event, state).state;\n if (page.scannedThroughOffset <= reducedThroughOffset) break; // nothing left below the target\n reducedThroughOffset = Math.min(page.scannedThroughOffset, target);\n }\n this.#writeCheckpointOrLatch(\n this.#contract.slug,\n { reducerVersion: this.#contract.version, reducedThroughOffset: target },\n state,\n true,\n );\n this.#reducedState = state;\n this.#reducedThroughOffset = target;\n this.#staleCheckpoint = undefined;\n // The one heal delta for clients synced to the OLD version's projection (the constructor's seed).\n this.publishLiveState();\n this.#resolveWaitUntilProcessedWaiters(target);\n }\n\n /** Rules 2–5 over one range (the caller has healed any durable prefix gap first). DURABLES reduce\n * at-most-once (`offset > cursor`); EPHEMERALS ALWAYS deliver — each rides exactly one push and\n * can never be a redelivery, so a durable-only wake that clamped the cursor PAST an ephemeral\n * offset must not suppress it. The cursor is a DURABLE-reduce watermark and never regresses. */\n async #reduceAndCommitEventBatch(\n events: StreamEvent[],\n range: ScannedRange,\n atHead: boolean,\n ): Promise<void> {\n const reducedThroughOffsetBefore = this.#reducedThroughOffset;\n const stateBefore = this.#reducedState;\n let state = stateBefore;\n const consumableEvents = events.filter(\n (event) =>\n reducesEvent(this.#contract.consumes, event) &&\n (event.ephemeral || event.offset > reducedThroughOffsetBefore),\n );\n let caughtUpDelivered = false;\n for (let i = 0; i < consumableEvents.length; i++) {\n const last = i === consumableEvents.length - 1;\n const r = await this.#reduceAndProcessEvent(consumableEvents[i], state, atHead && last);\n state = r.state;\n // A skipped (malformed) last event never delivered caught-up — the eventless pass below does.\n if (atHead && last && r.processed) caughtUpDelivered = true;\n }\n // Rule 5: reached the head with no caught-up event → one eventless at-head pass.\n if (atHead && !caughtUpDelivered)\n state = (await this.#reduceAndProcessEvent(null, state, true)).state;\n\n // Rule 4: ONE persist per range, iff a DURABLE actually ADVANCED the cursor — `advanced`\n // excludes a stale re-push (a no-op write), `sawDurable` the ephemeral-only range (the flood\n // stays free).\n const reducedThroughOffset = Math.max(reducedThroughOffsetBefore, range.through);\n const advanced = reducedThroughOffset > reducedThroughOffsetBefore;\n const sawDurable = events.some((event) => !event.ephemeral);\n if (sawDurable && advanced)\n this.#writeCheckpointOrLatch(\n this.#contract.slug,\n { reducerVersion: this.#contract.version, reducedThroughOffset },\n state,\n state !== stateBefore,\n );\n this.#reducedState = state;\n this.#reducedThroughOffset = reducedThroughOffset;\n this.#lastBatchAtHead = atHead;\n this.#resolveWaitUntilProcessedWaiters(reducedThroughOffset);\n // Persist FIRST, emit the live-state delta second: a crash between loses only a notification,\n // healed by the chain gap, never state. Re-projected after EVERY batch, not only when the reduce\n // moved, so a runtime field bumped inside `processEvent` publishes on its own.\n this.publishLiveState();\n }\n\n // ── rule 3's claim: work in flight ⇒ a claim on the context's alarm ──\n\n /** Start an attempt: the first in flight claims the alarm (not awaited — a claim that has not\n * landed when the host dies revives nothing either way, and the attempt must not wait on it);\n * the last to settle releases the claim. */\n #runInBackground(work: () => Promise<unknown>): void {\n this.#backgroundWorkInFlight += 1;\n if (this.#backgroundWorkInFlight === 1) this.#claim(REVIVE_AFTER_MS);\n void work()\n .catch((error) => reportIssue(\"processor.background\", error, { slug: this.#contract.slug }))\n .finally(() => {\n this.#backgroundWorkInFlight -= 1;\n if (this.#backgroundWorkInFlight === 0) {\n this.#revivesWhileBusy = 0;\n this.#claim(null);\n }\n });\n }\n\n #claim(afterMs: number | null): void {\n const at = afterMs === null ? null : Date.now() + afterMs;\n this.#claimChain = this.#claimChain\n .then(() => this.#stream.claim(at))\n .catch((error) => reportIssue(\"processor.claim\", error, { slug: this.#contract.slug }));\n }\n\n /** THE REVIVE — the context's alarm pass calls this for a due claim (spent by then): catch up\n * from the log and run the at-head pass, so a processor restarts what state says is still owed\n * (rule 3). A fresh incarnation finds nothing in flight and starts it; an attempt still in flight\n * here claims again, later each time (20 s, 40 s, … `REVIVE_AFTER_MAX_MS`). */\n async revive(): Promise<void> {\n this.#lastBatchAtHead = false;\n await this.catchUpFromLog();\n // A catch-up that reached the head already ran the at-head pass (rule 5); one that found\n // nothing to read runs it here, once.\n if (!this.#lastBatchAtHead)\n await this.#runOnSerialChain(async () => {\n this.#reducedState = (\n await this.#reduceAndProcessEvent(null, this.#reducedState, true)\n ).state;\n this.publishLiveState();\n });\n if (this.#backgroundWorkInFlight === 0) return;\n this.#revivesWhileBusy += 1;\n this.#claim(Math.min(REVIVE_AFTER_MS * 2 ** this.#revivesWhileBusy, REVIVE_AFTER_MAX_MS));\n // Awaited: the pass that called this derives its next deadline as soon as it returns, so the\n // claim must have landed by then — one alarm write, not a delete and a set.\n await this.#claimChain;\n }\n\n /** THE GUARDED REDUCE, shared by the live flow and the version replay. A reducer that throws on an\n * event (malformed, or one an OLDER version accepted) must never wedge the processor: on a version\n * replay it would fail the catch-up before the new checkpoint is written, every incarnation. */\n #reduceOrKeep(event: StreamEvent, state: State): State {\n try {\n return this.processor.reduce({ event, state }) ?? state;\n } catch (error) {\n reportIssue(\"processor.reduce\", error, { slug: this.#contract.slug, offset: event.offset });\n return state;\n }\n }\n\n /** Validate a consumed event's payload against the contract's declared schema, then reduce it — or,\n * for a malformed payload, skip the fold and report (it must never corrupt reduced state, the\n * exported view a live client parses). Returns the next state AND the event to carry onward,\n * NORMALIZED to the schema's `z.output` (coercions/defaults applied) when it validated — so the\n * reducer, the effect hook, and the version replay all see exactly what `ConsumedEvent<Contract>`\n * promises. SHARED by the live flow and `#rereduceIfVersionChanged`, so the two can never diverge\n * (a coercion applied live but not on replay would make a version bump rewrite state). A payload-less\n * event validates as `{}` (the \"empty defaults\" convention the contract requires of its stateSchema);\n * a contract with no `events` catalog (the kernel-generic processors) folds unvalidated. */\n #validateNormalizeAndReduce(\n event: StreamEvent,\n state: State,\n ): { state: State; event: StreamEvent; valid: boolean } {\n // oxlint-disable-next-line iterate/simple-truthiness-check -- only a null/undefined payload defaults to {}; a falsy NON-object payload off the wire (0, false, \"\") must fail schema validation and be reported, not be folded as an empty object\n const parsed = this.#contract.payloadSchemaFor?.(event.type)?.safeParse(event.payload ?? {});\n if (parsed && !parsed.success) {\n reportIssue(\"processor.reduce.payload\", parsed.error, {\n slug: this.#contract.slug,\n offset: event.offset,\n type: event.type,\n });\n return { state, event, valid: false }; // malformed: not folded, and the caller skips its effect\n }\n // Owned-event payloads are object schemas, so `z.output` is a record.\n const normalized = parsed\n ? { ...event, payload: parsed.data as Record<string, unknown> }\n : event;\n return { state: this.#reduceOrKeep(normalized, state), event: normalized, valid: true };\n }\n\n /** THE per-event primitive (rules 2–3) — the batch loop and the eventless at-head pass both come\n * here: a GUARDED reduce, then `processEvent` with a FIFO blocker chain drained to a FIXED POINT.\n * Returns the next state and whether the effect ran — a malformed payload for a KNOWN event is\n * SKIPPED for BOTH reduce and effect (the effect hook is typed against `ConsumedEvent`'s `z.output`,\n * so handing it garbage would throw and wedge the batch — checkpoints never advance, catch-up\n * refails the same row); `processed: false` lets the batch fall back to the eventless caught-up pass.\n * Owns NO cursor / persist / waiter — the caller does. */\n async #reduceAndProcessEvent(\n event: StreamEvent | null,\n state: State,\n caughtUp: boolean,\n ): Promise<{ state: State; processed: boolean }> {\n const { slug, version, emits } = this.#contract;\n const previousState = state;\n if (event) {\n // Validate + normalize + fold via the ONE shared path (so replay can't diverge). On the valid\n // path the reducer AND the effect hook below both see the NORMALIZED event (schema `z.output`);\n // a malformed payload is neither folded nor delivered to the (typed) effect hook.\n const reduced = this.#validateNormalizeAndReduce(event, state);\n if (!reduced.valid) return { state: reduced.state, processed: false };\n state = reduced.state;\n event = reduced.event;\n }\n // FIFO blocker chain for THIS event (rule 2); background work escapes it (rule 3).\n let blockers: Promise<unknown> = Promise.resolve();\n // Validated against the declared `emits` and stamped with provenance. A certificate an entity\n // cross-posts to ANOTHER context (`/`, the project catalog) is its own `itx.cd(path).append(...)`\n // through the host's `withItx` — the context's own append, no second verb here.\n const stamped = (emittedEvents: StreamEventInput[]): StreamEventInput[] => {\n for (const emitted of emittedEvents) {\n if (!emits.includes(emitted.type))\n throw new Error(\n `processor \"${slug}\" emits ${JSON.stringify(emitted.type)} without declaring it`,\n );\n emitted.source = {\n processor: {\n slug,\n version,\n ...(event && { whileProcessing: { offset: event.offset, type: event.type } }),\n },\n };\n }\n return emittedEvents;\n };\n this.processor.processEvent({\n event,\n state,\n previousState,\n append: async (...emittedEvents) => await this.#stream.append(...stamped(emittedEvents)),\n blockProcessorWhile: (work) => {\n blockers = blockers.then(() => work());\n },\n runInBackground: (work) => this.#runInBackground(work),\n delivery: { caughtUp },\n });\n // STRICT PER-EVENT ORDERING (rule 2): drain the blocker chain to a FIXED POINT. A\n // blockProcessorWhile called from INSIDE a running blocker extends the chain (still THIS event's\n // blocking work), so re-await until it stops growing — latching the pre-nesting snapshot would\n // let the next event's processEvent (and the batch commit) overtake it.\n for (let awaited: Promise<unknown> | undefined; awaited !== blockers; ) {\n awaited = blockers;\n await awaited;\n }\n return { state, processed: true };\n }\n\n /** The checkpoint write, with the latch: a refusal stamped `retryable: false` can only repeat. */\n #writeCheckpointOrLatch(\n slug: string,\n cursor: { reducerVersion: string; reducedThroughOffset: number },\n state: State,\n stateChanged: boolean,\n ): void {\n try {\n this.#storage.write(slug, cursor, state, stateChanged);\n } catch (error) {\n if ((error as { retryable?: unknown } | null)?.retryable === false)\n this.#latchedRefusal = error instanceof Error ? error : new Error(String(error));\n throw error;\n }\n }\n\n /** Resolve the waiters a cursor advance satisfies; keep the rest. */\n #resolveWaitUntilProcessedWaiters(reducedThroughOffset: number): void {\n for (const w of this.#waitUntilProcessedWaiters.splice(0)) {\n if (reducedThroughOffset >= w.offset) w.resolve();\n else this.#waitUntilProcessedWaiters.push(w);\n }\n }\n}\n\n// ── events ── the stream event envelope + idempotency rules. Zod-FREE: the envelope carries no\n// runtime validator (the processor contract section below has the zod half).\n\n/** What `append` accepts: the event body, before the stream assigns its committed identity. The\n * append method checks ONE rule by hand: `type` is a non-empty string. */\nexport type StreamEventInput = {\n /** `events.iterate.com/<namespace>/<event>` for the platform's types, named by the rules in\n * packages/iterate/README.md#event-types; any other string is the appender's own. */\n type: string;\n payload?: Record<string, unknown>;\n metadata?: Record<string, unknown>;\n /** Provenance: which processor (while processing what) appended this — stamped by the engine's\n * `append` — and WHO: the session's verified principal (src/principal.ts), set by the DO's append\n * root from the session's project token and never taken from a client. */\n source?: {\n /** The durable schedule definition responsible for this occurrence. */\n schedule?: {\n key: string;\n scheduledAtOffset: number;\n at: string;\n /** Attribution of the definition, distinct from the platform writing the occurrence. */\n definedBy?: Omit<NonNullable<StreamEventInput[\"source\"]>, \"schedule\">;\n };\n processor?: {\n slug: string;\n version: string;\n whileProcessing?: { offset: number; type: string };\n };\n principal?: { actor: string; email?: string };\n /** THE CONNECTION the principal acted through: the OAuth grant's id — one per\n * connected client (a Claude Code install, a dash sign-in, a personal token). Stamped beside\n * `principal` by the platform when it appends; absent for the admin secret and the kernel. */\n grant?: string;\n /** THE PLATFORM WROTE THIS FACT, on the principal's behalf:\n * what a processor folding an account's or an organization's facts requires — a client can\n * append any type to a context it holds, never this. */\n platform?: true;\n };\n /** Same key + same body = dedupe (the existing event is returned); different body = loud error. */\n idempotencyKey?: string;\n /** OPTIONAL PRECONDITION: land at exactly this offset or refuse the whole batch with\n * OFFSET_CONFLICT — \"nothing has happened since I last looked\". Never stored in the body. */\n offset?: number;\n /** An EPHEMERAL event rides the stream to live subscribers but is NEVER persisted: it consumes an\n * offset, triggers zero writes, and its body is gone the moment the incarnation ends — nobody can\n * redeliver it (stream.ts, the zero-write contract). A durable OMITS the field. */\n ephemeral?: true;\n};\n\n/** A committed event: the input plus the identity the stream assigned at its commit point. */\nexport type StreamEvent = Omit<StreamEventInput, \"offset\"> & {\n offset: number;\n createdAt: string;\n path: string;\n};\n\n// ── idempotency (message text stays greppable across RPC hops) ──\n\nexport function idempotencyConflictMessage(idempotencyKey: string, existingOffset: number): string {\n return `idempotency key \"${idempotencyKey}\" already names a different event at offset ${existingOffset}`;\n}\n\n/** Structural equality of the parts an idempotent retry must not change. */\nexport function sameIdempotentEvent(\n existingEvent: StreamEventInput,\n requestedEvent: StreamEventInput,\n): boolean {\n return (\n existingEvent.type === requestedEvent.type &&\n jsonEqual(existingEvent.payload, requestedEvent.payload) &&\n jsonEqual(existingEvent.metadata, requestedEvent.metadata)\n );\n}\n\n// ── reduce checkpoint ── THE ONE spelling of a persisted reduce checkpoint, shared by BOTH hosts (the\n// stream's core reduce and the facet-hosted `ProcessorEngine`). ONE ROW per slug: the reducer\n// version, the offset reduced through, and the state as JSON (NULL = the reduce never changed it =\n// `initialState()` — a pure side-effect processor reusing its cursor never re-fires its effect\n// history). ONE statement per write, so a checkpoint can never tear; the state column is rewritten\n// only when the reduce changed it (`COALESCE`).\n//\n// THE CELL CEILING: a checkpoint is one SQLite cell — 2 MB in production, SQLITE_TOOBIG past it. A\n// state whose JSON would not fit is refused BEFORE the write with a coded error, so the caller sees\n// why instead of the platform's raw message, and nothing lands. No cloudflare:workers import on\n// purpose: this module rides the SDK bundle into every facet isolate.\n\n/** Under the 2 MB cell, with room for the row's other columns. */\nconst REDUCE_CHECKPOINT_STATE_MAX_CHARS = 2 * 1024 * 1024 - 4096;\n\n/** Sync SQLite as the platform hands it over (`ctx.storage.sql`): a query is a LAZY cursor —\n * iterate it, or `toArray()`. Spelled structurally so a node:sqlite stand-in satisfies it. */\nexport type SqlStorageHandle = {\n exec<T extends Record<string, SqlStorageValue>>(\n query: string,\n ...bindings: unknown[]\n ): Iterable<T> & { toArray(): T[] };\n};\n\n/** A persisted checkpoint as read back: the version it was reduced under (the caller gates on it),\n * the offset reduced through, and the state — `undefined` when the reduce never changed it. */\nexport type ReduceCheckpoint<State> = {\n reducerVersion: string;\n reducedThroughOffset: number;\n state: State | undefined;\n};\n\n/** What BOTH hosts read and write their checkpoints through — the stream's storage and a facet's\n * own (the Node unit tests drive it over node:sqlite, stream/test-support.ts). */\nexport class ReduceCheckpointTable {\n readonly #sql: SqlStorageHandle;\n\n /** `createTable: false` when the caller knows the table exists (the stream's storage skips every\n * CREATE on a re-wake); a facet host constructs one per incarnation and lets it create. */\n constructor(sql: SqlStorageHandle, options: { createTable: boolean } = { createTable: true }) {\n this.#sql = sql;\n if (options.createTable) ReduceCheckpointTable.createTable(sql);\n }\n\n static createTable(sql: SqlStorageHandle): void {\n sql.exec(\n `CREATE TABLE IF NOT EXISTS reduce_checkpoints (\n slug TEXT PRIMARY KEY,\n reducer_version TEXT NOT NULL,\n reduced_through_offset INTEGER NOT NULL,\n state TEXT\n )`,\n );\n }\n\n read<State>(slug: string): ReduceCheckpoint<State> | undefined {\n const row = this.#sql\n .exec<{ reducer_version: string; reduced_through_offset: number; state: string | null }>(\n \"SELECT reducer_version, reduced_through_offset, state FROM reduce_checkpoints WHERE slug = ?\",\n slug,\n )\n .toArray()[0];\n if (!row) return undefined;\n return {\n reducerVersion: String(row.reducer_version),\n reducedThroughOffset: Number(row.reduced_through_offset),\n state: row.state ? (JSON.parse(String(row.state)) as State) : undefined,\n };\n }\n\n /** ALWAYS the cursor; the state ONLY when `stateChanged` — one write either way. */\n write<State>(\n slug: string,\n cursor: { reducerVersion: string; reducedThroughOffset: number },\n state: State,\n stateChanged: boolean,\n ): void {\n const serializedState = stateChanged ? (JSON.stringify(state) ?? null) : null;\n // Stamped `retryable: false` (the flag workerd itself uses): the same state serializes to the\n // same size on every retry — a delivery loop halts on it instead of climbing its ladder.\n if (serializedState && serializedState.length > REDUCE_CHECKPOINT_STATE_MAX_CHARS)\n throw Object.assign(\n codedError(\n \"REDUCE_CHECKPOINT_TOO_LARGE\",\n `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`,\n { slug, chars: serializedState.length, maxChars: REDUCE_CHECKPOINT_STATE_MAX_CHARS },\n ),\n { retryable: false },\n );\n this.#sql.exec(\n `INSERT INTO reduce_checkpoints (slug, reducer_version, reduced_through_offset, state)\n VALUES (?, ?, ?, ?)\n ON CONFLICT(slug) DO UPDATE SET\n reducer_version = excluded.reducer_version,\n reduced_through_offset = excluded.reduced_through_offset,\n state = COALESCE(excluded.state, reduce_checkpoints.state)`,\n slug,\n cursor.reducerVersion,\n cursor.reducedThroughOffset,\n serializedState,\n );\n }\n}\n\n// ── live state ── THE live-state primitive: one value, its revision chain, and the diff→emit\n// dance Phoenix LiveView does, over the project's stream. Used two ways: a mini-app DO owns one\n// directly as its store; the ProcessorEngine owns one per processor and `set`s the projection after\n// every batch (sdk/index.ts shows both).\n//\n// MUTATION AND NOTIFICATION ARE INSEPARABLE: `set(next)` diffs the held value → next; on a real\n// change it bumps the revision and appends the ephemeral `itx/live-state-changed` delta carrying\n// `{key, from, to, patch}` onto the stream. `snapshot()` is the SEED READ. The stream keeps no\n// per-subscriber state — the CLIENT owns its chain: seed from `snapshot()`, apply a payload whose\n// `from` matches its held rev, re-read the seed on any mismatch (live-state-chains-client-side.e2e).\n//\n// The revision is seeded from a per-incarnation EPOCH (not 0): a reborn holder mints a fresh epoch,\n// so every stale client rev mismatches and re-reads the seed instead of applying a patch onto a\n// diverged base. Lossy by contract — a dropped delta append is a chain gap the client heals, never\n// state loss. HARD RULE: no processor can ever REDUCE the delta (processor.ts `reducesEvent`), so a\n// state-change notification can never feed a reduce; a SUBSCRIPTION may name the type to watch it.\n\n/** A delta whose patch is over this many chars is not sent: a whole-array replace of a large\n * projection would cost every watcher the projection per set, and past the event ceiling the append\n * would refuse it outright. The delta rides with `patch: null` instead — the rev moved, re-seed. */\nconst LIVE_STATE_PATCH_MAX_CHARS = 1024 * 1024;\n\n/** The only thing a LiveState needs from its host: somewhere to append the delta. A\n * `ProcessorStream` satisfies it; a facet that is no processor passes one round trip per delta,\n * `{ append: (e) => withItx(this.env.ITX, (itx) => itx.append(e)) }` (sdk/index.ts), never a scope it\n * holds. */\nexport type LiveStateSink = {\n append(event: { type: string; ephemeral?: true; payload?: Record<string, unknown> }): unknown;\n};\n\nexport class LiveState<S> {\n readonly #liveStateSink: LiveStateSink;\n readonly #liveStateKey: string;\n #state: S;\n /** The DIFF BASE: the last value that serialized — what a client that applied every delta holds.\n * Kept apart from `#state` so a value the wire cannot carry, adopted without an emit, never\n * becomes the base every later diff would throw against. */\n #lastSerializedState: S;\n #liveStateRev: number;\n /** THE DELTA APPEND CHAIN — at most one delta append in flight, so commit order = mint order for a\n * CROSS-HOP sink: `withItx(env.ITX, (itx) => itx.append(e))` mints a FRESH scope per call, so two deltas\n * issued in different turns race across the hop and the second can commit first — ~14% of rapid\n * pairs on the deployed edge (never locally, the hop is sub-ms). Nothing is dropped by a reorder,\n * but it costs every watcher the full seed re-read the deltas exist to avoid. Every delta rides\n * this ONE chain and is emitted only after the previous append settles (see set()). Nobody waits\n * on this. */\n #liveStateDeltaAppendChain: Promise<unknown> = Promise.resolve();\n\n constructor(sink: LiveStateSink, key: string, initial: S) {\n this.#liveStateSink = sink;\n this.#liveStateKey = key;\n this.#state = initial;\n this.#lastSerializedState = initial;\n this.#liveStateRev = Date.now() * 4096 + Math.floor(Math.random() * 4096);\n }\n\n /** The current value (reflects every `set`). */\n get(): S {\n return this.#state;\n }\n\n /** THE seed read: `{rev, state}` read together (single-threaded ⇒ atomically), which is what lets\n * a client chain patches exactly instead of guessing which changes its snapshot already contains. */\n snapshot(): { rev: number; state: S } {\n return { rev: this.#liveStateRev, state: this.#state };\n }\n\n /** Replace the value: diff the last serialized base → next; on a real change bump the revision\n * and append the delta. Build a NEW value (don't mutate `next` in place) — the diff is over JSON.\n * A diff/append failure degrades to a LOST notification (the client re-seeds on the chain gap),\n * never a throw the caller sees. */\n set(next: S): void {\n // The SAME object is the same JSON (the contract forbids in-place mutation, which is what makes\n // identity a proof of equality): no diff, no delta, no rev move — the common case for a\n // processor whose projection is its unchanged reduced state.\n if (next === this.#lastSerializedState) return;\n // A `next` the wire cannot carry (a BigInt, a cycle) throws in the diff: adopt it anyway, and\n // STILL advance the rev — the bump mints the chain gap that forces a stale client's re-seed\n // (without it, a later emit's `from` would match the client's held rev and it would apply a\n // patch computed against a base it never received: silent corruption). The serialized base\n // stays put, so the next serializable value emits as a diff from what the client last saw.\n let patch;\n try {\n patch = diff(this.#lastSerializedState, next);\n } catch {\n this.#state = next;\n this.#liveStateRev += 1;\n return;\n }\n this.#state = next;\n this.#lastSerializedState = next;\n if (!patch) return;\n const from = this.#liveStateRev;\n const to = from + 1; // a LOCAL: a later set's rev must not be read into this delta's payload\n this.#liveStateRev = to;\n const wirePatch = JSON.stringify(patch).length > LIVE_STATE_PATCH_MAX_CHARS ? null : patch;\n const emitDelta = () =>\n this.#liveStateSink.append({\n type: \"events.iterate.com/itx/live-state-changed\",\n ephemeral: true,\n payload: { key: this.#liveStateKey, from, to, patch: wirePatch },\n });\n // A cross-hop sink mints a FRESH capability per call, so two deltas can race on the wire: every\n // delta rides ONE chain, each emitted only after the previous append settles, so mint order IS\n // the delivery order. No in-flight flag + synchronous fast path — clearing the flag in the first\n // append's `finally` while a later delta was still queued let a newer delta see \"idle\", fork a\n // parallel chain, and overtake the queued one (commit order 1, 3, 2). The chain's own `.catch`\n // contains a dropped delta (a sync throw or a rejection): a chain gap the client heals (the rev\n // already advanced); it must never reach the caller.\n this.#liveStateDeltaAppendChain = this.#liveStateDeltaAppendChain\n .then(emitDelta)\n .catch(() => {});\n }\n}\n\n// ── processor contract ── the focused Zod contract helper; zod\n// rides the SDK bundle with it. A contract declares its\n// identity, reduced-state schema, the events it OWNS (`events`, keyed by the durable type string,\n// each with a zod payload schema — so the type strings and payload shapes are visible right here),\n// the events it `consumes`/`emits`, and optional `processorDeps` (other contracts whose events it may\n// consume without owning). The reduce's event union and the state type are DERIVED from the contract\n// (`ConsumedEvent` / `ProcessorState`) — no hand-kept discriminated union to drift.\n\n/** One owned event: its description and the zod schema for its payload. `ephemeral: true` marks a\n * non-durable event (delivered only when its type is named in `consumes`). */\nexport type EventDefinition = { description: string; payloadSchema: z.ZodType; ephemeral?: true };\n/** A durable event type string → its definition. */\nexport type EventCatalog = Record<string, EventDefinition>;\n\n/** A `processorDeps` entry's own event catalog. */\ntype DepCatalog<Dep> = Dep extends { events: infer Events extends EventCatalog } ? Events : never;\n/** The definition owning `Type` — local events win, then each dep. */\ntype DefinitionForType<\n Events extends EventCatalog,\n Deps extends readonly unknown[],\n Type extends string,\n> = Type extends keyof Events\n ? Events[Type]\n : Deps[number] extends infer Dep\n ? Dep extends unknown\n ? Type extends keyof DepCatalog<Dep>\n ? DepCatalog<Dep>[Type]\n : never\n : never\n : never;\n\n/** The committed event for one resolved type: `StreamEvent` narrowed to its `{ type, payload }`. */\ntype EventForType<\n Events extends EventCatalog,\n Deps extends readonly unknown[],\n Type extends string,\n> = Type extends unknown\n ? DefinitionForType<Events, Deps, Type> extends { payloadSchema: infer Schema extends z.ZodType }\n ? StreamEvent & { type: Type; payload: z.output<Schema> }\n : never\n : never;\n\n/** The reduce union for a `consumes` tuple — `\"*\"` alone means any `StreamEvent`. */\ntype EventForTypes<\n Events extends EventCatalog,\n Deps extends readonly unknown[],\n Types extends readonly string[],\n> = \"*\" extends Types[number] ? StreamEvent : EventForType<Events, Deps, Types[number]>;\n\n/** A contract's `processorDeps` tuple, defaulting to empty. */\ntype DepsOf<Contract> = Contract extends { processorDeps: infer Deps extends readonly unknown[] }\n ? Deps\n : readonly [];\n\n/** A contract's reduced-state type, inferred from its `stateSchema`. */\nexport type ProcessorState<Contract> = Contract extends {\n stateSchema: infer Schema extends z.ZodType;\n}\n ? z.output<Schema>\n : never;\n\n/** The committed-event union a contract's `consumes` list can deliver to `reduce`/`processEvent`. */\nexport type ConsumedEvent<Contract> = Contract extends {\n events: infer Events extends EventCatalog;\n consumes: infer Consumes extends readonly string[];\n}\n ? EventForTypes<Events, DepsOf<Contract>, Consumes>\n : never;\n\n/** The input for ONE event type as a catalog spells it (`EventInput`'s row) — or, for a type no\n * catalog defines (a core control event a processor emits, `itx/ingress-configured`), the plain\n * input: it widens the whole union, so a contract that emits one undefined type appends untyped\n * until that type is in a catalog it depends on. */\ntype EventInputForType<\n Events extends EventCatalog,\n Deps extends readonly unknown[],\n Type extends string,\n> = Type extends unknown\n ? [DefinitionForType<Events, Deps, Type>] extends [never]\n ? StreamEventInput\n : DefinitionForType<Events, Deps, Type> extends {\n payloadSchema: infer Schema extends z.ZodType;\n }\n ? {\n type: Type;\n payload: z.input<Schema>;\n idempotencyKey?: string;\n metadata?: Record<string, unknown>;\n } & (DefinitionForType<Events, Deps, Type> extends { ephemeral: true }\n ? { ephemeral: true }\n : { ephemeral?: never })\n : never\n : never;\n\n/** What a processor's `append` takes: one input per type the contract `emits` — its own\n * events and its deps' as their catalogs spell them (`z.input`), a type no catalog defines as the\n * plain input under that name. A contract whose `emits` is not a literal tuple gets every input. */\nexport type EmittedEventInput<Contract> = Contract extends {\n events: infer Events extends EventCatalog;\n emits: infer Emits extends readonly string[];\n}\n ? string[] extends Emits\n ? StreamEventInput\n : Emits extends readonly []\n ? StreamEventInput // emits nothing: no call to type, and `never` would break the host's variance\n : EventInputForType<Events, DepsOf<Contract>, Emits[number]>\n : StreamEventInput;\n\n/** What a caller APPENDS for one of a contract's OWNED events — the typed write on an entity\n * (`itx.agents.get(path).append(…)`, library.ts): the type string, the payload as its schema takes\n * it (`z.input`), a key and metadata; `ephemeral` only where the definition says so. Derived from\n * the catalog, so a payload field renamed in the contract is a type error at every call site. */\nexport type EventInput<Contract> = Contract extends { events: infer Events extends EventCatalog }\n ? {\n [Type in keyof Events & string]: {\n type: Type;\n payload: z.input<Events[Type][\"payloadSchema\"]>;\n idempotencyKey?: string;\n metadata?: Record<string, unknown>;\n } & (Events[Type] extends { ephemeral: true } ? { ephemeral: true } : { ephemeral?: never });\n }[keyof Events & string]\n : never;\n\n/** What `defineProcessorContract` returns: the base the engine reads, plus the events catalog and the\n * resolved deps. (Events are written LITERALLY at the call site — `itx.append({ type, payload })` —\n * so there is no event-builder here; the engine validates the payload against `payloadSchemaFor` at\n * reduce, and `ConsumedEvent`/`ProcessorState` give the reduce its types.) */\nexport type DefinedProcessorContract<\n StateSchema extends z.ZodType,\n Events extends EventCatalog,\n Consumes extends readonly string[],\n Deps extends readonly unknown[],\n Emits extends readonly string[] = readonly string[],\n> = ProcessorContract<z.output<StateSchema>> & {\n stateSchema: StateSchema;\n events: Events;\n // The literal consumes and emits tuples are preserved (not widened to string[]) so `ConsumedEvent`\n // and `EmittedEventInput` can map each type to its event; the base ProcessorContract only needs\n // `readonly string[]`.\n consumes: Consumes;\n emits: Emits;\n processorDeps: Deps;\n};\n\nexport function defineProcessorContract<\n const StateSchema extends z.ZodType,\n const Events extends EventCatalog = Record<string, never>,\n const Consumes extends readonly string[] = readonly string[],\n const Deps extends readonly { events: EventCatalog }[] = readonly [],\n const Emits extends readonly string[] = readonly string[],\n>(contract: {\n slug: string;\n version: string;\n description: string;\n /** Must parse `{}` — the initial state is `stateSchema.parse({})` (all fields defaulted). */\n stateSchema: StateSchema;\n /** The events this contract OWNS, keyed by durable type string. Omit for a kernel-generic\n * processor that types its own reduce through the `Event` param instead of an events catalog. */\n events?: Events;\n /** Other processors' contracts whose events this one may `consumes`/`emits` without owning. */\n processorDeps?: Deps;\n consumes: Consumes;\n emits: Emits;\n}): DefinedProcessorContract<StateSchema, Events, Consumes, Deps, Emits> {\n if (!contract.stateSchema.safeParse({}).success)\n throw new Error(`contract \"${contract.slug}\": stateSchema must parse {} (default every field)`);\n const events = (contract.events ?? {}) as Events;\n const processorDeps = (contract.processorDeps ?? []) as Deps;\n // One owner per event type: a local event may not shadow a dep's event, and two deps may not both\n // declare one. Otherwise `resolve` (and the runtime payload validation it backs) would pick just the\n // first while `ConsumedEvent`'s type union includes BOTH payload types — a second dep's events would\n // then validate against the wrong schema.\n const depEventTypes = new Set<string>();\n for (const dep of processorDeps as readonly { events: EventCatalog }[])\n for (const type of Object.keys(dep.events)) {\n if (type in events)\n throw new Error(`contract \"${contract.slug}\": event \"${type}\" is already owned by a dep`);\n if (depEventTypes.has(type))\n throw new Error(`contract \"${contract.slug}\": event \"${type}\" is declared by two deps`);\n depEventTypes.add(type);\n }\n const resolve = (type: string): EventDefinition | undefined =>\n events[type] ??\n (processorDeps as readonly { events: EventCatalog }[])\n .map((dep) => dep.events[type])\n .find(Boolean);\n return {\n slug: contract.slug,\n version: contract.version,\n description: contract.description,\n consumes: contract.consumes,\n emits: contract.emits,\n stateSchema: contract.stateSchema,\n events,\n processorDeps,\n initialState: () => contract.stateSchema.parse({}) as z.output<StateSchema>,\n payloadSchemaFor: (type: string) => resolve(type)?.payloadSchema,\n };\n}\n","/// <reference types=\"node\" />\n// stream/test-support.ts — the in-memory stand-ins a processor's unit tests drive the engine with\n// (`iterate/stream/test-support`, Node only: node:sqlite). The SDK's own engine tests use them\n// (processor.test.ts, processor-rules.test.ts), and so does every processor author, first-party or\n// not, so the commit semantics the tests assume are ONE copy.\n//\n// reduceProcessor — fold events through a processor's pure `reduce`, as the engine does\n// memoryStream — the Stream's commit semantics in memory (one offset sequence, idempotency on\n// append, the scanned-range proof) plus THE PUMP: a fire-and-forget\n// `processEventBatch` to every engine in `engines` after each append (awaited, it\n// would deadlock a processor that appends during its own batch). A short page's\n// proof is the in-memory head, so the engine's stale-push and ephemeral-window\n// rules are exercised directly; the platform's real Stream stops at the DURABLE\n// mark\n// memoryStorage — the real `ReduceCheckpointTable` over node:sqlite, counting writes\n// nodeSqliteDurableObjectStorage — a Durable Object's `sql` + `transactionSync` over node:sqlite\n// settle — wait for fire-and-forget pushes to land\nimport { DatabaseSync } from \"node:sqlite\";\nimport type { SqlStorageValue } from \"@cloudflare/workers-types\";\nimport {\n idempotencyConflictMessage,\n sameIdempotentEvent,\n type StreamEvent,\n type StreamEventInput,\n type ProcessorContract,\n type ProcessorEngine,\n type ProcessorStream,\n ReduceCheckpointTable,\n type SqlStorageHandle,\n} from \"./processor.ts\";\n\n/** THE PROCESSOR HARNESS: fold `inputs` through a processor's pure `reduce`, exactly as the engine\n * does — start from the contract's initial state, validate each payload against the contract (a\n * malformed KNOWN payload is SKIPPED, never reduced), reduce, thread the state — for a declarative\n * `{ events → state }` processor spec with no engine, storage, or effects. Construct the\n * processor with `new` and hand it the events; the offsets are the input order. Ephemeral inputs are\n * reduced like any other — the reduce decides what it folds (presence's `poke` returns undefined). */\nexport function reduceProcessor<State>(\n processor: {\n contract: ProcessorContract<State>;\n // Method syntax (bivariant params) so a processor whose reduce narrows `event` to its own\n // contract's union is accepted — the harness only ever hands it events its contract consumes.\n reduce(args: { event: StreamEvent; state: State }): State | null | undefined;\n },\n inputs: readonly { type: string; payload?: unknown; source?: StreamEvent[\"source\"] }[],\n): State {\n let state = processor.contract.initialState();\n inputs.forEach((input, index) => {\n const parsed = processor.contract\n .payloadSchemaFor?.(input.type)\n ?.safeParse(input.payload ?? {});\n if (parsed && !parsed.success) return; // the engine skips a malformed known payload\n const event = {\n type: input.type,\n payload: parsed?.success ? parsed.data : input.payload,\n source: input.source,\n offset: index + 1,\n createdAt: new Date((index + 1) * 1000).toISOString(),\n path: \"/\",\n } as StreamEvent;\n state = processor.reduce({ event, state }) ?? state;\n });\n return state;\n}\n\nexport function memoryStream(path = \"/\") {\n const durableEvents: StreamEvent[] = []; // the durable log — what `read` answers\n const pushedEvents: StreamEvent[] = []; // every committed event, ephemerals included (the pump's view)\n const eventsByIdempotencyKey = new Map<string, StreamEvent>();\n const engines: ProcessorEngine<any>[] = []; // the pump only needs `processEventBatch`\n // The engine's claims on the context's alarm, RECORDED in order (a time, or null = released); a\n // test that wants the revive calls `engine.revive()` itself.\n const claims: (number | null)[] = [];\n let maxAssigned = 0;\n let reads = 0;\n const stream: ProcessorStream = {\n claim: (at) => {\n claims.push(at);\n return Promise.resolve();\n },\n append: (...events: StreamEventInput[]) => {\n const scannedAfterOffset = maxAssigned;\n const committedEvents = events.map((event) => {\n if (event.idempotencyKey) {\n const existingEvent = eventsByIdempotencyKey.get(event.idempotencyKey);\n if (existingEvent) {\n if (sameIdempotentEvent(existingEvent, event)) return existingEvent;\n throw new Error(idempotencyConflictMessage(event.idempotencyKey, existingEvent.offset));\n }\n }\n maxAssigned += 1;\n const committedEvent: StreamEvent = {\n ...event,\n offset: maxAssigned,\n createdAt: new Date(0).toISOString(),\n path,\n };\n if (!event.ephemeral) {\n durableEvents.push(committedEvent);\n if (event.idempotencyKey)\n eventsByIdempotencyKey.set(event.idempotencyKey, committedEvent);\n }\n return committedEvent;\n });\n pushedEvents.push(...committedEvents);\n if (maxAssigned > scannedAfterOffset) {\n const scannedOffsetRange = { after: scannedAfterOffset, through: maxAssigned };\n for (const engine of engines)\n void engine.processEventBatch(committedEvents, scannedOffsetRange).catch(() => {});\n }\n return committedEvents;\n },\n read: (afterOffset = 0, limit = 500) => {\n reads += 1;\n const page = durableEvents.filter((event) => event.offset > afterOffset).slice(0, limit);\n return Promise.resolve({\n events: page,\n scannedThroughOffset:\n page.length === limit ? page[page.length - 1].offset : Math.max(afterOffset, maxAssigned),\n // Short page, or a full page whose last row is the durable head (the real Stream's rule).\n atHead:\n page.length < limit ||\n page[page.length - 1].offset === durableEvents[durableEvents.length - 1].offset,\n });\n },\n };\n return {\n stream,\n events: durableEvents,\n pushedEvents,\n engines,\n claims,\n get reads() {\n return reads;\n },\n };\n}\n\n/** A facet's checkpoint table (processor.ts `ReduceCheckpointTable`) over an in-memory\n * node:sqlite database — the real table, so the unit tests checkpoint exactly as a facet does —\n * with `writes` counting every write: rule 4 (\"one durable commit per batch\") and the ephemeral\n * zero-write rule are pinned by counting it. */\nclass WriteCountingReduceCheckpointTable extends ReduceCheckpointTable {\n writes = 0;\n override write<State>(\n slug: string,\n cursor: { reducerVersion: string; reducedThroughOffset: number },\n state: State,\n stateChanged: boolean,\n ): void {\n this.writes++;\n super.write(slug, cursor, state, stateChanged);\n }\n}\n\nexport function memoryStorage(): WriteCountingReduceCheckpointTable {\n return new WriteCountingReduceCheckpointTable(nodeSqliteDurableObjectStorage().sql);\n}\n\n/** Wait `ms` on a timer — long enough, by default, for fire-and-forget pushes to land. */\nexport const settle = (ms = 25) => new Promise((r) => setTimeout(r, ms));\n\n// ── node:sqlite durable object storage ── a Durable Object's `sql` and `transactionSync` over\n// node:sqlite, so the REAL checkpoint table, and anything else written against a Durable Object's\n// storage, runs in plain Node, where a capped V8 enforces the isolate memory limit local workerd\n// does not. Faithful where memory is concerned: `exec` hands back node:sqlite's LAZY row\n// iterator, as workerd's cursor is, and `transactionSync` is a real BEGIN/ROLLBACK, so a throw\n// inside a commit undoes its rows. The cell ceiling workerd enforces (2 MB, SQLITE_TOOBIG) is not\n// reproduced here: the typed modules refuse before it, coded (processor.ts `ReduceCheckpointTable`).\n\nexport function nodeSqliteDurableObjectStorage(): {\n sql: SqlStorageHandle;\n transactionSync<T>(closure: () => T): T;\n} {\n const db = new DatabaseSync(\":memory:\");\n return {\n sql: {\n exec<T extends Record<string, SqlStorageValue>>(query: string, ...bindings: unknown[]) {\n const statement = db.prepare(query);\n const bound = bindings as (string | number | null)[];\n // A write runs NOW (an un-consumed `iterate()` never executes); a read stays lazy.\n if (statement.columns().length === 0) {\n statement.run(...bound);\n return Object.assign([] as T[], { toArray: () => [] as T[] });\n }\n const rows = statement.iterate(...bound) as IterableIterator<T>;\n return Object.assign(rows, { toArray: () => [...rows] });\n },\n },\n transactionSync: <T>(closure: () => T): T => {\n db.exec(\"BEGIN\");\n try {\n const result = closure();\n db.exec(\"COMMIT\");\n return result;\n } catch (error) {\n db.exec(\"ROLLBACK\");\n throw error;\n }\n },\n };\n}\n"],"mappings":";;;;AA0vBA,SAAgB,2BAA2B,gBAAwB,gBAAgC;CACjG,OAAO,oBAAoB,eAAe,8CAA8C;AAC1F;;AAGA,SAAgB,oBACd,eACA,gBACS;CACT,OACE,cAAc,SAAS,eAAe,QACtC,UAAU,cAAc,SAAS,eAAe,OAAO,KACvD,UAAU,cAAc,UAAU,eAAe,QAAQ;AAE7D;;AAeA,MAAM,oCAAoC,IAAI,OAAO,OAAO;;;AAqB5D,IAAa,wBAAb,MAAa,sBAAsB;CACjC;;;CAIA,YAAY,KAAuB,UAAoC,EAAE,aAAa,KAAK,GAAG;EAC5F,KAAKA,OAAO;EACZ,IAAI,QAAQ,aAAa,sBAAsB,YAAY,GAAG;CAChE;CAEA,OAAO,YAAY,KAA6B;EAC9C,IAAI,KACF;;;;;SAMF;CACF;CAEA,KAAY,MAAmD;EAC7D,MAAM,MAAM,KAAKA,KACd,KACC,gGACA,IACF,CAAC,CACA,QAAQ,CAAC,CAAC;EACb,IAAI,CAAC,KAAK,OAAO,KAAA;EACjB,OAAO;GACL,gBAAgB,OAAO,IAAI,eAAe;GAC1C,sBAAsB,OAAO,IAAI,sBAAsB;GACvD,OAAO,IAAI,QAAS,KAAK,MAAM,OAAO,IAAI,KAAK,CAAC,IAAc,KAAA;EAChE;CACF;;CAGA,MACE,MACA,QACA,OACA,cACM;EACN,MAAM,kBAAkB,eAAgB,KAAK,UAAU,KAAK,KAAK,OAAQ;EAGzE,IAAI,mBAAmB,gBAAgB,SAAS,mCAC9C,MAAM,OAAO,OACX,WACE,+BACA,eAAe,KAAK,qCAAqC,gBAAgB,OAAO,mBAAmB,kCAAkC,+GACrI;GAAE;GAAM,OAAO,gBAAgB;GAAQ,UAAU;EAAkC,CACrF,GACA,EAAE,WAAW,MAAM,CACrB;EACF,KAAKA,KAAK,KACR;;;;;wEAMA,MACA,OAAO,gBACP,OAAO,sBACP,eACF;CACF;AACF;;;;;;;;;AC30BA,SAAgB,gBACd,WAMA,QACO;CACP,IAAI,QAAQ,UAAU,SAAS,aAAa;CAC5C,OAAO,SAAS,OAAO,UAAU;EAC/B,MAAM,SAAS,UAAU,SACtB,mBAAmB,MAAM,IAAI,CAAC,EAC7B,UAAU,MAAM,WAAW,CAAC,CAAC;EACjC,IAAI,UAAU,CAAC,OAAO,SAAS;EAC/B,MAAM,QAAQ;GACZ,MAAM,MAAM;GACZ,SAAS,QAAQ,UAAU,OAAO,OAAO,MAAM;GAC/C,QAAQ,MAAM;GACd,QAAQ,QAAQ;GAChB,4BAAW,IAAI,MAAM,QAAQ,KAAK,GAAI,EAAA,CAAE,YAAY;GACpD,MAAM;EACR;EACA,QAAQ,UAAU,OAAO;GAAE;GAAO;EAAM,CAAC,KAAK;CAChD,CAAC;CACD,OAAO;AACT;AAEA,SAAgB,aAAa,OAAO,KAAK;CACvC,MAAM,gBAA+B,CAAC;CACtC,MAAM,eAA8B,CAAC;CACrC,MAAM,yCAAyB,IAAI,IAAyB;CAC5D,MAAM,UAAkC,CAAC;CAGzC,MAAM,SAA4B,CAAC;CACnC,IAAI,cAAc;CAClB,IAAI,QAAQ;CAoDZ,OAAO;EACL,QAAA;GAnDA,QAAQ,OAAO;IACb,OAAO,KAAK,EAAE;IACd,OAAO,QAAQ,QAAQ;GACzB;GACA,SAAS,GAAG,WAA+B;IACzC,MAAM,qBAAqB;IAC3B,MAAM,kBAAkB,OAAO,KAAK,UAAU;KAC5C,IAAI,MAAM,gBAAgB;MACxB,MAAM,gBAAgB,uBAAuB,IAAI,MAAM,cAAc;MACrE,IAAI,eAAe;OACjB,IAAI,oBAAoB,eAAe,KAAK,GAAG,OAAO;OACtD,MAAM,IAAI,MAAM,2BAA2B,MAAM,gBAAgB,cAAc,MAAM,CAAC;MACxF;KACF;KACA,eAAe;KACf,MAAM,iBAA8B;MAClC,GAAG;MACH,QAAQ;MACR,4BAAW,IAAI,KAAK,CAAC,EAAA,CAAE,YAAY;MACnC;KACF;KACA,IAAI,CAAC,MAAM,WAAW;MACpB,cAAc,KAAK,cAAc;MACjC,IAAI,MAAM,gBACR,uBAAuB,IAAI,MAAM,gBAAgB,cAAc;KACnE;KACA,OAAO;IACT,CAAC;IACD,aAAa,KAAK,GAAG,eAAe;IACpC,IAAI,cAAc,oBAAoB;KACpC,MAAM,qBAAqB;MAAE,OAAO;MAAoB,SAAS;KAAY;KAC7E,KAAK,MAAM,UAAU,SACnB,OAAY,kBAAkB,iBAAiB,kBAAkB,CAAC,CAAC,YAAY,CAAC,CAAC;IACrF;IACA,OAAO;GACT;GACA,OAAO,cAAc,GAAG,QAAQ,QAAQ;IACtC,SAAS;IACT,MAAM,OAAO,cAAc,QAAQ,UAAU,MAAM,SAAS,WAAW,CAAC,CAAC,MAAM,GAAG,KAAK;IACvF,OAAO,QAAQ,QAAQ;KACrB,QAAQ;KACR,sBACE,KAAK,WAAW,QAAQ,KAAK,KAAK,SAAS,EAAE,CAAC,SAAS,KAAK,IAAI,aAAa,WAAW;KAE1F,QACE,KAAK,SAAS,SACd,KAAK,KAAK,SAAS,EAAE,CAAC,WAAW,cAAc,cAAc,SAAS,EAAE,CAAC;IAC7E,CAAC;GACH;EAGK;EACL,QAAQ;EACR;EACA;EACA;EACA,IAAI,QAAQ;GACV,OAAO;EACT;CACF;AACF;;;;;AAMA,IAAM,qCAAN,cAAiD,sBAAsB;CACrE,SAAS;CACT,MACE,MACA,QACA,OACA,cACM;EACN,KAAK;EACL,MAAM,MAAM,MAAM,QAAQ,OAAO,YAAY;CAC/C;AACF;AAEA,SAAgB,gBAAoD;CAClE,OAAO,IAAI,mCAAmC,+BAA+B,CAAC,CAAC,GAAG;AACpF;;AAGA,MAAa,UAAU,KAAK,OAAO,IAAI,SAAS,MAAM,WAAW,GAAG,EAAE,CAAC;AAUvE,SAAgB,iCAGd;CACA,MAAM,KAAK,IAAI,aAAa,UAAU;CACtC,OAAO;EACL,KAAK,EACH,KAAgD,OAAe,GAAG,UAAqB;GACrF,MAAM,YAAY,GAAG,QAAQ,KAAK;GAClC,MAAM,QAAQ;GAEd,IAAI,UAAU,QAAQ,CAAC,CAAC,WAAW,GAAG;IACpC,UAAU,IAAI,GAAG,KAAK;IACtB,OAAO,OAAO,OAAO,CAAC,GAAU,EAAE,eAAe,CAAC,EAAS,CAAC;GAC9D;GACA,MAAM,OAAO,UAAU,QAAQ,GAAG,KAAK;GACvC,OAAO,OAAO,OAAO,MAAM,EAAE,eAAe,CAAC,GAAG,IAAI,EAAE,CAAC;EACzD,EACF;EACA,kBAAqB,YAAwB;GAC3C,GAAG,KAAK,OAAO;GACf,IAAI;IACF,MAAM,SAAS,QAAQ;IACvB,GAAG,KAAK,QAAQ;IAChB,OAAO;GACT,SAAS,OAAO;IACd,GAAG,KAAK,UAAU;IAClB,MAAM;GACR;EACF;CACF;AACF"}
|