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
|
@@ -1,2082 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { RpcTarget } from "@iterate-com/capnweb";
|
|
3
|
-
//#region src/processors/rpc-types.ts
|
|
4
|
-
/** Maximum serialized durable-event bytes one Stream read may return. */
|
|
5
|
-
const MAX_STREAM_EVENT_READ_BYTE_LIMIT = 8 * 1024 * 1024;
|
|
6
|
-
/**
|
|
7
|
-
* A durable receiver's declaration that it cannot accept ANY batch right now —
|
|
8
|
-
* part of the delivery contract, not an implementation detail. The subscription's cursor row
|
|
9
|
-
* treats a rejection carrying this name as "the receiver is down/not ready"
|
|
10
|
-
* and backs off or halts even under `onFailingEvent: "skip"`,
|
|
11
|
-
* because failing-event confirmation is a verdict about ONE event and an unavailable
|
|
12
|
-
* receiver fails every event: confirming skips during an outage window steps
|
|
13
|
-
* over healthy events forever (the bootstrap incarnation: the project-worker
|
|
14
|
-
* feed called its receiver before the config repo seeded, and permanently skipped the
|
|
15
|
-
* events that raced the seed).
|
|
16
|
-
*
|
|
17
|
-
* Matched by NAME, not instanceof: the rejection crosses Workers RPC hops
|
|
18
|
-
* (loopback itx roots, DO bindings), which preserve `error.name` but not
|
|
19
|
-
* class identity.
|
|
20
|
-
*/
|
|
21
|
-
var StreamReceiverUnavailableError = class StreamReceiverUnavailableError extends Error {
|
|
22
|
-
static NAME = "StreamReceiverUnavailableError";
|
|
23
|
-
name = StreamReceiverUnavailableError.NAME;
|
|
24
|
-
};
|
|
25
|
-
/** A compare-and-append assertion lost to another committed stream event. */
|
|
26
|
-
var StreamOffsetConflictError = class StreamOffsetConflictError extends Error {
|
|
27
|
-
static NAME = "StreamOffsetConflictError";
|
|
28
|
-
name = StreamOffsetConflictError.NAME;
|
|
29
|
-
};
|
|
30
|
-
/** An operation was bound to a stream lifetime that this path no longer names. */
|
|
31
|
-
var StreamIdMismatchError = class StreamIdMismatchError extends Error {
|
|
32
|
-
static NAME = "StreamIdMismatchError";
|
|
33
|
-
name = StreamIdMismatchError.NAME;
|
|
34
|
-
};
|
|
35
|
-
/** Canonical guarded-append rejection text, including across RPC hops that
|
|
36
|
-
* normalize the custom error name to `Error`. */
|
|
37
|
-
function streamIdMismatchMessage(expectedStreamId, actualStreamId) {
|
|
38
|
-
return `stream ID changed (${expectedStreamId} -> ${String(actualStreamId)}); append rejected`;
|
|
39
|
-
}
|
|
40
|
-
const STREAM_ID_MISMATCH_MESSAGE = /^stream ID changed \(.+ -> .+\); append rejected$/;
|
|
41
|
-
/**
|
|
42
|
-
* Match a guarded append rejected because its source stream was recreated.
|
|
43
|
-
* Durable Object RPC preserves the custom name; CapnWeb can reduce it to a
|
|
44
|
-
* plain Error, so the exact canonical message remains a narrow fallback.
|
|
45
|
-
*/
|
|
46
|
-
function isStreamIdMismatchError(error) {
|
|
47
|
-
const candidate = error;
|
|
48
|
-
return candidate?.name === StreamIdMismatchError.NAME || candidate?.name === "Error" && typeof candidate.message === "string" && STREAM_ID_MISMATCH_MESSAGE.test(candidate.message);
|
|
49
|
-
}
|
|
50
|
-
/** Canonical compare-and-append conflict text, including across RPC hops that
|
|
51
|
-
* normalize the custom error name to `Error`. */
|
|
52
|
-
function streamOffsetConflictMessage(expectedOffset, actualOffset) {
|
|
53
|
-
return `expected next offset ${expectedOffset}, found ${actualOffset}`;
|
|
54
|
-
}
|
|
55
|
-
const STREAM_OFFSET_CONFLICT_MESSAGE = /^expected next offset \d+, found \d+$/;
|
|
56
|
-
/**
|
|
57
|
-
* Match by name because Durable Object RPC preserves names, not prototypes.
|
|
58
|
-
* CapnWeb's public itx boundary currently normalizes custom error names to
|
|
59
|
-
* `Error`, so retain an exact message fallback for that hop. Keep this
|
|
60
|
-
* deliberately narrow: callers use the result to retry a compare-and-append.
|
|
61
|
-
*/
|
|
62
|
-
function isStreamOffsetConflictError(error) {
|
|
63
|
-
const candidate = error;
|
|
64
|
-
return candidate?.name === StreamOffsetConflictError.NAME || candidate?.name === "Error" && typeof candidate.message === "string" && STREAM_OFFSET_CONFLICT_MESSAGE.test(candidate.message);
|
|
65
|
-
}
|
|
66
|
-
function isStreamReceiverUnavailableError(error) {
|
|
67
|
-
return error?.name === StreamReceiverUnavailableError.NAME;
|
|
68
|
-
}
|
|
69
|
-
//#endregion
|
|
70
|
-
//#region src/processors/stream-handle.ts
|
|
71
|
-
/**
|
|
72
|
-
* Resolve the path accepted by `stream.at(path)`. Absolute paths start at the
|
|
73
|
-
* stream root; relative paths start at `basePath`; `..` may climb only as far
|
|
74
|
-
* as the root. Keeping this next to {@link ProcessorStream} lets processor
|
|
75
|
-
* appends decide whether a resolved destination is their own stream without
|
|
76
|
-
* relying on RPC-target object identity.
|
|
77
|
-
*/
|
|
78
|
-
function resolveStreamPath(basePath, streamPath) {
|
|
79
|
-
const segments = streamPath.startsWith("/") ? [] : basePath.split("/").filter(Boolean);
|
|
80
|
-
for (const segment of streamPath.split("/")) {
|
|
81
|
-
if (segment === "" || segment === ".") continue;
|
|
82
|
-
if (segment === "..") {
|
|
83
|
-
if (segments.length === 0) throw new Error(`stream path "${streamPath}" escapes the stream root (resolved from "${basePath}")`);
|
|
84
|
-
segments.pop();
|
|
85
|
-
continue;
|
|
86
|
-
}
|
|
87
|
-
segments.push(segment);
|
|
88
|
-
}
|
|
89
|
-
return segments.length === 0 ? "/" : `/${segments.join("/")}`;
|
|
90
|
-
}
|
|
91
|
-
//#endregion
|
|
92
|
-
//#region src/processors/stream-runtime-metrics.ts
|
|
93
|
-
/**
|
|
94
|
-
* Fixed-capacity ring of latency samples. `stats()` is `null` until the first
|
|
95
|
-
* sample — surfaces render "—" instead of a made-up number.
|
|
96
|
-
*/
|
|
97
|
-
var LatencyRing = class {
|
|
98
|
-
#capacity;
|
|
99
|
-
#samples = [];
|
|
100
|
-
#next = 0;
|
|
101
|
-
#last = 0;
|
|
102
|
-
#lastAt = 0;
|
|
103
|
-
constructor(capacity = 32) {
|
|
104
|
-
if (!Number.isInteger(capacity) || capacity <= 0) throw new Error("LatencyRing capacity must be a positive integer");
|
|
105
|
-
this.#capacity = capacity;
|
|
106
|
-
}
|
|
107
|
-
record(ms, atMs) {
|
|
108
|
-
if (!Number.isFinite(ms)) return;
|
|
109
|
-
const sample = Math.max(0, Math.round(ms));
|
|
110
|
-
if (this.#samples.length < this.#capacity) this.#samples.push(sample);
|
|
111
|
-
else this.#samples[this.#next] = sample;
|
|
112
|
-
this.#next = (this.#next + 1) % this.#capacity;
|
|
113
|
-
this.#last = sample;
|
|
114
|
-
this.#lastAt = atMs;
|
|
115
|
-
}
|
|
116
|
-
stats() {
|
|
117
|
-
if (this.#samples.length === 0) return null;
|
|
118
|
-
const sorted = [...this.#samples].sort((a, b) => a - b);
|
|
119
|
-
const rank = (q) => sorted[Math.max(0, Math.ceil(q * sorted.length) - 1)];
|
|
120
|
-
return {
|
|
121
|
-
last: this.#last,
|
|
122
|
-
p50: rank(.5),
|
|
123
|
-
p95: rank(.95),
|
|
124
|
-
samples: sorted.length,
|
|
125
|
-
lastAt: this.#lastAt
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
};
|
|
129
|
-
/**
|
|
130
|
-
* 60 one-second buckets of {count, bytes}. Stale slots (lapped by the ring)
|
|
131
|
-
* are ignored at read time, so a burst followed by silence decays to zero
|
|
132
|
-
* without a sweeper.
|
|
133
|
-
*/
|
|
134
|
-
var MinuteBuckets = class {
|
|
135
|
-
#seconds = new Array(60).fill(-1);
|
|
136
|
-
#counts = new Array(60).fill(0);
|
|
137
|
-
#bytes = new Array(60).fill(0);
|
|
138
|
-
bump(atMs, count, bytes) {
|
|
139
|
-
const second = Math.floor(atMs / 1e3);
|
|
140
|
-
const slot = (second % 60 + 60) % 60;
|
|
141
|
-
if (this.#seconds[slot] !== second) {
|
|
142
|
-
this.#seconds[slot] = second;
|
|
143
|
-
this.#counts[slot] = 0;
|
|
144
|
-
this.#bytes[slot] = 0;
|
|
145
|
-
}
|
|
146
|
-
this.#counts[slot] += count;
|
|
147
|
-
this.#bytes[slot] += bytes;
|
|
148
|
-
}
|
|
149
|
-
lastMinute(nowMs) {
|
|
150
|
-
const { count, bytes } = this.window(nowMs, 60);
|
|
151
|
-
return {
|
|
152
|
-
count,
|
|
153
|
-
bytes,
|
|
154
|
-
perSecond: count / 60
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
/** Totals over the trailing `seconds` (≤60) — short windows make responsive rates. */
|
|
158
|
-
window(nowMs, seconds) {
|
|
159
|
-
const nowSecond = Math.floor(nowMs / 1e3);
|
|
160
|
-
let count = 0;
|
|
161
|
-
let bytes = 0;
|
|
162
|
-
for (let slot = 0; slot < 60; slot += 1) {
|
|
163
|
-
const second = this.#seconds[slot];
|
|
164
|
-
if (second < 0 || second > nowSecond || second <= nowSecond - seconds) continue;
|
|
165
|
-
count += this.#counts[slot];
|
|
166
|
-
bytes += this.#bytes[slot];
|
|
167
|
-
}
|
|
168
|
-
return {
|
|
169
|
-
count,
|
|
170
|
-
bytes
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
/**
|
|
174
|
-
* The raw per-second buckets, oldest→newest, always exactly 60 entries
|
|
175
|
-
* (silent seconds are zero) — what a UI graphs directly, so the graph is
|
|
176
|
-
* the measurement rather than a client-side reconstruction of it.
|
|
177
|
-
*/
|
|
178
|
-
series(nowMs) {
|
|
179
|
-
const nowSecond = Math.floor(nowMs / 1e3);
|
|
180
|
-
const counts = new Array(60).fill(0);
|
|
181
|
-
const bytes = new Array(60).fill(0);
|
|
182
|
-
for (let slot = 0; slot < 60; slot += 1) {
|
|
183
|
-
const second = this.#seconds[slot];
|
|
184
|
-
if (second < 0 || second > nowSecond || second <= nowSecond - 60) continue;
|
|
185
|
-
const index = 59 - (nowSecond - second);
|
|
186
|
-
counts[index] = this.#counts[slot];
|
|
187
|
-
bytes[index] = this.#bytes[slot];
|
|
188
|
-
}
|
|
189
|
-
return {
|
|
190
|
-
counts,
|
|
191
|
-
bytes
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
};
|
|
195
|
-
/** The stream Durable Object's in-memory throughput accounting. */
|
|
196
|
-
var StreamRuntimeMetrics = class {
|
|
197
|
-
#measuredSinceMs;
|
|
198
|
-
ingress = new MinuteBuckets();
|
|
199
|
-
egress = new MinuteBuckets();
|
|
200
|
-
constructor(nowMs) {
|
|
201
|
-
this.#measuredSinceMs = nowMs;
|
|
202
|
-
}
|
|
203
|
-
report(nowMs) {
|
|
204
|
-
const direction = (buckets) => {
|
|
205
|
-
const trailing5s = buckets.window(nowMs, 5);
|
|
206
|
-
return {
|
|
207
|
-
perSecond5s: trailing5s.count / 5,
|
|
208
|
-
bytesPerSecond5s: trailing5s.bytes / 5,
|
|
209
|
-
lastMinute: buckets.lastMinute(nowMs),
|
|
210
|
-
series: buckets.series(nowMs)
|
|
211
|
-
};
|
|
212
|
-
};
|
|
213
|
-
return {
|
|
214
|
-
measuredSince: new Date(this.#measuredSinceMs).toISOString(),
|
|
215
|
-
reportedAt: new Date(nowMs).toISOString(),
|
|
216
|
-
ingress: direction(this.ingress),
|
|
217
|
-
egress: direction(this.egress)
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
};
|
|
221
|
-
/**
|
|
222
|
-
* Age an event-driven throughput snapshot against the local wall clock. This
|
|
223
|
-
* keeps trailing windows truthful during silence without polling the stream.
|
|
224
|
-
*/
|
|
225
|
-
function ageStreamThroughputMetrics(metrics, nowMs) {
|
|
226
|
-
const reportedAtMs = Date.parse(metrics.reportedAt);
|
|
227
|
-
if (!Number.isFinite(reportedAtMs)) return metrics;
|
|
228
|
-
const elapsedSeconds = Math.max(0, Math.floor(nowMs / 1e3) - Math.floor(reportedAtMs / 1e3));
|
|
229
|
-
if (elapsedSeconds === 0) return metrics;
|
|
230
|
-
const age = (report) => {
|
|
231
|
-
const shift = (values) => {
|
|
232
|
-
const seconds = Math.min(values.length, elapsedSeconds);
|
|
233
|
-
return [...values.slice(seconds), ...new Array(seconds).fill(0)];
|
|
234
|
-
};
|
|
235
|
-
const counts = shift(report.series.counts);
|
|
236
|
-
const bytes = shift(report.series.bytes);
|
|
237
|
-
const sum = (values) => values.reduce((total, value) => total + value, 0);
|
|
238
|
-
const count = sum(counts);
|
|
239
|
-
const byteCount = sum(bytes);
|
|
240
|
-
return {
|
|
241
|
-
perSecond5s: sum(counts.slice(-5)) / 5,
|
|
242
|
-
bytesPerSecond5s: sum(bytes.slice(-5)) / 5,
|
|
243
|
-
lastMinute: {
|
|
244
|
-
count,
|
|
245
|
-
bytes: byteCount,
|
|
246
|
-
perSecond: count / 60
|
|
247
|
-
},
|
|
248
|
-
series: {
|
|
249
|
-
counts,
|
|
250
|
-
bytes
|
|
251
|
-
}
|
|
252
|
-
};
|
|
253
|
-
};
|
|
254
|
-
return {
|
|
255
|
-
...metrics,
|
|
256
|
-
ingress: age(metrics.ingress),
|
|
257
|
-
egress: age(metrics.egress)
|
|
258
|
-
};
|
|
259
|
-
}
|
|
260
|
-
/**
|
|
261
|
-
* The mutual ping's NTP-style math, shared by both requesters (a stream
|
|
262
|
-
* pinging a callback owner; anything pinging the stream). The requester stamps
|
|
263
|
-
* `t0` and observes `t3`; the responder reports receive/reply-send times on
|
|
264
|
-
* ITS clock. RTT excludes responder processing time; `clockOffsetMs`
|
|
265
|
-
* estimates `responderClock - requesterClock`.
|
|
266
|
-
*/
|
|
267
|
-
function pingRoundTrip(reply, t3) {
|
|
268
|
-
return {
|
|
269
|
-
rttMs: Math.max(0, t3 - reply.t0 - (reply.t2 - reply.t1)),
|
|
270
|
-
clockOffsetMs: (reply.t1 - reply.t0 + (reply.t2 - t3)) / 2
|
|
271
|
-
};
|
|
272
|
-
}
|
|
273
|
-
//#endregion
|
|
274
|
-
//#region src/processors/event-consumption-metrics.ts
|
|
275
|
-
/** In-flight own-append correlation entries beyond this are dropped oldest-first. */
|
|
276
|
-
const MAX_PENDING_OWN_APPENDS = 16;
|
|
277
|
-
var EventConsumptionMetrics = class {
|
|
278
|
-
#measuredSinceMs;
|
|
279
|
-
#consumeOwnAppend = new LatencyRing();
|
|
280
|
-
#appendRoundTrip = new LatencyRing();
|
|
281
|
-
#deliveryAge = new LatencyRing();
|
|
282
|
-
#ingest = new LatencyRing();
|
|
283
|
-
#batchesIngested = 0;
|
|
284
|
-
#eventsIngested = 0;
|
|
285
|
-
#clockOffsetMs = null;
|
|
286
|
-
/** Highest offset this host has ingested through (see noteBatchIngested). */
|
|
287
|
-
#ingestedThroughOffset = 0;
|
|
288
|
-
/** Own appends awaiting their loop-back delivery: committed offset + call-start time. */
|
|
289
|
-
#pendingOwnAppends = [];
|
|
290
|
-
constructor(nowMs) {
|
|
291
|
-
this.#measuredSinceMs = nowMs;
|
|
292
|
-
}
|
|
293
|
-
/**
|
|
294
|
-
* An `append()` this host issued resolved: `t0` is when the host called it,
|
|
295
|
-
* `atMs` is when the commit came back, and `maxCommittedOffset` is the
|
|
296
|
-
* highest committed offset that CAN come back to this host — the caller's
|
|
297
|
-
* judgement, because only the caller knows what it consumes. `null` means
|
|
298
|
-
* the append carried nothing this host will ever be delivered, and is timed
|
|
299
|
-
* for its round trip alone.
|
|
300
|
-
*/
|
|
301
|
-
noteAppendCommitted(args) {
|
|
302
|
-
this.#appendRoundTrip.record(args.atMs - args.t0, args.atMs);
|
|
303
|
-
if (args.maxCommittedOffset === null) return;
|
|
304
|
-
if (args.maxCommittedOffset <= this.#ingestedThroughOffset) {
|
|
305
|
-
this.#consumeOwnAppend.record(args.atMs - args.t0, args.atMs);
|
|
306
|
-
return;
|
|
307
|
-
}
|
|
308
|
-
this.#pendingOwnAppends.push({
|
|
309
|
-
offset: args.maxCommittedOffset,
|
|
310
|
-
t0: args.t0
|
|
311
|
-
});
|
|
312
|
-
if (this.#pendingOwnAppends.length > MAX_PENDING_OWN_APPENDS) this.#pendingOwnAppends.splice(0, this.#pendingOwnAppends.length - MAX_PENDING_OWN_APPENDS);
|
|
313
|
-
}
|
|
314
|
-
/** One delivered batch fully ingested (fold applied / SQLite write done). */
|
|
315
|
-
noteBatchIngested(args) {
|
|
316
|
-
this.#batchesIngested += 1;
|
|
317
|
-
this.#eventsIngested += args.ingestedOffsets.length;
|
|
318
|
-
this.#ingestedThroughOffset = Math.max(this.#ingestedThroughOffset, args.ingestedThroughOffset);
|
|
319
|
-
this.#ingest.record(args.atMs - args.ingestStartedAtMs, args.atMs);
|
|
320
|
-
if (args.newestEventCreatedAtMs !== void 0 && Number.isFinite(args.newestEventCreatedAtMs)) {
|
|
321
|
-
const hostNowOnStreamClock = args.atMs - (this.#clockOffsetMs ?? 0);
|
|
322
|
-
this.#deliveryAge.record(hostNowOnStreamClock - args.newestEventCreatedAtMs, args.atMs);
|
|
323
|
-
}
|
|
324
|
-
if (this.#pendingOwnAppends.length > 0) {
|
|
325
|
-
const stillPending = [];
|
|
326
|
-
for (const pending of this.#pendingOwnAppends) {
|
|
327
|
-
if (pending.offset > args.ingestedThroughOffset) {
|
|
328
|
-
stillPending.push(pending);
|
|
329
|
-
continue;
|
|
330
|
-
}
|
|
331
|
-
if (args.ingestedOffsets.includes(pending.offset)) this.#consumeOwnAppend.record(args.atMs - pending.t0, args.atMs);
|
|
332
|
-
}
|
|
333
|
-
this.#pendingOwnAppends = stillPending;
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
/**
|
|
337
|
-
* The stream pinged this host: `t0` is the stream's send time (stream
|
|
338
|
-
* clock), `t1` the host's receive time (host clock). With a one-way-delay
|
|
339
|
-
* estimate (half the host's measured transport RTT, when it has one) this
|
|
340
|
-
* yields the host−stream clock offset used to correct delivery ages.
|
|
341
|
-
*/
|
|
342
|
-
notePingObserved(args) {
|
|
343
|
-
this.#clockOffsetMs = args.t1 - args.t0 - (args.oneWayEstimateMs ?? 0);
|
|
344
|
-
}
|
|
345
|
-
/** The host's event connection reopened: in-flight own-append correlations are void. */
|
|
346
|
-
clearPendingAppends() {
|
|
347
|
-
this.#pendingOwnAppends = [];
|
|
348
|
-
}
|
|
349
|
-
report() {
|
|
350
|
-
return {
|
|
351
|
-
measuredSince: new Date(this.#measuredSinceMs).toISOString(),
|
|
352
|
-
consumeOwnAppendMs: this.#consumeOwnAppend.stats(),
|
|
353
|
-
appendRoundTripMs: this.#appendRoundTrip.stats(),
|
|
354
|
-
deliveryAgeMs: this.#deliveryAge.stats(),
|
|
355
|
-
ingestMs: this.#ingest.stats(),
|
|
356
|
-
batchesIngested: this.#batchesIngested,
|
|
357
|
-
eventsIngested: this.#eventsIngested,
|
|
358
|
-
clockOffsetMs: this.#clockOffsetMs
|
|
359
|
-
};
|
|
360
|
-
}
|
|
361
|
-
};
|
|
362
|
-
//#endregion
|
|
363
|
-
//#region src/processors/schemas.ts
|
|
364
|
-
/**
|
|
365
|
-
* Maximum number of stream-to-stream copies retained in one event's provenance.
|
|
366
|
-
* Cycles normally stop a chain earlier; this bounds acyclic graphs and the
|
|
367
|
-
* serialized event growth they can produce.
|
|
368
|
-
*/
|
|
369
|
-
const MAX_COPIED_FROM_HOPS = 32;
|
|
370
|
-
/** Append input before the stream assigns offset and timestamp. */
|
|
371
|
-
const StreamEventInput = z.strictObject({
|
|
372
|
-
type: z.string(),
|
|
373
|
-
payload: z.record(z.string(), z.unknown()).optional(),
|
|
374
|
-
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
375
|
-
source: z.strictObject({
|
|
376
|
-
processor: z.strictObject({
|
|
377
|
-
slug: z.string(),
|
|
378
|
-
version: z.string(),
|
|
379
|
-
stream: z.strictObject({
|
|
380
|
-
path: z.string().trim().min(1),
|
|
381
|
-
projectId: z.string().trim().min(1).nullable(),
|
|
382
|
-
/** Exact lifetime of the processor's home stream. */
|
|
383
|
-
streamId: z.uuid()
|
|
384
|
-
}),
|
|
385
|
-
whileProcessing: z.strictObject({
|
|
386
|
-
offset: z.number().int().nonnegative(),
|
|
387
|
-
type: z.string().trim().min(1)
|
|
388
|
-
}).optional()
|
|
389
|
-
}).optional(),
|
|
390
|
-
copiedFrom: z.array(z.strictObject({
|
|
391
|
-
/** Name of the source stream's subscription that copied this event. */
|
|
392
|
-
name: z.string().trim().min(1),
|
|
393
|
-
/** Random identity assigned when that source stream's storage was created. */
|
|
394
|
-
streamId: z.uuid(),
|
|
395
|
-
/** Creation time of that source stream, used to order destructive recreations. */
|
|
396
|
-
streamCreatedAt: z.string().trim().min(1),
|
|
397
|
-
/** Configure or cursor-set event that started this delivered copy. */
|
|
398
|
-
cursorChangedAtSourceOffset: z.number().int().positive(),
|
|
399
|
-
createdAt: z.string(),
|
|
400
|
-
offset: z.number().int().nonnegative(),
|
|
401
|
-
path: z.string().trim().min(1),
|
|
402
|
-
projectId: z.string().trim().min(1).nullable(),
|
|
403
|
-
type: z.string().trim().min(1)
|
|
404
|
-
})).min(1).max(32).optional()
|
|
405
|
-
}).optional(),
|
|
406
|
-
idempotencyKey: z.string().trim().min(1).optional(),
|
|
407
|
-
/**
|
|
408
|
-
* Ephemeral events receive real stream offsets but their bodies are NEVER
|
|
409
|
-
* written to the Stream Durable Object's SQLite. The current Durable Object
|
|
410
|
-
* incarnation keeps up to 10 MiB of serialized ephemeral events in memory,
|
|
411
|
-
* evicting the oldest first. A restart forgets all of them.
|
|
412
|
-
*
|
|
413
|
-
* Range reads exclude ephemeral events unless `includeEphemeral: true`;
|
|
414
|
-
* point reads by offset return one only while it remains in memory. Session
|
|
415
|
-
* connections replay currently buffered ephemeral events after their replay
|
|
416
|
-
* cursor and receive new ones live. Durable subscriptions never deliver
|
|
417
|
-
* them. An ephemeral event cannot have an idempotency key.
|
|
418
|
-
*
|
|
419
|
-
* Nothing durable may depend on an ephemeral event. Use it for streaming
|
|
420
|
-
* signals whose durable truth lands separately — for example, LLM response
|
|
421
|
-
* chunks followed by a durable assistant context item.
|
|
422
|
-
* `z.literal(true)`, not boolean: absent = durable, so committed rows stay
|
|
423
|
-
* self-describing and `ephemeral: false` is a loud input error, not a
|
|
424
|
-
* silent synonym for omitting the flag.
|
|
425
|
-
*/
|
|
426
|
-
ephemeral: z.literal(true).optional()
|
|
427
|
-
}).superRefine((event, context) => {
|
|
428
|
-
if (event.ephemeral === true && event.idempotencyKey !== void 0) context.addIssue({
|
|
429
|
-
code: "custom",
|
|
430
|
-
message: "ephemeral events cannot have an idempotencyKey",
|
|
431
|
-
path: ["idempotencyKey"]
|
|
432
|
-
});
|
|
433
|
-
});
|
|
434
|
-
/** Offset-assigned stream event after commit. */
|
|
435
|
-
const StreamEvent = StreamEventInput.safeExtend({
|
|
436
|
-
offset: z.number().int().nonnegative(),
|
|
437
|
-
createdAt: z.string(),
|
|
438
|
-
path: z.string().trim().min(1)
|
|
439
|
-
});
|
|
440
|
-
/**
|
|
441
|
-
* One known stream in a project's reduced state — what the project processor
|
|
442
|
-
* records per agent/repo/secret/stream and what the collection `list()`
|
|
443
|
-
* methods return.
|
|
444
|
-
*/
|
|
445
|
-
const StreamListItem = z.object({
|
|
446
|
-
createdAt: z.string(),
|
|
447
|
-
path: z.string()
|
|
448
|
-
});
|
|
449
|
-
//#endregion
|
|
450
|
-
//#region src/processors/processor-contracts.ts
|
|
451
|
-
/**
|
|
452
|
-
* Merge one processor configuration patch into its current configuration.
|
|
453
|
-
*
|
|
454
|
-
* Configuration patches recurse only through plain JSON objects. Arrays,
|
|
455
|
-
* scalars, and `null` replace the previous value wholesale; omitted keys are
|
|
456
|
-
* retained. Processors validate the merged result with their own complete
|
|
457
|
-
* configuration schema before storing it in reduced state.
|
|
458
|
-
*/
|
|
459
|
-
function mergeProcessorConfig(base, patch) {
|
|
460
|
-
if (!isPlainObject(base) || !isPlainObject(patch)) return patch;
|
|
461
|
-
const merged = { ...base };
|
|
462
|
-
for (const [key, patchValue] of Object.entries(patch)) {
|
|
463
|
-
const baseValue = merged[key];
|
|
464
|
-
merged[key] = isPlainObject(baseValue) && isPlainObject(patchValue) ? mergeProcessorConfig(baseValue, patchValue) : patchValue;
|
|
465
|
-
}
|
|
466
|
-
return merged;
|
|
467
|
-
}
|
|
468
|
-
function isPlainObject(value) {
|
|
469
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
470
|
-
const prototype = Object.getPrototypeOf(value);
|
|
471
|
-
return prototype === Object.prototype || prototype === null;
|
|
472
|
-
}
|
|
473
|
-
/**
|
|
474
|
-
* Rebuild the concrete Zod envelope for one committed event from its catalog
|
|
475
|
-
* key plus `payloadSchema`. Contracts author event definitions as plain
|
|
476
|
-
* `{ description, payloadSchema }` values keyed by the event type string, so
|
|
477
|
-
* replay and live delivery share one validation path.
|
|
478
|
-
*/
|
|
479
|
-
function getEventSchema(args) {
|
|
480
|
-
return z.looseObject({
|
|
481
|
-
type: z.literal(args.type),
|
|
482
|
-
payload: args.payloadSchema,
|
|
483
|
-
metadata: StreamEvent.shape.metadata,
|
|
484
|
-
source: StreamEvent.shape.source,
|
|
485
|
-
idempotencyKey: StreamEvent.shape.idempotencyKey,
|
|
486
|
-
ephemeral: StreamEvent.shape.ephemeral,
|
|
487
|
-
offset: StreamEvent.shape.offset,
|
|
488
|
-
createdAt: StreamEvent.shape.createdAt,
|
|
489
|
-
path: StreamEvent.shape.path
|
|
490
|
-
}).superRefine(rejectEphemeralIdempotency);
|
|
491
|
-
}
|
|
492
|
-
/** The envelope `ephemeral` slot: for a definition marked `ephemeral: true`,
|
|
493
|
-
* absent defaults to `true` and an explicit `false` FAILS the parse — the
|
|
494
|
-
* contract, not the append site, decides that the event never becomes a
|
|
495
|
-
* durable stream fact. */
|
|
496
|
-
function ephemeralEnvelopeSchema(forced, standard) {
|
|
497
|
-
return forced === true ? z.literal(true).default(true) : standard;
|
|
498
|
-
}
|
|
499
|
-
function rejectEphemeralIdempotency(event, context) {
|
|
500
|
-
if (event.ephemeral === true && event.idempotencyKey !== void 0) context.addIssue({
|
|
501
|
-
code: "custom",
|
|
502
|
-
message: "ephemeral events cannot have an idempotencyKey",
|
|
503
|
-
path: ["idempotencyKey"]
|
|
504
|
-
});
|
|
505
|
-
}
|
|
506
|
-
/**
|
|
507
|
-
* `getEventSchema` without offset/createdAt (and strict, so an accidental
|
|
508
|
-
* `offset` key on an append input fails loudly). Gives pre-append policy code
|
|
509
|
-
* the same payload validation as reducers, without fabricating a committed
|
|
510
|
-
* event just to get at the typed payload.
|
|
511
|
-
*/
|
|
512
|
-
function getEventInputSchema(args) {
|
|
513
|
-
return z.strictObject({
|
|
514
|
-
type: z.literal(args.type),
|
|
515
|
-
payload: args.payloadSchema,
|
|
516
|
-
metadata: StreamEventInput.shape.metadata,
|
|
517
|
-
source: StreamEventInput.shape.source,
|
|
518
|
-
idempotencyKey: StreamEventInput.shape.idempotencyKey,
|
|
519
|
-
ephemeral: ephemeralEnvelopeSchema(args.ephemeral, StreamEventInput.shape.ephemeral)
|
|
520
|
-
}).superRefine(rejectEphemeralIdempotency);
|
|
521
|
-
}
|
|
522
|
-
/**
|
|
523
|
-
* Memoized twins of {@link getEventSchema} / {@link getEventInputSchema} for
|
|
524
|
-
* hot paths. Constructing the zod wrapper per call costs ~20µs (~50x the
|
|
525
|
-
* parse itself), and the reduce/append paths run once per event. Keyed by
|
|
526
|
-
* payload-schema identity, then event type: catalog entries are module
|
|
527
|
-
* constants, so the WeakMap never grows past the contract surface.
|
|
528
|
-
*/
|
|
529
|
-
const eventSchemaCache = /* @__PURE__ */ new WeakMap();
|
|
530
|
-
function cachedSchema(cache, build, args) {
|
|
531
|
-
let byType = cache.get(args.payloadSchema);
|
|
532
|
-
if (byType === void 0) {
|
|
533
|
-
byType = /* @__PURE__ */ new Map();
|
|
534
|
-
cache.set(args.payloadSchema, byType);
|
|
535
|
-
}
|
|
536
|
-
let schema = byType.get(args.type);
|
|
537
|
-
if (schema === void 0) {
|
|
538
|
-
schema = build(args);
|
|
539
|
-
byType.set(args.type, schema);
|
|
540
|
-
}
|
|
541
|
-
return schema;
|
|
542
|
-
}
|
|
543
|
-
/** Memoized {@link getEventSchema} (see {@link eventSchemaCache}). */
|
|
544
|
-
function cachedEventSchema(args) {
|
|
545
|
-
return cachedSchema(eventSchemaCache, getEventSchema, args);
|
|
546
|
-
}
|
|
547
|
-
/**
|
|
548
|
-
* Validate an append input with the payload schema resolved from a processor
|
|
549
|
-
* contract. Prefer the contract-bound `contract.buildEvent(event)` API, which
|
|
550
|
-
* carries the same types without repeating the contract in the argument.
|
|
551
|
-
*
|
|
552
|
-
* @deprecated Use `contract.buildEvent(event)`.
|
|
553
|
-
*/
|
|
554
|
-
function buildEvent(args) {
|
|
555
|
-
return parseResolvedEventInput(args.contract, args.event);
|
|
556
|
-
}
|
|
557
|
-
function parseResolvedEventInput(contract, event) {
|
|
558
|
-
const eventDefinition = getResolvedEventDefinition({
|
|
559
|
-
contract,
|
|
560
|
-
eventType: event.type
|
|
561
|
-
});
|
|
562
|
-
if (eventDefinition === void 0) {
|
|
563
|
-
const owner = contract.slug == null ? "contract" : `processor "${contract.slug}"`;
|
|
564
|
-
throw new Error(`${owner} cannot build unresolved event "${event.type}".`);
|
|
565
|
-
}
|
|
566
|
-
return getEventInputSchema({
|
|
567
|
-
type: event.type,
|
|
568
|
-
payloadSchema: eventDefinition.payloadSchema,
|
|
569
|
-
ephemeral: eventDefinition.ephemeral
|
|
570
|
-
}).parse(event);
|
|
571
|
-
}
|
|
572
|
-
function defineProcessorContract(contract) {
|
|
573
|
-
assertNoLocalProcessorDepEventConflicts(contract);
|
|
574
|
-
assertDefaultStateSchema(contract);
|
|
575
|
-
if (typeof contract !== "object" || contract === null) throw new Error("Processor contract must be an object.");
|
|
576
|
-
for (const method of [
|
|
577
|
-
"buildEvent",
|
|
578
|
-
"parseEvent",
|
|
579
|
-
"parseEventInput",
|
|
580
|
-
"parseConsumedInput"
|
|
581
|
-
]) if (method in contract) throw new Error(`Processor "${getProcessorSlug(contract)}" must not define ${method}.`);
|
|
582
|
-
const typedContract = contract;
|
|
583
|
-
return Object.assign(typedContract, {
|
|
584
|
-
buildEvent(event) {
|
|
585
|
-
return parseResolvedEventInput(typedContract, event);
|
|
586
|
-
},
|
|
587
|
-
parseEvent: makeContractEventParser(typedContract, getEventSchema),
|
|
588
|
-
parseEventInput: makeContractEventParser(typedContract, getEventInputSchema),
|
|
589
|
-
parseConsumedInput: makeContractConsumedInputParser(typedContract)
|
|
590
|
-
});
|
|
591
|
-
}
|
|
592
|
-
/**
|
|
593
|
-
* Runtime twin of {@link ConsumedInput}. Unlike `parseEventInput`, this parser
|
|
594
|
-
* rejects a resolved event that the processor does not actually consume. A
|
|
595
|
-
* domain object's typed `append` door uses both so its remote runtime boundary
|
|
596
|
-
* cannot drift from the processor contract after TypeScript has been erased.
|
|
597
|
-
*/
|
|
598
|
-
function makeContractConsumedInputParser(contract) {
|
|
599
|
-
const parserCache = /* @__PURE__ */ new Map();
|
|
600
|
-
return (event) => {
|
|
601
|
-
if (event.ephemeral === true) throw new Error(`Processor "${getProcessorSlug(contract)}" cannot consume ephemeral event "${event.type}".`);
|
|
602
|
-
const eventDefinition = getConsumedEventDefinition({
|
|
603
|
-
contract,
|
|
604
|
-
eventType: event.type
|
|
605
|
-
});
|
|
606
|
-
if (eventDefinition === void 0) throw new Error(`Processor "${getProcessorSlug(contract)}" does not consume event "${event.type}".`);
|
|
607
|
-
let schema = parserCache.get(event.type);
|
|
608
|
-
if (schema === void 0) {
|
|
609
|
-
schema = getEventInputSchema({
|
|
610
|
-
type: event.type,
|
|
611
|
-
payloadSchema: eventDefinition.payloadSchema,
|
|
612
|
-
ephemeral: eventDefinition.ephemeral
|
|
613
|
-
});
|
|
614
|
-
parserCache.set(event.type, schema);
|
|
615
|
-
}
|
|
616
|
-
return schema.parse(event);
|
|
617
|
-
};
|
|
618
|
-
}
|
|
619
|
-
/**
|
|
620
|
-
* Shared runtime body of `contract.parseEvent(...)` and
|
|
621
|
-
* `contract.parseEventInput(...)`: both resolve the payload schema from the
|
|
622
|
-
* contract catalog, so an edit to a core event schema automatically affects
|
|
623
|
-
* committed-event reduction and pre-commit validation together.
|
|
624
|
-
*/
|
|
625
|
-
function makeContractEventParser(contract, schemaFor) {
|
|
626
|
-
const parserCache = /* @__PURE__ */ new Map();
|
|
627
|
-
return (event) => {
|
|
628
|
-
const eventType = event.type;
|
|
629
|
-
const eventDefinition = getResolvedEventDefinition({
|
|
630
|
-
contract,
|
|
631
|
-
eventType
|
|
632
|
-
});
|
|
633
|
-
if (eventDefinition == null) throw new Error(`Processor "${getProcessorSlug(contract)}" cannot parse unresolved event "${eventType}".`);
|
|
634
|
-
let schema = parserCache.get(eventType);
|
|
635
|
-
if (schema === void 0) {
|
|
636
|
-
schema = schemaFor({
|
|
637
|
-
type: eventType,
|
|
638
|
-
payloadSchema: eventDefinition.payloadSchema,
|
|
639
|
-
ephemeral: eventDefinition.ephemeral
|
|
640
|
-
});
|
|
641
|
-
parserCache.set(eventType, schema);
|
|
642
|
-
}
|
|
643
|
-
return schema.parse(event);
|
|
644
|
-
};
|
|
645
|
-
}
|
|
646
|
-
/**
|
|
647
|
-
* Enforces the invariant that reduced processor state is object-shaped (so
|
|
648
|
-
* state slices can evolve safely and hooks never branch on primitive state).
|
|
649
|
-
*/
|
|
650
|
-
function assertObjectProcessorState(args) {
|
|
651
|
-
if (typeof args.value === "object" && args.value !== null && !Array.isArray(args.value)) return;
|
|
652
|
-
throw new Error(`Processor "${args.processorSlug}" state must be an object.`);
|
|
653
|
-
}
|
|
654
|
-
function assertDefaultStateSchema(contract) {
|
|
655
|
-
if (typeof contract !== "object" || contract === null) throw new Error("Processor contract must be an object.");
|
|
656
|
-
const processorSlug = getProcessorSlug(contract);
|
|
657
|
-
if (!("stateSchema" in contract) || !isZodSchema(contract.stateSchema)) throw new Error(`Processor "${processorSlug}" must define stateSchema.`);
|
|
658
|
-
let defaultState;
|
|
659
|
-
try {
|
|
660
|
-
defaultState = contract.stateSchema.parse({});
|
|
661
|
-
} catch (error) {
|
|
662
|
-
throw new Error(`Processor "${processorSlug}" stateSchema must parse {}.`, { cause: error });
|
|
663
|
-
}
|
|
664
|
-
assertObjectProcessorState({
|
|
665
|
-
processorSlug,
|
|
666
|
-
value: defaultState
|
|
667
|
-
});
|
|
668
|
-
}
|
|
669
|
-
/**
|
|
670
|
-
* Resolve the payload schema a processor should use for an incoming event:
|
|
671
|
-
* the named definition when the type is listed in `consumes`, a permissive
|
|
672
|
-
* `z.unknown()` definition when the contract consumes `"*"`, and `undefined`
|
|
673
|
-
* when the event is not consumed at all. Runtime counterpart of
|
|
674
|
-
* `ConsumedEvent<Contract>`.
|
|
675
|
-
*
|
|
676
|
-
* `"*"` NEVER MATCHES AN EPHEMERAL EVENT, and that one rule is what lets
|
|
677
|
-
* ephemeral types live in `consumes` beside durable ones instead of in a
|
|
678
|
-
* parallel list. Naming a type explicitly is the opt-in: you cannot be handed
|
|
679
|
-
* a microphone firehose by a wildcard you wrote for durable facts, and a
|
|
680
|
-
* processor that wants live events says so by type. Ephemeral bodies live
|
|
681
|
-
* only in the Stream DO's bounded buffer, so a processor receiving one must
|
|
682
|
-
* have decided it can cope with never seeing it again — a decision nobody
|
|
683
|
-
* makes by writing `"*"`.
|
|
684
|
-
*/
|
|
685
|
-
function getConsumedEventDefinition(args) {
|
|
686
|
-
if (!args.contract.consumes.includes(args.eventType)) {
|
|
687
|
-
if (args.ephemeral === true) return void 0;
|
|
688
|
-
if (args.contract.consumes.includes("*")) return { payloadSchema: z.unknown() };
|
|
689
|
-
return;
|
|
690
|
-
}
|
|
691
|
-
const eventDefinition = getResolvedEventDefinition(args);
|
|
692
|
-
if (eventDefinition == null) throw new Error(`Unresolved stream processor consumes event type "${args.eventType}".`);
|
|
693
|
-
if (args.ephemeral === true && eventDefinition.ephemeral !== true) return void 0;
|
|
694
|
-
return eventDefinition;
|
|
695
|
-
}
|
|
696
|
-
function getResolvedEventDefinition(args) {
|
|
697
|
-
const localEventDefinition = args.contract.events[args.eventType];
|
|
698
|
-
if (localEventDefinition != null) return localEventDefinition;
|
|
699
|
-
for (const dependency of args.contract.processorDeps ?? []) {
|
|
700
|
-
const dependencyEventDefinition = getDependencyEvents(dependency)?.[args.eventType];
|
|
701
|
-
if (dependencyEventDefinition != null) return dependencyEventDefinition;
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
function getDependencyEvents(dependency) {
|
|
705
|
-
if (isEventCatalog(dependency)) return dependency;
|
|
706
|
-
if (typeof dependency === "object" && dependency !== null && "events" in dependency && isEventCatalog(dependency.events)) return dependency.events;
|
|
707
|
-
}
|
|
708
|
-
function assertNoLocalProcessorDepEventConflicts(contract) {
|
|
709
|
-
if (typeof contract !== "object" || contract === null || !("events" in contract)) return;
|
|
710
|
-
if (!isEventCatalog(contract.events)) return;
|
|
711
|
-
const processorDeps = "processorDeps" in contract && Array.isArray(contract.processorDeps) ? contract.processorDeps : [];
|
|
712
|
-
for (const dependency of processorDeps) {
|
|
713
|
-
const dependencyEvents = getDependencyEvents(dependency);
|
|
714
|
-
if (dependencyEvents === void 0) continue;
|
|
715
|
-
for (const type of Object.keys(contract.events)) {
|
|
716
|
-
if (!Object.prototype.hasOwnProperty.call(dependencyEvents, type)) continue;
|
|
717
|
-
throw new Error(`Processor "${getProcessorSlug(contract)}" defines event "${type}" that is already owned by processor dependency "${getProcessorSlug(dependency)}".`);
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
function isEventCatalog(value) {
|
|
722
|
-
if (typeof value !== "object" || value === null) return false;
|
|
723
|
-
return Object.values(value).every(isEventDefinition);
|
|
724
|
-
}
|
|
725
|
-
function isEventDefinition(value) {
|
|
726
|
-
return typeof value === "object" && value !== null && "payloadSchema" in value && typeof value.payloadSchema === "object" && value.payloadSchema !== null;
|
|
727
|
-
}
|
|
728
|
-
function isZodSchema(value) {
|
|
729
|
-
return typeof value === "object" && value !== null && "parse" in value && typeof value.parse === "function";
|
|
730
|
-
}
|
|
731
|
-
function getProcessorSlug(contract) {
|
|
732
|
-
if (typeof contract === "object" && contract !== null && "slug" in contract && typeof contract.slug === "string") return contract.slug;
|
|
733
|
-
return "unknown";
|
|
734
|
-
}
|
|
735
|
-
/**
|
|
736
|
-
* The ONE platform revival fact for every recovery-wired stream processor.
|
|
737
|
-
* Appended by the platform keepalive (`durableObjectRecovery` in
|
|
738
|
-
* durable-object-processor-durability.ts) when a processor is revived after
|
|
739
|
-
* its incarnation died owing background work — never emitted by a processor.
|
|
740
|
-
* Per-processor identity rides the payload's `processorSlug` and the
|
|
741
|
-
* `processor-revived:<slug>@...` idempotency key, not the type string.
|
|
742
|
-
* Consuming it is OPTIONAL: a processor should do so only when it reacts to
|
|
743
|
-
* the fact itself. Its append still wakes delivery when it is unconsumed, and
|
|
744
|
-
* a head-reaching frame receives the runner's eventless
|
|
745
|
-
* `processEvent(event: null, caughtUp: true)` pass so open obligations are not
|
|
746
|
-
* stranded. The event DEFINITION (payload schema) lives with the platform's
|
|
747
|
-
* core stream contract; this constant is here so contracts and the recovery
|
|
748
|
-
* adapter agree on the type string without importing that contract.
|
|
749
|
-
*/
|
|
750
|
-
const STREAM_PROCESSOR_REVIVED_EVENT_TYPE = "events.iterate.com/stream/processor-revived";
|
|
751
|
-
/**
|
|
752
|
-
* A processor contract announcement carried on `connection-opened` when the
|
|
753
|
-
* callback owner is a hosted stream processor. UIs and tooling read it from
|
|
754
|
-
* that event and from `runtime.connections[..].openedBy`.
|
|
755
|
-
*/
|
|
756
|
-
const ProcessorContractAnnouncement = z.object({
|
|
757
|
-
slug: z.string().trim().min(1),
|
|
758
|
-
version: z.string().trim().min(1),
|
|
759
|
-
description: z.string(),
|
|
760
|
-
consumes: z.array(z.string()),
|
|
761
|
-
emits: z.array(z.string()),
|
|
762
|
-
ownedEvents: z.array(z.object({
|
|
763
|
-
type: z.string().trim().min(1),
|
|
764
|
-
description: z.string().optional()
|
|
765
|
-
}))
|
|
766
|
-
});
|
|
767
|
-
/**
|
|
768
|
-
* Platform stream events a processor contract may CONSUME without owning —
|
|
769
|
-
* pass as a `processorDeps` entry. Currently just the keepalive revival fact.
|
|
770
|
-
* Consumption is optional and belongs only in processors that react to the
|
|
771
|
-
* fact itself; an unconsumed revival tail receives the runner's eventless
|
|
772
|
-
* at-head turn. The event's authoritative definition lives with the
|
|
773
|
-
* platform's core stream contract, and this catalog is deliberately
|
|
774
|
-
* payload-loose.
|
|
775
|
-
*/
|
|
776
|
-
const PLATFORM_STREAM_EVENTS = { [STREAM_PROCESSOR_REVIVED_EVENT_TYPE]: {
|
|
777
|
-
description: "Platform keepalive revival fact: appended when a processor's incarnation died owing background work; consumed when the processor reacts to the fact itself, otherwise followed by an eventless at-head processEvent turn.",
|
|
778
|
-
payloadSchema: z.looseObject({})
|
|
779
|
-
} };
|
|
780
|
-
//#endregion
|
|
781
|
-
//#region src/processors/stream-processor.ts
|
|
782
|
-
async function awaitKeepAliveBacked(keepAliveWhile, work) {
|
|
783
|
-
if (keepAliveWhile === void 0) return await work();
|
|
784
|
-
return await new Promise((resolve, reject) => {
|
|
785
|
-
keepAliveWhile(async () => {
|
|
786
|
-
try {
|
|
787
|
-
const result = await work();
|
|
788
|
-
resolve(result);
|
|
789
|
-
return result;
|
|
790
|
-
} catch (error) {
|
|
791
|
-
reject(error);
|
|
792
|
-
throw error;
|
|
793
|
-
}
|
|
794
|
-
});
|
|
795
|
-
});
|
|
796
|
-
}
|
|
797
|
-
/**
|
|
798
|
-
* Class-based stream processor.
|
|
799
|
-
*
|
|
800
|
-
* The model in one sentence: the StreamProcessorRunner
|
|
801
|
-
* (stream-processor-runner.ts) delivers ordered events, folds each consumed
|
|
802
|
-
* event into state through `reduce`, hands each reduction to the side-effect
|
|
803
|
-
* hooks, and owns cursors, checkpoints, retry, and recovery — the processor
|
|
804
|
-
* itself is only the hooks.
|
|
805
|
-
*
|
|
806
|
-
* Subclasses override up to two hooks:
|
|
807
|
-
*
|
|
808
|
-
* - `reduce` — pure projection of one consumed event into the next state
|
|
809
|
-
* - `processEvent` — synchronous per-event side effects; what most processors
|
|
810
|
-
* implement. Side effects derived from the whole fold (rather than the
|
|
811
|
-
* delivered event) belong here too, guarded by `args.delivery.caughtUp`.
|
|
812
|
-
* `args.event` is `null` only when a caught-up scan contained no
|
|
813
|
-
* consumed event; authors skip their per-event switch but can still act on
|
|
814
|
-
* the fold.
|
|
815
|
-
*
|
|
816
|
-
* Every hook runs inside the runner's serialized delivery chain: a later
|
|
817
|
-
* batch never starts until the previous one has completed or failed, and the
|
|
818
|
-
* cursor is only committed after the hooks (plus any `blockProcessorWhile`
|
|
819
|
-
* work) succeed.
|
|
820
|
-
*/
|
|
821
|
-
var StreamProcessor = class extends RpcTarget {
|
|
822
|
-
stream;
|
|
823
|
-
/** Path of the home stream — the one `this.stream` points at. */
|
|
824
|
-
path;
|
|
825
|
-
/** Owning project, or null on a global (deployment-root) stream. */
|
|
826
|
-
projectId;
|
|
827
|
-
deps;
|
|
828
|
-
/**
|
|
829
|
-
* Self-measured consumption metrics (see event-consumption-metrics.ts): every
|
|
830
|
-
* home-stream append and every committed event batch feeds it (the
|
|
831
|
-
* latter through the driver's `noteBatchIngested`), closing the
|
|
832
|
-
* consume-your-own-appends loop on the processor's own clock. HOSTS merge
|
|
833
|
-
* `eventConsumptionMetrics.report()` into the `getRuntimeState` answer they give
|
|
834
|
-
* the stream (`runtime.metrics`) — merged host-side so a subclass
|
|
835
|
-
* overriding `getRuntimeState` with its own `runtime` bag cannot
|
|
836
|
-
* accidentally drop it. In-memory; resets with the isolate.
|
|
837
|
-
*/
|
|
838
|
-
eventConsumptionMetrics = new EventConsumptionMetrics(Date.now());
|
|
839
|
-
#keepAliveWhile;
|
|
840
|
-
constructor(args) {
|
|
841
|
-
super();
|
|
842
|
-
const { stream, path, projectId, keepAliveWhile, ...deps } = args;
|
|
843
|
-
this.stream = stream;
|
|
844
|
-
this.path = path;
|
|
845
|
-
this.projectId = projectId;
|
|
846
|
-
this.deps = deps;
|
|
847
|
-
this.#keepAliveWhile = keepAliveWhile;
|
|
848
|
-
}
|
|
849
|
-
/**
|
|
850
|
-
* @internal Hands the StreamProcessorRunner its {@link StreamProcessorRunnerHooks}.
|
|
851
|
-
* A STATIC accessor on purpose: statics may reach protected/private members
|
|
852
|
-
* of instances of their own class, so the runner gets the hooks without any
|
|
853
|
-
* new public instance member (nothing for subclasses to see, shadow, or
|
|
854
|
-
* call). Authors never touch this; the runner is its only caller.
|
|
855
|
-
*/
|
|
856
|
-
static runnerHooks(processor) {
|
|
857
|
-
return {
|
|
858
|
-
contract: processor.contract,
|
|
859
|
-
initialState: () => processor.contract.stateSchema.parse({}),
|
|
860
|
-
parseState: (value) => {
|
|
861
|
-
const parsed = processor.contract.stateSchema.safeParse(value);
|
|
862
|
-
return parsed.success ? {
|
|
863
|
-
success: true,
|
|
864
|
-
state: parsed.data
|
|
865
|
-
} : {
|
|
866
|
-
success: false,
|
|
867
|
-
error: parsed.error
|
|
868
|
-
};
|
|
869
|
-
},
|
|
870
|
-
reduceRawEvent: (args) => processor.#reduceRawEvent(args),
|
|
871
|
-
isDeliverable: (event) => processor.#isDeliverable(event),
|
|
872
|
-
processEvent: (args) => processor.processEvent(args),
|
|
873
|
-
noteBatchIngested: (args) => processor.eventConsumptionMetrics.noteBatchIngested(args),
|
|
874
|
-
idempotencyKey: (key, whileProcessing) => processor.idempotencyKey(key, whileProcessing),
|
|
875
|
-
processorStamp: (streamId, whileProcessing) => processor.#processorStamp(streamId, whileProcessing),
|
|
876
|
-
append: (opts, input) => processor.#appendStamped({
|
|
877
|
-
target: processor.stream,
|
|
878
|
-
targetPath: processor.path,
|
|
879
|
-
sourceStreamId: opts.streamId,
|
|
880
|
-
whileProcessing: opts.whileProcessing
|
|
881
|
-
}, input),
|
|
882
|
-
appendTo: (path, opts, input) => processor.#appendStamped({
|
|
883
|
-
...processor.#appendTarget(path),
|
|
884
|
-
sourceStreamId: opts.streamId,
|
|
885
|
-
whileProcessing: opts.whileProcessing
|
|
886
|
-
}, input)
|
|
887
|
-
};
|
|
888
|
-
}
|
|
889
|
-
/**
|
|
890
|
-
* The processor-contributed slice of the published runtime state: the
|
|
891
|
-
* operational `runtime` bag only (see {@link ProcessorRuntimeContribution}).
|
|
892
|
-
* Subclasses override to expose debug data; the base contributes nothing.
|
|
893
|
-
* The snapshot half comes from the runner, and event-consumption metrics are
|
|
894
|
-
* merged in host-side — never read cursor state here.
|
|
895
|
-
*/
|
|
896
|
-
async getRuntimeState() {
|
|
897
|
-
return {};
|
|
898
|
-
}
|
|
899
|
-
/** Build and validate an append input for an event listed in `contract.emits`. */
|
|
900
|
-
#buildEmittedEvent(event) {
|
|
901
|
-
if (!this.contract.emits.includes(event.type)) throw new Error(`Processor "${this.contract.slug}" cannot build emitted event "${event.type}".`);
|
|
902
|
-
const eventDefinition = getResolvedEventDefinition({
|
|
903
|
-
contract: this.contract,
|
|
904
|
-
eventType: event.type
|
|
905
|
-
});
|
|
906
|
-
if (eventDefinition === void 0) throw new Error(`Unresolved stream processor emits event type "${event.type}".`);
|
|
907
|
-
return getEventInputSchema({
|
|
908
|
-
type: event.type,
|
|
909
|
-
payloadSchema: eventDefinition.payloadSchema,
|
|
910
|
-
ephemeral: eventDefinition.ephemeral
|
|
911
|
-
}).parse(event);
|
|
912
|
-
}
|
|
913
|
-
/**
|
|
914
|
-
* Pure projection of one consumed event into the next state. Defaults to
|
|
915
|
-
* identity; returning `null`/`undefined` also keeps the current state.
|
|
916
|
-
*/
|
|
917
|
-
reduce(args) {
|
|
918
|
-
return args.state;
|
|
919
|
-
}
|
|
920
|
-
/**
|
|
921
|
-
* Synchronous side-effect hook, called by the runner once per consumed event
|
|
922
|
-
* and, when necessary, once more with `event: null` for a caught-up scan
|
|
923
|
-
* that consumed nothing. It is ALSO the caught-up processing: when
|
|
924
|
-
* `args.delivery.caughtUp` is true (`args.state` is the whole observed fold),
|
|
925
|
-
* an obligation processor
|
|
926
|
-
* drives its undriven obligations and settles dead ones — scheduling that
|
|
927
|
-
* async work via `args.blockProcessorWhile`, keyed by STABLE obligation keys
|
|
928
|
-
* (`this.idempotencyKey(<obligation>)` with the deciding state folded into
|
|
929
|
-
* the key and NO event bound, so a redelivery/revival does not rotate the
|
|
930
|
-
* key and re-run the effect).
|
|
931
|
-
* The runner never sets `caughtUp` below its highest observed offset — no override
|
|
932
|
-
* needs its own mid-catch-up gate. Simple processors ignore the flag.
|
|
933
|
-
*/
|
|
934
|
-
processEvent(_args) {}
|
|
935
|
-
/** Parse a raw event against the contract: `undefined` (type not consumed),
|
|
936
|
-
* a Zod error (consumed type, bad shape), or the typed consumed event.
|
|
937
|
-
* Stateless — shared by {@link #reduceRawEvent} and {@link #isDeliverable}. */
|
|
938
|
-
#parseConsumedEvent(event) {
|
|
939
|
-
const eventDefinition = getConsumedEventDefinition({
|
|
940
|
-
contract: this.contract,
|
|
941
|
-
eventType: event.type,
|
|
942
|
-
ephemeral: event.ephemeral
|
|
943
|
-
});
|
|
944
|
-
if (eventDefinition === void 0) return { ok: false };
|
|
945
|
-
const parsed = cachedEventSchema({
|
|
946
|
-
type: event.type,
|
|
947
|
-
payloadSchema: eventDefinition.payloadSchema,
|
|
948
|
-
ephemeral: eventDefinition.ephemeral
|
|
949
|
-
}).safeParse(event);
|
|
950
|
-
if (!parsed.success) return {
|
|
951
|
-
ok: false,
|
|
952
|
-
error: parsed.error
|
|
953
|
-
};
|
|
954
|
-
return {
|
|
955
|
-
ok: true,
|
|
956
|
-
event: parsed.data
|
|
957
|
-
};
|
|
958
|
-
}
|
|
959
|
-
/** True when this event will reach `processEvent`: a consumed type that
|
|
960
|
-
* parses. A malformed consumed event is deliberately NOT deliverable. */
|
|
961
|
-
#isDeliverable(event) {
|
|
962
|
-
return this.#parseConsumedEvent(event).ok;
|
|
963
|
-
}
|
|
964
|
-
/**
|
|
965
|
-
* Reduce one raw stream event against explicit state, without touching any
|
|
966
|
-
* processor-internal state. Returns `undefined` for events this processor
|
|
967
|
-
* does not consume, and a {@link ConsumedEventParseFailure} for events of a
|
|
968
|
-
* consumed TYPE whose shape fails the contract parse — streams accept raw
|
|
969
|
-
* appends by design, so a malformed event is a fact of the log, not an
|
|
970
|
-
* exception: throwing here would wedge the cursor on it forever.
|
|
971
|
-
*/
|
|
972
|
-
#reduceRawEvent(args) {
|
|
973
|
-
const parsed = this.#parseConsumedEvent(args.event);
|
|
974
|
-
if (!parsed.ok) return parsed.error === void 0 ? void 0 : { parseError: parsed.error };
|
|
975
|
-
const event = parsed.event;
|
|
976
|
-
const state = this.reduce({
|
|
977
|
-
event,
|
|
978
|
-
state: args.state
|
|
979
|
-
}) ?? args.state;
|
|
980
|
-
assertObjectProcessorState({
|
|
981
|
-
processorSlug: this.contract.slug,
|
|
982
|
-
value: state
|
|
983
|
-
});
|
|
984
|
-
return {
|
|
985
|
-
event,
|
|
986
|
-
previousState: args.state,
|
|
987
|
-
state
|
|
988
|
-
};
|
|
989
|
-
}
|
|
990
|
-
/**
|
|
991
|
-
* Fire-and-forget async work backed by the injected keep-alive, with
|
|
992
|
-
* failures logged. For work launched OUTSIDE a delivery hook (DO verbs,
|
|
993
|
-
* alarm handlers); inside `processEvent`, use the `runInBackground` helper
|
|
994
|
-
* from the hook args — that one rides the runner's recovery keepalive.
|
|
995
|
-
*/
|
|
996
|
-
runInBackground(work) {
|
|
997
|
-
awaitKeepAliveBacked(this.#keepAliveWhile, work).catch((error) => {
|
|
998
|
-
console.error("stream processor background work failed", error);
|
|
999
|
-
});
|
|
1000
|
-
}
|
|
1001
|
-
/**
|
|
1002
|
-
* Append events listed in `contract.emits` to this processor's own stream,
|
|
1003
|
-
* stamped with `source.processor` provenance (no `whileProcessing`: this
|
|
1004
|
-
* overload is for appends outside any event batch — alarm handlers, DO methods —
|
|
1005
|
-
* and for decisions derived from the whole fold). Inside `processEvent`,
|
|
1006
|
-
* prefer the event-bound `args.append`.
|
|
1007
|
-
*/
|
|
1008
|
-
append(...input) {
|
|
1009
|
-
return this.#appendStamped({
|
|
1010
|
-
target: this.stream,
|
|
1011
|
-
targetPath: this.path
|
|
1012
|
-
}, input);
|
|
1013
|
-
}
|
|
1014
|
-
/** Like {@link append}, onto a sibling stream (resolved via `stream.at(path)`). */
|
|
1015
|
-
appendTo(path, ...input) {
|
|
1016
|
-
return this.#appendStamped(this.#appendTarget(path), input);
|
|
1017
|
-
}
|
|
1018
|
-
#appendTarget(path) {
|
|
1019
|
-
const targetPath = resolveStreamPath(this.path, path);
|
|
1020
|
-
return {
|
|
1021
|
-
target: targetPath === this.path ? this.stream : this.stream.at(path),
|
|
1022
|
-
targetPath
|
|
1023
|
-
};
|
|
1024
|
-
}
|
|
1025
|
-
/**
|
|
1026
|
-
* Processor-scoped idempotency key: `<slug>/<key>`, plus `@<path>:<offset>`
|
|
1027
|
-
* when the append is a deterministic consequence of processing one event —
|
|
1028
|
-
* a resent event batch then dedupes instead of double-appending. The path
|
|
1029
|
-
* makes fan-in safe: two same-slug processors on different streams
|
|
1030
|
-
* forwarding into one target can never collide. Omit `whileProcessing` for
|
|
1031
|
-
* state-derived appends and fold the deciding state into `key` instead
|
|
1032
|
-
* (e.g. a generation counter).
|
|
1033
|
-
*/
|
|
1034
|
-
idempotencyKey(key, whileProcessing) {
|
|
1035
|
-
const base = `${this.contract.slug}/${key}`;
|
|
1036
|
-
if (whileProcessing === void 0) return base;
|
|
1037
|
-
return `${base}@${whileProcessing.path}:${whileProcessing.offset}`;
|
|
1038
|
-
}
|
|
1039
|
-
/**
|
|
1040
|
-
* The provenance stamp for one append. Always overwrites any
|
|
1041
|
-
* caller-supplied `source.processor`: the stamp describes THIS append, and
|
|
1042
|
-
* ancestry stays walkable through `whileProcessing` (and `copiedFrom`
|
|
1043
|
-
* for subscription copies, which preserve the original stamp).
|
|
1044
|
-
*/
|
|
1045
|
-
#processorStamp(streamId, whileProcessing) {
|
|
1046
|
-
return {
|
|
1047
|
-
slug: this.contract.slug,
|
|
1048
|
-
version: this.contract.version,
|
|
1049
|
-
stream: {
|
|
1050
|
-
path: this.path,
|
|
1051
|
-
projectId: this.projectId,
|
|
1052
|
-
streamId
|
|
1053
|
-
},
|
|
1054
|
-
...whileProcessing === void 0 ? {} : { whileProcessing: {
|
|
1055
|
-
offset: whileProcessing.offset,
|
|
1056
|
-
type: whileProcessing.type
|
|
1057
|
-
} }
|
|
1058
|
-
};
|
|
1059
|
-
}
|
|
1060
|
-
#appendStamped(args, input) {
|
|
1061
|
-
const builtEvents = input.map((event) => this.#buildEmittedEvent(event));
|
|
1062
|
-
return this.#appendBuiltEvents(args, builtEvents);
|
|
1063
|
-
}
|
|
1064
|
-
async #appendBuiltEvents(args, builtEvents) {
|
|
1065
|
-
const sourceStreamId = args.sourceStreamId ?? (await this.stream.getEventPage({
|
|
1066
|
-
afterOffset: Number.MAX_SAFE_INTEGER,
|
|
1067
|
-
limit: 1
|
|
1068
|
-
})).streamId;
|
|
1069
|
-
const processor = this.#processorStamp(sourceStreamId, args.whileProcessing);
|
|
1070
|
-
let events = builtEvents.map((built) => ({
|
|
1071
|
-
...built,
|
|
1072
|
-
source: {
|
|
1073
|
-
...built.source,
|
|
1074
|
-
processor
|
|
1075
|
-
}
|
|
1076
|
-
}));
|
|
1077
|
-
if (args.targetPath !== this.path) {
|
|
1078
|
-
events = events.map((event) => event.idempotencyKey === void 0 ? event : {
|
|
1079
|
-
...event,
|
|
1080
|
-
idempotencyKey: `${event.idempotencyKey}@source-stream:${sourceStreamId}`
|
|
1081
|
-
});
|
|
1082
|
-
return args.target.append(...events);
|
|
1083
|
-
}
|
|
1084
|
-
const t0 = Date.now();
|
|
1085
|
-
return this.stream.appendIfStreamId({
|
|
1086
|
-
streamId: sourceStreamId,
|
|
1087
|
-
events
|
|
1088
|
-
}).then((committed) => {
|
|
1089
|
-
if (committed.length === 0) return committed;
|
|
1090
|
-
let maxCommittedOffset = null;
|
|
1091
|
-
for (const event of committed) {
|
|
1092
|
-
if (!this.#isDeliverable(event)) continue;
|
|
1093
|
-
maxCommittedOffset = Math.max(maxCommittedOffset ?? 0, event.offset);
|
|
1094
|
-
}
|
|
1095
|
-
this.eventConsumptionMetrics.noteAppendCommitted({
|
|
1096
|
-
maxCommittedOffset,
|
|
1097
|
-
t0,
|
|
1098
|
-
atMs: Date.now()
|
|
1099
|
-
});
|
|
1100
|
-
return committed;
|
|
1101
|
-
});
|
|
1102
|
-
}
|
|
1103
|
-
};
|
|
1104
|
-
//#endregion
|
|
1105
|
-
//#region src/processors/stream-processor-runner.ts
|
|
1106
|
-
/**
|
|
1107
|
-
* Processes event batches for one processor on one stream. Runtime-neutral:
|
|
1108
|
-
* the browser, the Durable Object registry, and the in-memory
|
|
1109
|
-
* test harness all instantiate exactly this class and differ only in the
|
|
1110
|
-
* `durability` / `keepAlive` adapters they pass. One runner per processor —
|
|
1111
|
-
* the "host" of old survives only as a thin registry that builds adapters and
|
|
1112
|
-
* routes wakes/alarms to the right runner.
|
|
1113
|
-
*
|
|
1114
|
-
* Serialization: batches and self-pulls share ONE in-memory chain, so a
|
|
1115
|
-
* catch-up never interleaves with a half-processed batch. Cross-incarnation
|
|
1116
|
-
* races (a stale runner outliving progress made elsewhere) are fenced durably
|
|
1117
|
-
* instead, by the progress store's `cursorRevision` CAS + monotonic fence.
|
|
1118
|
-
*/
|
|
1119
|
-
var StreamProcessorRunner = class {
|
|
1120
|
-
processor;
|
|
1121
|
-
hooks;
|
|
1122
|
-
stream;
|
|
1123
|
-
durability;
|
|
1124
|
-
keepAlive;
|
|
1125
|
-
now;
|
|
1126
|
-
readPageSize;
|
|
1127
|
-
/** Memoized load for one stream lifetime; cleared on failure or recreation. */
|
|
1128
|
-
#loaded;
|
|
1129
|
-
#loadingStreamId;
|
|
1130
|
-
/** True once progress reflects a real load (fresh default over an empty
|
|
1131
|
-
* store counts; a pending/failed load does not) — the gate that keeps
|
|
1132
|
-
* default or partially-refolded state from ever escaping (the legacy
|
|
1133
|
-
* `isLoaded` invariant). `snapshot()` additionally
|
|
1134
|
-
* awaits the load, so partial state cannot escape through it either. */
|
|
1135
|
-
#hasLoaded = false;
|
|
1136
|
-
/** The COMMITTED, loaded progress — what snapshots and direct callbacks publish.
|
|
1137
|
-
* A hosted wake may publish only the separately-read processing cursor before this
|
|
1138
|
-
* reduction cache loads. Batch folds accumulate in locals and land here only after
|
|
1139
|
-
* the durable commit. */
|
|
1140
|
-
#progress;
|
|
1141
|
-
/** Highest stream offset observed across all batches this incarnation. */
|
|
1142
|
-
#highestObservedOffset = 0;
|
|
1143
|
-
/** Serializes batches + self-pulls; failures are contained per entry. */
|
|
1144
|
-
#chain = Promise.resolve();
|
|
1145
|
-
#disposed = false;
|
|
1146
|
-
#eventWaiters = /* @__PURE__ */ new Set();
|
|
1147
|
-
#stateChangeObservers = /* @__PURE__ */ new Set();
|
|
1148
|
-
/** Memoized schema default, for pre-load `currentState` reads. */
|
|
1149
|
-
#defaultState;
|
|
1150
|
-
constructor(args) {
|
|
1151
|
-
this.processor = args.processor;
|
|
1152
|
-
this.hooks = StreamProcessor.runnerHooks(args.processor);
|
|
1153
|
-
this.stream = args.stream;
|
|
1154
|
-
this.durability = args.durability;
|
|
1155
|
-
this.keepAlive = args.keepAlive;
|
|
1156
|
-
this.now = args.now ?? (() => Date.now());
|
|
1157
|
-
this.readPageSize = args.readPageSize ?? 500;
|
|
1158
|
-
}
|
|
1159
|
-
/**
|
|
1160
|
-
* Opens the processor's event-batch callback and returns its committed
|
|
1161
|
-
* processing offset. A hosted processor wake returns this pair to a source
|
|
1162
|
-
* stream; the browser database writer calls the same method directly.
|
|
1163
|
-
*
|
|
1164
|
-
* `checkpointOffset` is the PROCESSING cursor (`acknowledgedThroughOffset`),
|
|
1165
|
-
* never the reduction offset: the caller resumes after this value, and
|
|
1166
|
-
* resuming from a reduction-pinned snapshot
|
|
1167
|
-
* offset could skip events whose effects were never acknowledged.
|
|
1168
|
-
*
|
|
1169
|
-
* `processEventBatch` is the only place transport batching enters the
|
|
1170
|
-
* runner; inside it the runner reduces and processes one event at a time. A
|
|
1171
|
-
* hosting transport may adapt how the promise is observed, but must not
|
|
1172
|
-
* duplicate these semantics.
|
|
1173
|
-
*/
|
|
1174
|
-
async openEventBatchCallback(expectedStreamId) {
|
|
1175
|
-
this.#assertNotDisposed();
|
|
1176
|
-
const opened = await this.#enqueue(async () => {
|
|
1177
|
-
const streamId = await this.#readCurrentStreamId(expectedStreamId);
|
|
1178
|
-
await this.#load(streamId);
|
|
1179
|
-
return {
|
|
1180
|
-
streamId,
|
|
1181
|
-
checkpointOffset: this.#requireProgress().processing.acknowledgedThroughOffset
|
|
1182
|
-
};
|
|
1183
|
-
});
|
|
1184
|
-
return this.#eventBatchCallback({
|
|
1185
|
-
...opened,
|
|
1186
|
-
deferredLoad: false,
|
|
1187
|
-
sourceScansAllEvents: false
|
|
1188
|
-
});
|
|
1189
|
-
}
|
|
1190
|
-
/**
|
|
1191
|
-
* Open the callback used by a trusted hosted source Stream.
|
|
1192
|
-
*
|
|
1193
|
-
* The request already carries that source's authoritative stream ID. Reading
|
|
1194
|
-
* it back before returning would deadlock a colocated Processor Facet: the
|
|
1195
|
-
* source alarm owns the wake RPC while the facet's identity/refold read waits
|
|
1196
|
-
* for that same source turn. Return the durable processing cursor without a
|
|
1197
|
-
* source read, then finish any reduction-cache load when the source invokes
|
|
1198
|
-
* the independent one-way batch callback.
|
|
1199
|
-
*/
|
|
1200
|
-
async openHostedEventBatchCallback(streamId) {
|
|
1201
|
-
this.#assertNotDisposed();
|
|
1202
|
-
const checkpointOffset = await this.#enqueue(() => this.#prepareHostedCheckpoint(streamId));
|
|
1203
|
-
return this.#eventBatchCallback({
|
|
1204
|
-
streamId,
|
|
1205
|
-
checkpointOffset,
|
|
1206
|
-
deferredLoad: true,
|
|
1207
|
-
sourceScansAllEvents: true
|
|
1208
|
-
});
|
|
1209
|
-
}
|
|
1210
|
-
#eventBatchCallback(args) {
|
|
1211
|
-
return {
|
|
1212
|
-
checkpointOffset: args.checkpointOffset,
|
|
1213
|
-
processEventBatch: (batch) => {
|
|
1214
|
-
const attempt = this.#enqueue(async () => {
|
|
1215
|
-
if (args.deferredLoad) await this.#loadPreparedStream(args.streamId);
|
|
1216
|
-
await this.#processBatch(batch);
|
|
1217
|
-
});
|
|
1218
|
-
this.durability?.recovery?.keepAliveWhile(() => attempt);
|
|
1219
|
-
if (!args.sourceScansAllEvents) this.#runInBackground(() => attempt.then(() => this.#enqueue(async () => {
|
|
1220
|
-
const { processing } = this.#requireProgress();
|
|
1221
|
-
if (processing.acknowledgedThroughOffset < batch.streamMaxOffset) await this.#selfCatchUp();
|
|
1222
|
-
}), () => void 0));
|
|
1223
|
-
return attempt;
|
|
1224
|
-
}
|
|
1225
|
-
};
|
|
1226
|
-
}
|
|
1227
|
-
/** Handle a durable recovery alarm routed here by the hosting registry. */
|
|
1228
|
-
async handleAlarm(info) {
|
|
1229
|
-
const recovery = this.durability?.recovery;
|
|
1230
|
-
if (recovery === void 0) return;
|
|
1231
|
-
await recovery.handleAlarm(info);
|
|
1232
|
-
}
|
|
1233
|
-
/** One consistent read of the fold, pinned to `reducedThroughOffset`. */
|
|
1234
|
-
async snapshot() {
|
|
1235
|
-
return this.#enqueue(async () => {
|
|
1236
|
-
const streamId = await this.#readCurrentStreamId();
|
|
1237
|
-
await this.#load(streamId);
|
|
1238
|
-
const progress = this.#requireProgress();
|
|
1239
|
-
return {
|
|
1240
|
-
offset: progress.reduction.reducedThroughOffset,
|
|
1241
|
-
state: progress.reduction.state
|
|
1242
|
-
};
|
|
1243
|
-
});
|
|
1244
|
-
}
|
|
1245
|
-
/**
|
|
1246
|
-
* Whether published state IS a real fold rather than the schema default —
|
|
1247
|
-
* the legacy `isLoaded` gate. With the runner, the load itself performs any
|
|
1248
|
-
* pending refold, so this is true whenever a load has completed and false
|
|
1249
|
-
* only before the first successful load.
|
|
1250
|
-
*/
|
|
1251
|
-
get isLoaded() {
|
|
1252
|
-
return this.#hasLoaded;
|
|
1253
|
-
}
|
|
1254
|
-
/** Highest offset whose processing and blocking consequences have committed. */
|
|
1255
|
-
get currentAcknowledgedThroughOffset() {
|
|
1256
|
-
return this.#progress?.processing.acknowledgedThroughOffset ?? 0;
|
|
1257
|
-
}
|
|
1258
|
-
/** Source lifetime paired atomically with the current committed state and cursor. */
|
|
1259
|
-
get currentStreamId() {
|
|
1260
|
-
return this.#progress?.streamId;
|
|
1261
|
-
}
|
|
1262
|
-
/**
|
|
1263
|
-
* The current committed fold, synchronously (the schema default until the
|
|
1264
|
-
* first load) — the legacy `StreamProcessor.currentState`,
|
|
1265
|
-
* kept so a hosting registry can assemble its
|
|
1266
|
-
* live state without an async hop. Gate on {@link isLoaded} first: a cold
|
|
1267
|
-
* runner reports the default, and publishing that anywhere live would wipe
|
|
1268
|
-
* real facts for state observers.
|
|
1269
|
-
*/
|
|
1270
|
-
get currentState() {
|
|
1271
|
-
if (this.#progress !== void 0) return this.#progress.reduction.state;
|
|
1272
|
-
this.#defaultState ??= this.hooks.initialState();
|
|
1273
|
-
return this.#defaultState;
|
|
1274
|
-
}
|
|
1275
|
-
/**
|
|
1276
|
-
* Observe committed reduced-state changes IN-PROCESS: the observer is a
|
|
1277
|
-
* local function (the hosting registry wires it to reassemble its
|
|
1278
|
-
* live-state engine), never a retained RPC stub. It fires after a batch
|
|
1279
|
-
* commit lands durably AND the committed state changed identity — the
|
|
1280
|
-
* runner's home for the legacy `StreamProcessor.observeStateChanges` +
|
|
1281
|
-
* post-persist notify. Returns a function that stops observing.
|
|
1282
|
-
*/
|
|
1283
|
-
observeStateChanges(observer) {
|
|
1284
|
-
this.#stateChangeObservers.add(observer);
|
|
1285
|
-
return () => void this.#stateChangeObservers.delete(observer);
|
|
1286
|
-
}
|
|
1287
|
-
/**
|
|
1288
|
-
* Read journal pages after the acknowledged cursor and process them until
|
|
1289
|
-
* caught up — the public method for read-your-writes and a
|
|
1290
|
-
* hosting registry's cold-load healing (the legacy host's `catchUpInternal`
|
|
1291
|
-
* shape). One page of lookahead, so every non-final batch carries a
|
|
1292
|
-
* `streamMaxOffset` past its own last event and only the genuinely final page is
|
|
1293
|
-
* marked caught up. Serialized with delivered batches on the runner's chain; failures
|
|
1294
|
-
* RETHROW — the caller owns any swallow-and-log policy.
|
|
1295
|
-
*/
|
|
1296
|
-
catchUp() {
|
|
1297
|
-
return this.#enqueue(async () => {
|
|
1298
|
-
const streamId = await this.#readCurrentStreamId();
|
|
1299
|
-
await this.#load(streamId);
|
|
1300
|
-
await this.#selfCatchUp();
|
|
1301
|
-
});
|
|
1302
|
-
}
|
|
1303
|
-
async waitUntilEvent(args) {
|
|
1304
|
-
if (args.signal?.aborted === true) throw abortReason(args.signal);
|
|
1305
|
-
if ("offset" in args) {
|
|
1306
|
-
if (!Number.isSafeInteger(args.offset) || args.offset < 0) throw new Error("waitUntilEvent offset must be a non-negative safe integer");
|
|
1307
|
-
const streamId = await this.#readCurrentStreamId();
|
|
1308
|
-
await this.#load(streamId);
|
|
1309
|
-
if (this.#requireProgress().processing.acknowledgedThroughOffset >= args.offset) return;
|
|
1310
|
-
const { offset, signal, timeoutMs } = args;
|
|
1311
|
-
const reached = this.#registerEventWaiter({
|
|
1312
|
-
kind: "offset",
|
|
1313
|
-
offset
|
|
1314
|
-
}, {
|
|
1315
|
-
signal,
|
|
1316
|
-
timeoutMs
|
|
1317
|
-
});
|
|
1318
|
-
this.catchUp().catch((error) => {
|
|
1319
|
-
reached.reject(error);
|
|
1320
|
-
});
|
|
1321
|
-
return await reached.promise;
|
|
1322
|
-
}
|
|
1323
|
-
const { predicate, signal, timeoutMs } = args;
|
|
1324
|
-
await this.#registerEventWaiter({
|
|
1325
|
-
kind: "predicate",
|
|
1326
|
-
predicate
|
|
1327
|
-
}, {
|
|
1328
|
-
signal,
|
|
1329
|
-
timeoutMs
|
|
1330
|
-
}).promise;
|
|
1331
|
-
}
|
|
1332
|
-
/** Release processor resources. Idempotent; a disposed runner rejects new work. */
|
|
1333
|
-
dispose() {
|
|
1334
|
-
this.#disposed = true;
|
|
1335
|
-
for (const waiter of this.#eventWaiters) this.#settleEventWaiter(waiter, { error: /* @__PURE__ */ new Error("StreamProcessorRunner disposed") });
|
|
1336
|
-
this.#stateChangeObservers.clear();
|
|
1337
|
-
}
|
|
1338
|
-
async #processBatch(batch) {
|
|
1339
|
-
const ingestStartedAtMs = this.now();
|
|
1340
|
-
assertProcessorEventBatch(batch);
|
|
1341
|
-
const committed = this.#requireProgress();
|
|
1342
|
-
if (batch.streamId !== committed.streamId) throw new Error(`stream processor "${this.hooks.contract.slug}" received batch for stream ID ${batch.streamId}; current progress belongs to ${committed.streamId}`);
|
|
1343
|
-
const committedThroughOffset = committed.processing.acknowledgedThroughOffset;
|
|
1344
|
-
if (batch.scannedAfterOffset > committedThroughOffset) throw new Error(`delivery batch starts after the committed scan cursor: ${batch.scannedAfterOffset} > ${committedThroughOffset}`);
|
|
1345
|
-
const batchScannedThroughOffset = Math.max(committedThroughOffset, batch.scannedThroughOffset);
|
|
1346
|
-
const pending = [];
|
|
1347
|
-
let scan = committedThroughOffset;
|
|
1348
|
-
for (const event of batch.events) {
|
|
1349
|
-
if (event.offset <= scan) continue;
|
|
1350
|
-
scan = event.offset;
|
|
1351
|
-
pending.push(event);
|
|
1352
|
-
}
|
|
1353
|
-
this.#highestObservedOffset = Math.max(this.#highestObservedOffset, batch.streamMaxOffset, batchScannedThroughOffset);
|
|
1354
|
-
if (pending.length === 0 && batchScannedThroughOffset === committedThroughOffset) return;
|
|
1355
|
-
const batchCaughtUp = batchScannedThroughOffset >= this.#highestObservedOffset;
|
|
1356
|
-
let lastDeliveredOffset = null;
|
|
1357
|
-
for (const event of pending) if (this.hooks.isDeliverable(event)) lastDeliveredOffset = event.offset;
|
|
1358
|
-
let firedCaughtUp = false;
|
|
1359
|
-
const ctx = {
|
|
1360
|
-
revision: committed.processing.cursorRevision,
|
|
1361
|
-
ingestStartedAtMs,
|
|
1362
|
-
state: committed.reduction.state,
|
|
1363
|
-
reducedThroughOffset: committed.reduction.reducedThroughOffset,
|
|
1364
|
-
completedThroughOffset: committed.processing.acknowledgedThroughOffset,
|
|
1365
|
-
eventsSinceCommit: 0,
|
|
1366
|
-
uncommittedEvents: [],
|
|
1367
|
-
uncommittedParseFailures: []
|
|
1368
|
-
};
|
|
1369
|
-
/** Every blocker started anywhere in this batch, for failure settlement. */
|
|
1370
|
-
const startedBlockers = [];
|
|
1371
|
-
try {
|
|
1372
|
-
for (const event of pending) {
|
|
1373
|
-
const reduction = this.hooks.reduceRawEvent({
|
|
1374
|
-
event,
|
|
1375
|
-
state: ctx.state
|
|
1376
|
-
});
|
|
1377
|
-
if (reduction !== void 0 && "parseError" in reduction) ctx.uncommittedParseFailures.push({
|
|
1378
|
-
event,
|
|
1379
|
-
error: reduction.parseError
|
|
1380
|
-
});
|
|
1381
|
-
else if (reduction !== void 0) {
|
|
1382
|
-
const caughtUp = batchCaughtUp && event.offset === lastDeliveredOffset;
|
|
1383
|
-
if (caughtUp) firedCaughtUp = true;
|
|
1384
|
-
const delivery = {
|
|
1385
|
-
caughtUp,
|
|
1386
|
-
streamId: batch.streamId
|
|
1387
|
-
};
|
|
1388
|
-
let eventChain = Promise.resolve();
|
|
1389
|
-
const whileProcessing = reduction.event;
|
|
1390
|
-
this.hooks.processEvent({
|
|
1391
|
-
event: reduction.event,
|
|
1392
|
-
previousState: reduction.previousState,
|
|
1393
|
-
state: reduction.state,
|
|
1394
|
-
delivery,
|
|
1395
|
-
blockProcessorWhile: (work) => {
|
|
1396
|
-
const attempt = eventChain.then(() => this.#keepAliveBackedWork(work).catch((error) => {
|
|
1397
|
-
console.error(`stream processor blocked work failed (${this.hooks.contract.slug})`, error);
|
|
1398
|
-
throw error;
|
|
1399
|
-
}));
|
|
1400
|
-
eventChain = attempt;
|
|
1401
|
-
startedBlockers.push(attempt);
|
|
1402
|
-
},
|
|
1403
|
-
runInBackground: (work) => this.#runInBackground(work),
|
|
1404
|
-
append: (...input) => this.hooks.append({
|
|
1405
|
-
streamId: batch.streamId,
|
|
1406
|
-
whileProcessing
|
|
1407
|
-
}, input),
|
|
1408
|
-
appendTo: (path, ...input) => this.hooks.appendTo(path, {
|
|
1409
|
-
streamId: batch.streamId,
|
|
1410
|
-
whileProcessing
|
|
1411
|
-
}, input)
|
|
1412
|
-
});
|
|
1413
|
-
await eventChain;
|
|
1414
|
-
ctx.state = reduction.state;
|
|
1415
|
-
}
|
|
1416
|
-
ctx.reducedThroughOffset = event.offset;
|
|
1417
|
-
ctx.completedThroughOffset = event.offset;
|
|
1418
|
-
ctx.eventsSinceCommit += 1;
|
|
1419
|
-
ctx.uncommittedEvents.push(event);
|
|
1420
|
-
}
|
|
1421
|
-
if (batchCaughtUp && !firedCaughtUp) {
|
|
1422
|
-
const delivery = {
|
|
1423
|
-
caughtUp: true,
|
|
1424
|
-
streamId: batch.streamId
|
|
1425
|
-
};
|
|
1426
|
-
let caughtUpChain = Promise.resolve();
|
|
1427
|
-
this.hooks.processEvent({
|
|
1428
|
-
event: null,
|
|
1429
|
-
previousState: ctx.state,
|
|
1430
|
-
state: ctx.state,
|
|
1431
|
-
delivery,
|
|
1432
|
-
blockProcessorWhile: (work) => {
|
|
1433
|
-
const attempt = caughtUpChain.then(() => this.#keepAliveBackedWork(work).catch((error) => {
|
|
1434
|
-
console.error(`stream processor blocked work failed (${this.hooks.contract.slug})`, error);
|
|
1435
|
-
throw error;
|
|
1436
|
-
}));
|
|
1437
|
-
caughtUpChain = attempt;
|
|
1438
|
-
startedBlockers.push(attempt);
|
|
1439
|
-
},
|
|
1440
|
-
runInBackground: (work) => this.#runInBackground(work),
|
|
1441
|
-
append: (...input) => this.hooks.append({ streamId: batch.streamId }, input),
|
|
1442
|
-
appendTo: (path, ...input) => this.hooks.appendTo(path, { streamId: batch.streamId }, input)
|
|
1443
|
-
});
|
|
1444
|
-
await caughtUpChain;
|
|
1445
|
-
}
|
|
1446
|
-
ctx.reducedThroughOffset = batchScannedThroughOffset;
|
|
1447
|
-
ctx.completedThroughOffset = batchScannedThroughOffset;
|
|
1448
|
-
} catch (error) {
|
|
1449
|
-
await Promise.allSettled(startedBlockers);
|
|
1450
|
-
throw error;
|
|
1451
|
-
}
|
|
1452
|
-
if (ctx.eventsSinceCommit > 0 || batchScannedThroughOffset > committedThroughOffset) await this.#commitBatchContext(ctx);
|
|
1453
|
-
}
|
|
1454
|
-
/**
|
|
1455
|
-
* Persist the batch context, THEN advance the published cursor, resolve
|
|
1456
|
-
* waiters, and flush parse-failure diagnostics for the covered events.
|
|
1457
|
-
*
|
|
1458
|
-
* Persist-before-advance is load-bearing (the legacy #ingest's ordering):
|
|
1459
|
-
* if the durable write fails, the batch must stay retryable — the redelivered
|
|
1460
|
-
* batch re-reduces from the OLD published state and retries the write.
|
|
1461
|
-
* Advancing in-memory first would make the retry a silent no-op (every
|
|
1462
|
-
* event filtered out, nothing re-saved), losing the batch durably.
|
|
1463
|
-
*/
|
|
1464
|
-
async #commitBatchContext(ctx) {
|
|
1465
|
-
const streamId = this.#requireProgress().streamId;
|
|
1466
|
-
const next = {
|
|
1467
|
-
streamId,
|
|
1468
|
-
reduction: {
|
|
1469
|
-
reducerVersion: this.hooks.contract.version,
|
|
1470
|
-
reducedThroughOffset: ctx.reducedThroughOffset,
|
|
1471
|
-
state: ctx.state
|
|
1472
|
-
},
|
|
1473
|
-
processing: {
|
|
1474
|
-
acknowledgedThroughOffset: ctx.completedThroughOffset,
|
|
1475
|
-
cursorRevision: ctx.revision
|
|
1476
|
-
}
|
|
1477
|
-
};
|
|
1478
|
-
const previousCommittedState = this.#progress?.reduction.state;
|
|
1479
|
-
await this.#commit(next, {
|
|
1480
|
-
expectedCursorRevision: ctx.revision,
|
|
1481
|
-
expectedStreamId: streamId
|
|
1482
|
-
});
|
|
1483
|
-
this.#progress = next;
|
|
1484
|
-
ctx.eventsSinceCommit = 0;
|
|
1485
|
-
const committedEvents = ctx.uncommittedEvents.splice(0);
|
|
1486
|
-
const committedFailures = ctx.uncommittedParseFailures.splice(0);
|
|
1487
|
-
if (committedEvents.length > 0) {
|
|
1488
|
-
const newestEventCreatedAtMs = Date.parse(committedEvents.at(-1).createdAt);
|
|
1489
|
-
this.hooks.noteBatchIngested({
|
|
1490
|
-
ingestedThroughOffset: next.processing.acknowledgedThroughOffset,
|
|
1491
|
-
ingestedOffsets: committedEvents.map((event) => event.offset),
|
|
1492
|
-
...Number.isFinite(newestEventCreatedAtMs) && { newestEventCreatedAtMs },
|
|
1493
|
-
ingestStartedAtMs: ctx.ingestStartedAtMs,
|
|
1494
|
-
atMs: this.now()
|
|
1495
|
-
});
|
|
1496
|
-
}
|
|
1497
|
-
if (!Object.is(previousCommittedState, next.reduction.state)) this.#notifyStateChange({
|
|
1498
|
-
offset: next.reduction.reducedThroughOffset,
|
|
1499
|
-
state: next.reduction.state
|
|
1500
|
-
});
|
|
1501
|
-
this.#resolveEventWaiters(committedEvents, next.processing.acknowledgedThroughOffset);
|
|
1502
|
-
for (const { event, error } of committedFailures) {
|
|
1503
|
-
const message = `stream processor "${this.hooks.contract.slug}" skipped event at offset ${event.offset} ("${event.type}"): it fails the contract's schema`;
|
|
1504
|
-
console.error(message, error);
|
|
1505
|
-
this.#runInBackground(() => this.stream.appendIfStreamId({
|
|
1506
|
-
streamId,
|
|
1507
|
-
events: [{
|
|
1508
|
-
type: "events.iterate.com/stream/error-occurred",
|
|
1509
|
-
idempotencyKey: this.hooks.idempotencyKey("event-parse-failed", event),
|
|
1510
|
-
source: { processor: this.hooks.processorStamp(streamId, event) },
|
|
1511
|
-
payload: {
|
|
1512
|
-
message,
|
|
1513
|
-
error: {
|
|
1514
|
-
name: error.name,
|
|
1515
|
-
message: error.message
|
|
1516
|
-
}
|
|
1517
|
-
}
|
|
1518
|
-
}]
|
|
1519
|
-
}));
|
|
1520
|
-
}
|
|
1521
|
-
}
|
|
1522
|
-
/**
|
|
1523
|
-
* Return the hosted source's authoritative effect cursor without reading
|
|
1524
|
-
* that source. Fresh/recreated lifetimes still land their durable fence
|
|
1525
|
-
* before the checkpoint escapes; an existing lifetime leaves its disposable
|
|
1526
|
-
* reduction cache unloaded until the one-way delivery callback.
|
|
1527
|
-
*/
|
|
1528
|
-
async #prepareHostedCheckpoint(streamId) {
|
|
1529
|
-
if (this.#hasLoaded && this.#progress?.streamId === streamId) return this.#progress.processing.acknowledgedThroughOffset;
|
|
1530
|
-
if (this.#hasLoaded) {
|
|
1531
|
-
this.#hasLoaded = false;
|
|
1532
|
-
this.#loaded = void 0;
|
|
1533
|
-
this.#loadingStreamId = void 0;
|
|
1534
|
-
}
|
|
1535
|
-
const persisted = await this.durability?.progress.read();
|
|
1536
|
-
if (persisted === void 0) {
|
|
1537
|
-
const fresh = this.#freshProgress(streamId, 0);
|
|
1538
|
-
await this.#commit(fresh, {
|
|
1539
|
-
expectedCursorRevision: 0,
|
|
1540
|
-
expectedStreamId: void 0
|
|
1541
|
-
});
|
|
1542
|
-
this.#progress = fresh;
|
|
1543
|
-
this.#hasLoaded = true;
|
|
1544
|
-
return 0;
|
|
1545
|
-
}
|
|
1546
|
-
if (persisted.streamId === streamId) return persisted.processing.acknowledgedThroughOffset;
|
|
1547
|
-
const replaceForStream = this.durability?.progress.replaceForStream;
|
|
1548
|
-
if (replaceForStream === void 0) throw new Error(`stream processor "${this.hooks.contract.slug}" progress belongs to stream ID ${persisted.streamId}, but the current stream ID is ${streamId}; this durability backend must reset its related projections before reopening`);
|
|
1549
|
-
const fresh = this.#freshProgress(streamId, persisted.processing.cursorRevision + 1);
|
|
1550
|
-
await replaceForStream(fresh, {
|
|
1551
|
-
expectedCursorRevision: persisted.processing.cursorRevision,
|
|
1552
|
-
expectedStreamId: persisted.streamId
|
|
1553
|
-
});
|
|
1554
|
-
this.#progress = fresh;
|
|
1555
|
-
this.#hasLoaded = true;
|
|
1556
|
-
this.#notifyStateChange({
|
|
1557
|
-
offset: 0,
|
|
1558
|
-
state: fresh.reduction.state
|
|
1559
|
-
});
|
|
1560
|
-
return 0;
|
|
1561
|
-
}
|
|
1562
|
-
#load(streamId) {
|
|
1563
|
-
return this.#loadWithStreamReplacement(streamId, true);
|
|
1564
|
-
}
|
|
1565
|
-
#loadPreparedStream(streamId) {
|
|
1566
|
-
if (this.#hasLoaded) return Promise.resolve();
|
|
1567
|
-
return this.#loadWithStreamReplacement(streamId, false);
|
|
1568
|
-
}
|
|
1569
|
-
#loadWithStreamReplacement(streamId, replaceMismatchedStream) {
|
|
1570
|
-
if (this.#hasLoaded && this.#progress?.streamId === streamId) return Promise.resolve();
|
|
1571
|
-
if (this.#hasLoaded) {
|
|
1572
|
-
this.#hasLoaded = false;
|
|
1573
|
-
this.#loaded = void 0;
|
|
1574
|
-
this.#loadingStreamId = void 0;
|
|
1575
|
-
}
|
|
1576
|
-
if (this.#loaded !== void 0) {
|
|
1577
|
-
if (this.#loadingStreamId === streamId) return this.#loaded;
|
|
1578
|
-
return this.#loaded.then(() => this.#loadWithStreamReplacement(streamId, replaceMismatchedStream));
|
|
1579
|
-
}
|
|
1580
|
-
this.#loadingStreamId = streamId;
|
|
1581
|
-
this.#loaded = this.#loadOnce(streamId, replaceMismatchedStream).catch((error) => {
|
|
1582
|
-
this.#loaded = void 0;
|
|
1583
|
-
this.#loadingStreamId = void 0;
|
|
1584
|
-
throw error;
|
|
1585
|
-
});
|
|
1586
|
-
return this.#loaded;
|
|
1587
|
-
}
|
|
1588
|
-
#freshProgress(streamId, cursorRevision) {
|
|
1589
|
-
return {
|
|
1590
|
-
streamId,
|
|
1591
|
-
reduction: {
|
|
1592
|
-
reducerVersion: this.hooks.contract.version,
|
|
1593
|
-
reducedThroughOffset: 0,
|
|
1594
|
-
state: this.hooks.initialState()
|
|
1595
|
-
},
|
|
1596
|
-
processing: {
|
|
1597
|
-
acknowledgedThroughOffset: 0,
|
|
1598
|
-
cursorRevision
|
|
1599
|
-
}
|
|
1600
|
-
};
|
|
1601
|
-
}
|
|
1602
|
-
async #loadOnce(streamId, replaceMismatchedStream) {
|
|
1603
|
-
const persisted = await this.durability?.progress.read();
|
|
1604
|
-
if (persisted === void 0) {
|
|
1605
|
-
const fresh = this.#freshProgress(streamId, 0);
|
|
1606
|
-
await this.#commit(fresh, {
|
|
1607
|
-
expectedCursorRevision: 0,
|
|
1608
|
-
expectedStreamId: void 0
|
|
1609
|
-
});
|
|
1610
|
-
this.#progress = fresh;
|
|
1611
|
-
this.#hasLoaded = true;
|
|
1612
|
-
return;
|
|
1613
|
-
}
|
|
1614
|
-
if (persisted.streamId !== streamId) {
|
|
1615
|
-
if (!replaceMismatchedStream) throw new Error(`hosted callback for stream ID ${streamId} is stale; processor progress belongs to ${persisted.streamId}`);
|
|
1616
|
-
const replaceForStream = this.durability?.progress.replaceForStream;
|
|
1617
|
-
if (replaceForStream === void 0) throw new Error(`stream processor "${this.hooks.contract.slug}" progress belongs to stream ID ${persisted.streamId}, but the current stream ID is ${streamId}; this durability backend must reset its related projections before reopening`);
|
|
1618
|
-
const fresh = this.#freshProgress(streamId, persisted.processing.cursorRevision + 1);
|
|
1619
|
-
await replaceForStream(fresh, {
|
|
1620
|
-
expectedCursorRevision: persisted.processing.cursorRevision,
|
|
1621
|
-
expectedStreamId: persisted.streamId
|
|
1622
|
-
});
|
|
1623
|
-
this.#progress = fresh;
|
|
1624
|
-
this.#hasLoaded = true;
|
|
1625
|
-
this.#notifyStateChange({
|
|
1626
|
-
offset: 0,
|
|
1627
|
-
state: fresh.reduction.state
|
|
1628
|
-
});
|
|
1629
|
-
return;
|
|
1630
|
-
}
|
|
1631
|
-
const acknowledged = persisted.processing.acknowledgedThroughOffset;
|
|
1632
|
-
const parsed = this.hooks.parseState(persisted.reduction.state);
|
|
1633
|
-
const reducedAheadOfAck = persisted.reduction.reducedThroughOffset > acknowledged;
|
|
1634
|
-
if (persisted.reduction.reducerVersion === this.hooks.contract.version && parsed.success && !reducedAheadOfAck) {
|
|
1635
|
-
let reduction = {
|
|
1636
|
-
...persisted.reduction,
|
|
1637
|
-
state: parsed.state
|
|
1638
|
-
};
|
|
1639
|
-
if (reduction.reducedThroughOffset < acknowledged) {
|
|
1640
|
-
reduction = await this.#rebuildReduction(streamId, acknowledged, {
|
|
1641
|
-
state: reduction.state,
|
|
1642
|
-
reducedThroughOffset: reduction.reducedThroughOffset
|
|
1643
|
-
});
|
|
1644
|
-
const progress = {
|
|
1645
|
-
streamId,
|
|
1646
|
-
reduction,
|
|
1647
|
-
processing: persisted.processing
|
|
1648
|
-
};
|
|
1649
|
-
await this.#commit(progress, {
|
|
1650
|
-
expectedCursorRevision: persisted.processing.cursorRevision,
|
|
1651
|
-
expectedStreamId: streamId
|
|
1652
|
-
});
|
|
1653
|
-
this.#progress = progress;
|
|
1654
|
-
this.#hasLoaded = true;
|
|
1655
|
-
return;
|
|
1656
|
-
}
|
|
1657
|
-
this.#progress = {
|
|
1658
|
-
streamId,
|
|
1659
|
-
reduction,
|
|
1660
|
-
processing: persisted.processing
|
|
1661
|
-
};
|
|
1662
|
-
this.#hasLoaded = true;
|
|
1663
|
-
return;
|
|
1664
|
-
}
|
|
1665
|
-
console.warn(reducedAheadOfAck ? `stream processor "${this.hooks.contract.slug}" persisted reduction cursor (${persisted.reduction.reducedThroughOffset}) is AHEAD of the acknowledged cursor (${acknowledged}) — an invalid record; discarding the fold and refolding reduce-only through the acknowledgement` : `stream processor "${this.hooks.contract.slug}" reduction cache is stale (persisted reducerVersion "${persisted.reduction.reducerVersion}", current "${this.hooks.contract.version}", state ${parsed.success ? "valid" : "invalid"}); refolding reduce-only through acknowledged offset ${acknowledged}`);
|
|
1666
|
-
const progress = {
|
|
1667
|
-
streamId,
|
|
1668
|
-
reduction: await this.#rebuildReduction(streamId, acknowledged),
|
|
1669
|
-
processing: persisted.processing
|
|
1670
|
-
};
|
|
1671
|
-
await this.#commit(progress, {
|
|
1672
|
-
expectedCursorRevision: persisted.processing.cursorRevision,
|
|
1673
|
-
expectedStreamId: streamId
|
|
1674
|
-
});
|
|
1675
|
-
this.#progress = progress;
|
|
1676
|
-
this.#hasLoaded = true;
|
|
1677
|
-
}
|
|
1678
|
-
/** Rebuild the fold through `throughOffset`, reduce ONLY, paged — from
|
|
1679
|
-
* offset 0 by default, or extending `from` (a valid persisted fold that
|
|
1680
|
-
* LAGS the target, so only the gap's events are read). */
|
|
1681
|
-
async #rebuildReduction(streamId, throughOffset, from) {
|
|
1682
|
-
let state = from?.state ?? this.hooks.initialState();
|
|
1683
|
-
let afterOffset = from?.reducedThroughOffset ?? 0;
|
|
1684
|
-
if (throughOffset > afterOffset) for (;;) {
|
|
1685
|
-
const page = await this.stream.getEventPage({
|
|
1686
|
-
afterOffset,
|
|
1687
|
-
beforeOffset: throughOffset + 1,
|
|
1688
|
-
byteLimit: MAX_STREAM_EVENT_READ_BYTE_LIMIT,
|
|
1689
|
-
limit: this.readPageSize
|
|
1690
|
-
});
|
|
1691
|
-
this.#assertReadStreamId(page.streamId, streamId);
|
|
1692
|
-
if (page.events.length === 0) break;
|
|
1693
|
-
for (const event of page.events) {
|
|
1694
|
-
if (event.offset > throughOffset) continue;
|
|
1695
|
-
const reduction = this.hooks.reduceRawEvent({
|
|
1696
|
-
event,
|
|
1697
|
-
state
|
|
1698
|
-
});
|
|
1699
|
-
if (reduction !== void 0 && !("parseError" in reduction)) state = reduction.state;
|
|
1700
|
-
}
|
|
1701
|
-
afterOffset = page.events.at(-1).offset;
|
|
1702
|
-
}
|
|
1703
|
-
return {
|
|
1704
|
-
reducerVersion: this.hooks.contract.version,
|
|
1705
|
-
reducedThroughOffset: throughOffset,
|
|
1706
|
-
state
|
|
1707
|
-
};
|
|
1708
|
-
}
|
|
1709
|
-
async #commit(progress, opts) {
|
|
1710
|
-
if (this.durability === void 0) return;
|
|
1711
|
-
await this.durability.progress.commit(progress, opts);
|
|
1712
|
-
}
|
|
1713
|
-
/**
|
|
1714
|
-
* Re-run `reduce` + `processEvent` from the acknowledged cursor by
|
|
1715
|
-
* reading the journal itself — catch-up cannot rely on a callback whose
|
|
1716
|
-
* starting offset was fixed when it opened. One page of lookahead means
|
|
1717
|
-
* every non-final batch carries a streamMaxOffset past its own last event (the
|
|
1718
|
-
* `caughtUp` flag appears only on the genuinely final page), matching the
|
|
1719
|
-
* host's catch-up.
|
|
1720
|
-
*/
|
|
1721
|
-
async #selfCatchUp() {
|
|
1722
|
-
const streamId = this.#requireProgress().streamId;
|
|
1723
|
-
let scannedAfterOffset = this.#requireProgress().processing.acknowledgedThroughOffset;
|
|
1724
|
-
let targetOffset;
|
|
1725
|
-
for (;;) {
|
|
1726
|
-
const page = await this.stream.getEventPage({
|
|
1727
|
-
afterOffset: scannedAfterOffset,
|
|
1728
|
-
...targetOffset === void 0 ? {} : { beforeOffset: targetOffset + 1 },
|
|
1729
|
-
byteLimit: MAX_STREAM_EVENT_READ_BYTE_LIMIT,
|
|
1730
|
-
limit: this.readPageSize
|
|
1731
|
-
});
|
|
1732
|
-
this.#assertReadStreamId(page.streamId, streamId);
|
|
1733
|
-
targetOffset ??= page.streamMaxOffset;
|
|
1734
|
-
const lastEventOffset = page.events.at(-1)?.offset;
|
|
1735
|
-
const isFinalPage = lastEventOffset === void 0 || lastEventOffset >= targetOffset;
|
|
1736
|
-
const scannedThroughOffset = isFinalPage ? targetOffset : lastEventOffset;
|
|
1737
|
-
if (scannedThroughOffset <= scannedAfterOffset) return;
|
|
1738
|
-
await this.#processBatch({
|
|
1739
|
-
streamId,
|
|
1740
|
-
events: page.events,
|
|
1741
|
-
scannedAfterOffset,
|
|
1742
|
-
scannedThroughOffset,
|
|
1743
|
-
streamMaxOffset: targetOffset
|
|
1744
|
-
});
|
|
1745
|
-
scannedAfterOffset = scannedThroughOffset;
|
|
1746
|
-
if (isFinalPage) return;
|
|
1747
|
-
}
|
|
1748
|
-
}
|
|
1749
|
-
async #readCurrentStreamId(expectedStreamId) {
|
|
1750
|
-
const page = await this.stream.getEventPage({
|
|
1751
|
-
afterOffset: Number.MAX_SAFE_INTEGER,
|
|
1752
|
-
limit: 1
|
|
1753
|
-
});
|
|
1754
|
-
if (expectedStreamId !== void 0 && page.streamId !== expectedStreamId) throw new Error(`stream processor "${this.hooks.contract.slug}" was opened for stream ID ${expectedStreamId}, but the stream at this path is ${page.streamId}`);
|
|
1755
|
-
return page.streamId;
|
|
1756
|
-
}
|
|
1757
|
-
#assertReadStreamId(actualStreamId, expectedStreamId) {
|
|
1758
|
-
if (actualStreamId === expectedStreamId) return;
|
|
1759
|
-
throw new Error(`stream processor "${this.hooks.contract.slug}" stream ID changed during a read (${expectedStreamId} -> ${actualStreamId})`);
|
|
1760
|
-
}
|
|
1761
|
-
/** Fire-and-forget async work backed by the keepalive, with failures logged. */
|
|
1762
|
-
#runInBackground(work) {
|
|
1763
|
-
this.#keepAliveBackedWork(work).catch((error) => {
|
|
1764
|
-
console.error("stream processor runner background work failed", error);
|
|
1765
|
-
});
|
|
1766
|
-
}
|
|
1767
|
-
/**
|
|
1768
|
-
* Route registered work through the recovery adapter's keepalive when
|
|
1769
|
-
* present (both `blockProcessorWhile` and `runInBackground` ride it — "the
|
|
1770
|
-
* DO died owing work" must equal "the alarm was armed"), else through the
|
|
1771
|
-
* plain `keepAlive` hook, else run directly.
|
|
1772
|
-
*/
|
|
1773
|
-
async #keepAliveBackedWork(work) {
|
|
1774
|
-
return await awaitKeepAliveBacked(this.durability?.recovery?.keepAliveWhile ?? this.keepAlive, work);
|
|
1775
|
-
}
|
|
1776
|
-
/** Serialize batches + self-pulls; the chain swallows each entry's
|
|
1777
|
-
* failure so one failed batch never wedges the entries behind it. */
|
|
1778
|
-
#enqueue(work) {
|
|
1779
|
-
const next = this.#chain.then(() => {
|
|
1780
|
-
this.#assertNotDisposed();
|
|
1781
|
-
return work();
|
|
1782
|
-
});
|
|
1783
|
-
this.#chain = next.then(() => void 0, () => void 0);
|
|
1784
|
-
return next;
|
|
1785
|
-
}
|
|
1786
|
-
#assertNotDisposed() {
|
|
1787
|
-
if (this.#disposed) throw new Error(`StreamProcessorRunner for "${this.hooks.contract.slug}" is disposed; it accepts no new work`);
|
|
1788
|
-
}
|
|
1789
|
-
#requireProgress() {
|
|
1790
|
-
if (this.#progress === void 0) throw new Error("StreamProcessorRunner progress read before load — this is a runner bug");
|
|
1791
|
-
return this.#progress;
|
|
1792
|
-
}
|
|
1793
|
-
#notifyStateChange(snapshot) {
|
|
1794
|
-
for (const observer of [...this.#stateChangeObservers]) try {
|
|
1795
|
-
observer(snapshot);
|
|
1796
|
-
} catch (error) {
|
|
1797
|
-
console.error("stream processor runner state-change observer failed", error);
|
|
1798
|
-
}
|
|
1799
|
-
}
|
|
1800
|
-
#registerEventWaiter(match, opts) {
|
|
1801
|
-
let waiter;
|
|
1802
|
-
return {
|
|
1803
|
-
promise: new Promise((resolve, reject) => {
|
|
1804
|
-
waiter = {
|
|
1805
|
-
...match,
|
|
1806
|
-
reject,
|
|
1807
|
-
resolve,
|
|
1808
|
-
signal: opts.signal
|
|
1809
|
-
};
|
|
1810
|
-
this.#eventWaiters.add(waiter);
|
|
1811
|
-
if (opts.timeoutMs !== void 0) waiter.timer = setTimeout(() => {
|
|
1812
|
-
this.#settleEventWaiter(waiter, { error: /* @__PURE__ */ new Error(`waitUntilEvent timed out after ${opts.timeoutMs}ms`) });
|
|
1813
|
-
}, opts.timeoutMs);
|
|
1814
|
-
if (opts.signal !== void 0) {
|
|
1815
|
-
waiter.abortListener = () => {
|
|
1816
|
-
this.#settleEventWaiter(waiter, { error: abortReason(opts.signal) });
|
|
1817
|
-
};
|
|
1818
|
-
opts.signal.addEventListener("abort", waiter.abortListener, { once: true });
|
|
1819
|
-
if (opts.signal.aborted) waiter.abortListener();
|
|
1820
|
-
}
|
|
1821
|
-
}),
|
|
1822
|
-
reject: (error) => this.#settleEventWaiter(waiter, { error })
|
|
1823
|
-
};
|
|
1824
|
-
}
|
|
1825
|
-
#settleEventWaiter(waiter, outcome) {
|
|
1826
|
-
if (!this.#eventWaiters.delete(waiter)) return;
|
|
1827
|
-
if (waiter.timer !== void 0) clearTimeout(waiter.timer);
|
|
1828
|
-
if (waiter.signal !== void 0 && waiter.abortListener !== void 0) waiter.signal.removeEventListener("abort", waiter.abortListener);
|
|
1829
|
-
if ("error" in outcome) waiter.reject(outcome.error);
|
|
1830
|
-
else waiter.resolve();
|
|
1831
|
-
}
|
|
1832
|
-
#resolveEventWaiters(events, acknowledgedThroughOffset) {
|
|
1833
|
-
for (const waiter of this.#eventWaiters) {
|
|
1834
|
-
let matched = false;
|
|
1835
|
-
try {
|
|
1836
|
-
matched = waiter.kind === "offset" ? acknowledgedThroughOffset >= waiter.offset : events.some(waiter.predicate);
|
|
1837
|
-
} catch (error) {
|
|
1838
|
-
this.#settleEventWaiter(waiter, { error });
|
|
1839
|
-
continue;
|
|
1840
|
-
}
|
|
1841
|
-
if (matched) this.#settleEventWaiter(waiter, { value: void 0 });
|
|
1842
|
-
}
|
|
1843
|
-
}
|
|
1844
|
-
};
|
|
1845
|
-
function assertProcessorEventBatch(batch) {
|
|
1846
|
-
const coordinates = [
|
|
1847
|
-
["scannedAfterOffset", batch.scannedAfterOffset],
|
|
1848
|
-
["scannedThroughOffset", batch.scannedThroughOffset],
|
|
1849
|
-
["streamMaxOffset", batch.streamMaxOffset]
|
|
1850
|
-
];
|
|
1851
|
-
for (const [name, value] of coordinates) if (!Number.isSafeInteger(value) || value < 0) throw new Error(`stream processor event batch ${name} must be a non-negative safe integer`);
|
|
1852
|
-
if (batch.scannedThroughOffset < batch.scannedAfterOffset) throw new Error(`stream processor event batch scan regressed: ${batch.scannedAfterOffset} -> ${batch.scannedThroughOffset}`);
|
|
1853
|
-
if (batch.streamMaxOffset < batch.scannedThroughOffset) throw new Error(`stream processor event batch scan ${batch.scannedThroughOffset} is ahead of stream maximum offset ${batch.streamMaxOffset}`);
|
|
1854
|
-
let previousOffset = batch.scannedAfterOffset;
|
|
1855
|
-
for (const event of batch.events) {
|
|
1856
|
-
if (!Number.isSafeInteger(event.offset) || event.offset <= previousOffset) throw new Error(`stream processor event batch events must increase strictly after scan cursor ${previousOffset}; found ${event.offset}`);
|
|
1857
|
-
if (event.offset > batch.scannedThroughOffset) throw new Error(`stream processor event batch event ${event.offset} is beyond scanned-through offset ${batch.scannedThroughOffset}`);
|
|
1858
|
-
previousOffset = event.offset;
|
|
1859
|
-
}
|
|
1860
|
-
}
|
|
1861
|
-
function abortReason(signal) {
|
|
1862
|
-
return signal.reason ?? /* @__PURE__ */ new Error("waitUntilEvent aborted");
|
|
1863
|
-
}
|
|
1864
|
-
//#endregion
|
|
1865
|
-
//#region src/processors/stream-processor-keepalive.ts
|
|
1866
|
-
/** How far ahead of in-flight work the alarm is scheduled. Bounds post-eviction
|
|
1867
|
-
* revival latency; a deploy mid-agent-turn recovers within roughly this. */
|
|
1868
|
-
const KEEPALIVE_ALARM_LEAD_MS = 1e4;
|
|
1869
|
-
/**
|
|
1870
|
-
* Floor between redundant re-assertions of an already-sufficient alarm.
|
|
1871
|
-
* See #ensureArmedForWork: the re-assert exists to heal a lost platform
|
|
1872
|
-
* write, and healing within a fraction of the lead is as good as instantly.
|
|
1873
|
-
*/
|
|
1874
|
-
const KEEPALIVE_REASSERT_MIN_INTERVAL_MS = 2500;
|
|
1875
|
-
/** Revival backoff by attempt number (1-based); past the table, the plateau. */
|
|
1876
|
-
const REVIVAL_BACKOFF_MS = [
|
|
1877
|
-
1e4,
|
|
1878
|
-
6e4,
|
|
1879
|
-
5 * 6e4,
|
|
1880
|
-
30 * 6e4
|
|
1881
|
-
];
|
|
1882
|
-
const REVIVAL_BACKOFF_PLATEAU_MS = 360 * 6e4;
|
|
1883
|
-
/** Attempts before the crash-loop evidence fact is appended (once per version). */
|
|
1884
|
-
const CRASH_LOOP_EVIDENCE_THRESHOLD = 3;
|
|
1885
|
-
/**
|
|
1886
|
-
* Consecutive busy fires with NO settlement in between before the window is
|
|
1887
|
-
* treated as wedged (a hung promise nothing will ever settle — e.g. a socket
|
|
1888
|
-
* with no deadline). 90 fires ≈ 15 minutes at the lead, comfortably past the
|
|
1889
|
-
* longest legitimate tracked work (the providers' 10-minute deadlines), so
|
|
1890
|
-
* legit work never trips it while a wedge decays into the revival backoff
|
|
1891
|
-
* instead of re-arming every lead interval forever.
|
|
1892
|
-
*/
|
|
1893
|
-
const MAX_CONSECUTIVE_BUSY_REFIRES = 90;
|
|
1894
|
-
function revivalBackoffMs(revivals) {
|
|
1895
|
-
return REVIVAL_BACKOFF_MS[revivals - 1] ?? 216e5;
|
|
1896
|
-
}
|
|
1897
|
-
const FRESH_RECORD = {
|
|
1898
|
-
revivals: 0,
|
|
1899
|
-
lastRevivalAt: 0,
|
|
1900
|
-
armedAtMs: null
|
|
1901
|
-
};
|
|
1902
|
-
var ProcessorKeepalive = class {
|
|
1903
|
-
#hooks;
|
|
1904
|
-
#inFlight = 0;
|
|
1905
|
-
/** Any tracked work settled successfully since the alarm was armed. A
|
|
1906
|
-
* successful revival pass also sets this — the pass IS settled work. */
|
|
1907
|
-
#sawCleanSettle = false;
|
|
1908
|
-
/** Any tracked work failed since the alarm was armed. Failures mean an
|
|
1909
|
-
* obligation may be unsettled (a debounce append that lost its stream), so
|
|
1910
|
-
* the next fire revives instead of disarming. */
|
|
1911
|
-
#sawFailure = false;
|
|
1912
|
-
/** Suppresses arm-earlier while the revival pass runs (see module doc). */
|
|
1913
|
-
#reviving = false;
|
|
1914
|
-
/** Consecutive busy fires without any settlement (wedged-work detector). */
|
|
1915
|
-
#busyRefires = 0;
|
|
1916
|
-
constructor(hooks) {
|
|
1917
|
-
this.#hooks = hooks;
|
|
1918
|
-
}
|
|
1919
|
-
/**
|
|
1920
|
-
* The keepalive's current alarm desire, for the host's slice merge. Read
|
|
1921
|
-
* straight from the durable record (synchronous DO KV) — a separate
|
|
1922
|
-
* in-memory copy would be one more thing to drift after an eviction, and
|
|
1923
|
-
* stale copied state is exactly the failure class this module hunts.
|
|
1924
|
-
*/
|
|
1925
|
-
/** When an already-armed desire was last re-asserted; in-memory on purpose
|
|
1926
|
-
* (a fresh incarnation should re-assert on its first tracked work). */
|
|
1927
|
-
#lastReassertAtMs = 0;
|
|
1928
|
-
get armedAtMs() {
|
|
1929
|
-
return this.#hooks.readRecord()?.armedAtMs ?? null;
|
|
1930
|
-
}
|
|
1931
|
-
/**
|
|
1932
|
-
* Register one unit of in-flight work. Every registered work closure —
|
|
1933
|
-
* blocking and background alike — rides through here, so "the DO died owing
|
|
1934
|
-
* work" is exactly "the DO died with the alarm armed".
|
|
1935
|
-
*/
|
|
1936
|
-
track(work) {
|
|
1937
|
-
this.#inFlight += 1;
|
|
1938
|
-
this.#ensureArmedForWork();
|
|
1939
|
-
this.#hooks.keepAlive(work.then(() => {
|
|
1940
|
-
this.#inFlight -= 1;
|
|
1941
|
-
this.#sawCleanSettle = true;
|
|
1942
|
-
this.#busyRefires = 0;
|
|
1943
|
-
}, () => {
|
|
1944
|
-
this.#inFlight -= 1;
|
|
1945
|
-
this.#sawFailure = true;
|
|
1946
|
-
this.#busyRefires = 0;
|
|
1947
|
-
}));
|
|
1948
|
-
}
|
|
1949
|
-
/**
|
|
1950
|
-
* The DO alarm handler body. The shared alarm may fire for another
|
|
1951
|
-
* subsystem's slice (the scheduler's), so this self-gates on the persisted
|
|
1952
|
-
* armed time and does nothing when the fire is not the keepalive's.
|
|
1953
|
-
*/
|
|
1954
|
-
async onAlarm() {
|
|
1955
|
-
const now = this.#hooks.now();
|
|
1956
|
-
const armedAt = this.armedAtMs;
|
|
1957
|
-
if (armedAt === null || now < armedAt) return "not_due";
|
|
1958
|
-
if (this.#inFlight > 0 || this.#reviving) {
|
|
1959
|
-
this.#busyRefires += 1;
|
|
1960
|
-
if (this.#busyRefires < 90) {
|
|
1961
|
-
this.#arm(now + KEEPALIVE_ALARM_LEAD_MS);
|
|
1962
|
-
return "busy_rearmed";
|
|
1963
|
-
}
|
|
1964
|
-
if (this.#reviving) {
|
|
1965
|
-
this.#arm(now + REVIVAL_BACKOFF_PLATEAU_MS);
|
|
1966
|
-
return "revival_hung_backoff";
|
|
1967
|
-
}
|
|
1968
|
-
}
|
|
1969
|
-
if (this.#inFlight === 0 && this.#sawCleanSettle && !this.#sawFailure) {
|
|
1970
|
-
this.#sawCleanSettle = false;
|
|
1971
|
-
this.#disarmAndReset();
|
|
1972
|
-
return "clean_disarmed";
|
|
1973
|
-
}
|
|
1974
|
-
return await this.#revive(now);
|
|
1975
|
-
}
|
|
1976
|
-
async #revive(now) {
|
|
1977
|
-
const previous = this.#hooks.readRecord();
|
|
1978
|
-
const priorRevivals = previous === void 0 || previous.version !== this.#hooks.version ? 0 : previous.revivals;
|
|
1979
|
-
const record = {
|
|
1980
|
-
revivals: priorRevivals + 1,
|
|
1981
|
-
lastRevivalAt: now,
|
|
1982
|
-
version: this.#hooks.version,
|
|
1983
|
-
armedAtMs: now + revivalBackoffMs(priorRevivals + 1)
|
|
1984
|
-
};
|
|
1985
|
-
this.#hooks.writeRecord(record);
|
|
1986
|
-
this.#hooks.armAlarm(record.armedAtMs);
|
|
1987
|
-
if (record.revivals === CRASH_LOOP_EVIDENCE_THRESHOLD) this.#hooks.appendFact({
|
|
1988
|
-
type: "events.iterate.com/stream/error-occurred",
|
|
1989
|
-
idempotencyKey: `processor-host-crash-loop:${record.version}`,
|
|
1990
|
-
payload: { message: `processor host revival has failed ${record.revivals} consecutive times on version ${record.version}; backing off (plateau ${REVIVAL_BACKOFF_PLATEAU_MS / 6e4}m). A deploy resets the budget.` }
|
|
1991
|
-
});
|
|
1992
|
-
this.#sawFailure = false;
|
|
1993
|
-
this.#sawCleanSettle = false;
|
|
1994
|
-
this.#reviving = true;
|
|
1995
|
-
try {
|
|
1996
|
-
await this.#hooks.revive(record);
|
|
1997
|
-
this.#sawCleanSettle = true;
|
|
1998
|
-
if (!(this.#inFlight > 0 && this.#busyRefires >= 90)) this.#arm(this.#hooks.now() + KEEPALIVE_ALARM_LEAD_MS);
|
|
1999
|
-
return "revived";
|
|
2000
|
-
} catch (error) {
|
|
2001
|
-
if (this.#hooks.discardFailedRevival?.(error, record) === true) {
|
|
2002
|
-
this.#sawFailure = false;
|
|
2003
|
-
this.#sawCleanSettle = false;
|
|
2004
|
-
return "revival_discarded";
|
|
2005
|
-
}
|
|
2006
|
-
console.error("stream processor host revival failed; backing off", {
|
|
2007
|
-
revivals: record.revivals,
|
|
2008
|
-
nextAttemptAt: record.armedAtMs,
|
|
2009
|
-
error
|
|
2010
|
-
});
|
|
2011
|
-
this.#sawFailure = true;
|
|
2012
|
-
return "revival_failed";
|
|
2013
|
-
} finally {
|
|
2014
|
-
this.#reviving = false;
|
|
2015
|
-
}
|
|
2016
|
-
}
|
|
2017
|
-
/**
|
|
2018
|
-
* The operator's no-deploy antidote: clear the crash-loop budget and, when
|
|
2019
|
-
* a retry is owed (the record is armed), pull it in to the confirmation
|
|
2020
|
-
* lead so the next fire revives promptly on the fresh budget. Without this
|
|
2021
|
-
* the mark resets only on a quiet-clean confirmation or a version change —
|
|
2022
|
-
* a 3-strikes plateau otherwise mutes a wedged processor for six hours at
|
|
2023
|
-
* a time with a deploy as the only cure (the 2026-08-11 prod incident).
|
|
2024
|
-
*/
|
|
2025
|
-
resetBackoff() {
|
|
2026
|
-
const record = this.#hooks.readRecord();
|
|
2027
|
-
if (record === void 0) return;
|
|
2028
|
-
if (record.armedAtMs === null) {
|
|
2029
|
-
this.#hooks.writeRecord({
|
|
2030
|
-
...FRESH_RECORD,
|
|
2031
|
-
version: this.#hooks.version
|
|
2032
|
-
});
|
|
2033
|
-
return;
|
|
2034
|
-
}
|
|
2035
|
-
const atMs = this.#hooks.now() + KEEPALIVE_ALARM_LEAD_MS;
|
|
2036
|
-
this.#hooks.writeRecord({
|
|
2037
|
-
...FRESH_RECORD,
|
|
2038
|
-
version: this.#hooks.version,
|
|
2039
|
-
armedAtMs: atMs
|
|
2040
|
-
});
|
|
2041
|
-
this.#hooks.armAlarm(atMs);
|
|
2042
|
-
}
|
|
2043
|
-
/** Arm for in-flight work: move the alarm earlier, never later, and never
|
|
2044
|
-
* during a revival pass (its backoff safety net must govern). */
|
|
2045
|
-
#ensureArmedForWork() {
|
|
2046
|
-
if (this.#reviving) return;
|
|
2047
|
-
const nowMs = this.#hooks.now();
|
|
2048
|
-
const atMs = nowMs + KEEPALIVE_ALARM_LEAD_MS;
|
|
2049
|
-
const armedAt = this.armedAtMs;
|
|
2050
|
-
if (armedAt !== null && armedAt <= atMs) {
|
|
2051
|
-
if (nowMs - this.#lastReassertAtMs < KEEPALIVE_REASSERT_MIN_INTERVAL_MS) return;
|
|
2052
|
-
this.#lastReassertAtMs = nowMs;
|
|
2053
|
-
this.#hooks.armAlarm(armedAt);
|
|
2054
|
-
return;
|
|
2055
|
-
}
|
|
2056
|
-
this.#lastReassertAtMs = nowMs;
|
|
2057
|
-
this.#arm(atMs);
|
|
2058
|
-
}
|
|
2059
|
-
#arm(atMs) {
|
|
2060
|
-
const record = this.#hooks.readRecord();
|
|
2061
|
-
this.#hooks.writeRecord({
|
|
2062
|
-
...record?.version === this.#hooks.version ? record : {
|
|
2063
|
-
...FRESH_RECORD,
|
|
2064
|
-
version: this.#hooks.version
|
|
2065
|
-
},
|
|
2066
|
-
armedAtMs: atMs
|
|
2067
|
-
});
|
|
2068
|
-
this.#hooks.armAlarm(atMs);
|
|
2069
|
-
}
|
|
2070
|
-
#disarmAndReset() {
|
|
2071
|
-
const record = this.#hooks.readRecord();
|
|
2072
|
-
if (record !== void 0 && (record.revivals !== 0 || record.armedAtMs !== null)) this.#hooks.writeRecord({
|
|
2073
|
-
...FRESH_RECORD,
|
|
2074
|
-
version: this.#hooks.version
|
|
2075
|
-
});
|
|
2076
|
-
this.#hooks.armAlarm(null);
|
|
2077
|
-
}
|
|
2078
|
-
};
|
|
2079
|
-
//#endregion
|
|
2080
|
-
export { resolveStreamPath as A, StreamListItem as C, StreamRuntimeMetrics as D, MinuteBuckets as E, isStreamIdMismatchError as F, isStreamOffsetConflictError as I, isStreamReceiverUnavailableError as L, StreamIdMismatchError as M, StreamOffsetConflictError as N, ageStreamThroughputMetrics as O, StreamReceiverUnavailableError as P, streamIdMismatchMessage as R, StreamEventInput as S, LatencyRing as T, getEventInputSchema as _, revivalBackoffMs as a, MAX_COPIED_FROM_HOPS as b, awaitKeepAliveBacked as c, STREAM_PROCESSOR_REVIVED_EVENT_TYPE as d, assertObjectProcessorState as f, getConsumedEventDefinition as g, defineProcessorContract as h, REVIVAL_BACKOFF_PLATEAU_MS as i, MAX_STREAM_EVENT_READ_BYTE_LIMIT as j, pingRoundTrip as k, PLATFORM_STREAM_EVENTS as l, cachedEventSchema as m, MAX_CONSECUTIVE_BUSY_REFIRES as n, StreamProcessorRunner as o, buildEvent as p, ProcessorKeepalive as r, StreamProcessor as s, KEEPALIVE_ALARM_LEAD_MS as t, ProcessorContractAnnouncement as u, getResolvedEventDefinition as v, EventConsumptionMetrics as w, StreamEvent as x, mergeProcessorConfig as y, streamOffsetConflictMessage as z };
|
|
2081
|
-
|
|
2082
|
-
//# sourceMappingURL=stream-processor-keepalive-DAQTP6m3.mjs.map
|