experimental-a2 0.2.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/CHANGELOG.md +169 -0
- package/dist/ai-B4YhEnfw.d.ts +333 -0
- package/dist/ai-server.d.ts +48 -9
- package/dist/ai-server.js +121 -49
- package/dist/ai.d.ts +2 -303
- package/dist/ai.js +231 -86
- package/dist/cli-B3VuxoDe.js +597 -0
- package/dist/cli-bin.d.ts +1 -0
- package/dist/cli-bin.js +5 -0
- package/dist/cli.d.ts +19 -0
- package/dist/cli.js +2 -0
- package/dist/client-BrfDXQ8A.d.ts +155 -0
- package/dist/client-Bt4tAKi9.js +798 -0
- package/dist/client.d.ts +2 -90
- package/dist/client.js +1 -409
- package/dist/{contract-CG_adnu_.js → contract-48bUMgcL.js} +10 -2
- package/dist/{contract-C_3dIIEU.d.ts → contract-jIfaR085.d.ts} +62 -8
- package/dist/devtools-J_jZ2vQf.d.ts +151 -0
- package/dist/devtools-kJJaORn-.js +338 -0
- package/dist/devtools-server.browser.js +1 -1
- package/dist/devtools-server.d.ts +2 -2
- package/dist/devtools-server.js +224 -43
- package/dist/devtools.d.ts +2 -0
- package/dist/devtools.js +2 -0
- package/dist/{errors-BJRMd-h6.js → errors-BQuJpe82.js} +4 -4
- package/dist/{errors-xL_JTXsY.d.ts → errors-W6nwJ-fm.d.ts} +1 -1
- package/dist/http.d.ts +71 -13
- package/dist/http.js +302 -41
- package/dist/{idempotent-replay-BMyHrP0L.js → idempotent-replay-DuqEkYA7.js} +2 -2
- package/dist/index.d.ts +5 -5
- package/dist/index.js +2 -2
- package/dist/{inspection-E7qbD0Xj.js → inspection-DaxB5jM2.js} +2 -1
- package/dist/internal-aEotMzu_.js +209 -0
- package/dist/otel.d.ts +1 -1
- package/dist/platform-B4TnJtWu.js +32 -0
- package/dist/react.d.ts +37 -14
- package/dist/react.js +26 -15
- package/dist/scheduler-qstash.d.ts +78 -0
- package/dist/scheduler-qstash.js +499 -0
- package/dist/scheduler-task-BpzhPnRS.js +54 -0
- package/dist/{recovery-vercel.d.ts → scheduler-vercel.d.ts} +17 -24
- package/dist/scheduler-vercel.js +226 -0
- package/dist/server-CcNnFnoW.js +1405 -0
- package/dist/server-YtPq7hjw.d.ts +260 -0
- package/dist/server.d.ts +4 -155
- package/dist/server.js +2 -2
- package/dist/{log-ldf5g8Cx.d.ts → store-C3sNAaBT.d.ts} +111 -35
- package/dist/{log-yJbXUf72.js → store-N8PXxDAS.js} +1 -1
- package/dist/store-codec-DTG0Ftek.js +8 -0
- package/dist/store-memory.d.ts +11 -0
- package/dist/{log-memory.js → store-memory.js} +127 -24
- package/dist/{log-polling-6COoN60V.js → store-polling-DgrrAE3d.js} +7 -6
- package/dist/{log-postgres.d.ts → store-postgres.d.ts} +6 -6
- package/dist/{log-postgres.js → store-postgres.js} +158 -24
- package/dist/{log-redis.js → store-redis-core-DWqx3F47.js} +294 -156
- package/dist/store-redis-http.d.ts +21 -0
- package/dist/store-redis-http.js +70 -0
- package/dist/store-redis.d.ts +37 -0
- package/dist/store-redis.js +298 -0
- package/dist/{log-sqlite.d.ts → store-sqlite.d.ts} +6 -6
- package/dist/{log-sqlite.js → store-sqlite.js} +116 -22
- package/dist/{telemetry-Cso0qyHQ.d.ts → telemetry-BjYHTfh2.d.ts} +1 -1
- package/dist/testing.browser.d.ts +1 -0
- package/dist/testing.browser.js +4 -0
- package/dist/testing.d.ts +31 -0
- package/dist/testing.js +101 -0
- package/dist/wire-DCUZBUlT.js +222 -0
- package/docs/01-quickstart.mdx +4 -5
- package/docs/concepts/01-contracts.mdx +21 -17
- package/docs/concepts/02-handlers.mdx +7 -7
- package/docs/concepts/03-durability.mdx +26 -29
- package/docs/concepts/04-state.mdx +18 -21
- package/docs/guides/01-timers.mdx +154 -54
- package/docs/guides/02-cancellation.mdx +30 -4
- package/docs/guides/03-react.mdx +20 -21
- package/docs/guides/04-local-first.mdx +1 -1
- package/docs/guides/05-production.mdx +321 -60
- package/docs/guides/06-ai-agents.mdx +249 -49
- package/docs/guides/07-devtools.mdx +137 -12
- package/docs/guides/08-application-data.mdx +1 -1
- package/docs/guides/09-presence.mdx +284 -0
- package/docs/guides/10-transports.mdx +131 -0
- package/docs/index.mdx +22 -46
- package/docs/reference/01-api.mdx +751 -102
- package/docs/reference/02-errors.mdx +10 -5
- package/package.json +23 -6
- package/dist/internal-D6wNxTck.js +0 -36
- package/dist/log-memory.d.ts +0 -11
- package/dist/log-redis.d.ts +0 -31
- package/dist/recovery-vercel.js +0 -119
- package/dist/server-DJgD2YWP.js +0 -877
- package/dist/wire-BVsgR8o9.js +0 -62
package/dist/server-DJgD2YWP.js
DELETED
|
@@ -1,877 +0,0 @@
|
|
|
1
|
-
import { n as validateSync } from "./validate-XKT4FSNn.js";
|
|
2
|
-
import { n as asLogUnavailable, t as A2Error } from "./errors-BJRMd-h6.js";
|
|
3
|
-
import { i as serverInternals, t as DRAIN_TIMINGS } from "./internal-D6wNxTck.js";
|
|
4
|
-
import { n as serverInspection } from "./inspection-E7qbD0Xj.js";
|
|
5
|
-
import { t as retryableLazy } from "./retryable-lazy-DZWmHpii.js";
|
|
6
|
-
//#region src/deterministic-id.ts
|
|
7
|
-
/**
|
|
8
|
-
* Deterministic default ids for handler `session.append` calls. A handler
|
|
9
|
-
* re-run after a crash calls `session.append` again; giving
|
|
10
|
-
* each call a deterministic id — derived from the triggering event's id
|
|
11
|
-
* and the caller's stable name — makes the re-run a no-op via the log's
|
|
12
|
-
* unique index on event ids.
|
|
13
|
-
*
|
|
14
|
-
* The id is a UUIDv5-shaped SHA-1 over
|
|
15
|
-
* `(namespace, triggerEventId, name, itemOrdinal)`, computed with
|
|
16
|
-
* WebCrypto so it runs on any platform a2 core targets.
|
|
17
|
-
*/
|
|
18
|
-
const APPEND_NAMESPACE = "a2:handler-append:v2";
|
|
19
|
-
const RETURN_NAMESPACE = "a2:handler-return:v1";
|
|
20
|
-
async function deterministicId(parts) {
|
|
21
|
-
const input = parts.join("\0");
|
|
22
|
-
const digest = await crypto.subtle.digest("SHA-1", new TextEncoder().encode(input));
|
|
23
|
-
const bytes = new Uint8Array(digest).slice(0, 16);
|
|
24
|
-
bytes[6] = (bytes[6] ?? 0) & 15 | 80;
|
|
25
|
-
bytes[8] = (bytes[8] ?? 0) & 63 | 128;
|
|
26
|
-
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
27
|
-
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
|
|
28
|
-
}
|
|
29
|
-
async function deterministicEventId(triggerEventId, name, itemOrdinal) {
|
|
30
|
-
if (name.length === 0) throw new TypeError("append name must not be empty");
|
|
31
|
-
return deterministicId([
|
|
32
|
-
APPEND_NAMESPACE,
|
|
33
|
-
triggerEventId,
|
|
34
|
-
name,
|
|
35
|
-
itemOrdinal
|
|
36
|
-
]);
|
|
37
|
-
}
|
|
38
|
-
async function deterministicReturnedEventId(triggerEventId, itemOrdinal) {
|
|
39
|
-
return deterministicId([
|
|
40
|
-
RETURN_NAMESPACE,
|
|
41
|
-
triggerEventId,
|
|
42
|
-
itemOrdinal
|
|
43
|
-
]);
|
|
44
|
-
}
|
|
45
|
-
//#endregion
|
|
46
|
-
//#region src/platform.ts
|
|
47
|
-
const SYMBOL_FOR_REQ_CONTEXT = Symbol.for("@vercel/request-context");
|
|
48
|
-
function requestContext() {
|
|
49
|
-
try {
|
|
50
|
-
return Reflect.get(globalThis, SYMBOL_FOR_REQ_CONTEXT)?.get?.() ?? {};
|
|
51
|
-
} catch {
|
|
52
|
-
return {};
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
function platformWaitUntil(promise) {
|
|
56
|
-
try {
|
|
57
|
-
requestContext().waitUntil?.(promise);
|
|
58
|
-
} catch {}
|
|
59
|
-
}
|
|
60
|
-
/** The invocation's termination time in epoch ms, or null if unknowable. */
|
|
61
|
-
function invocationDeadlineMs() {
|
|
62
|
-
const deadline = requestContext().deadline;
|
|
63
|
-
if (deadline === void 0 || deadline === null) return null;
|
|
64
|
-
const ms = new Date(deadline).getTime();
|
|
65
|
-
return Number.isNaN(ms) ? null : ms;
|
|
66
|
-
}
|
|
67
|
-
//#endregion
|
|
68
|
-
//#region src/telemetry.ts
|
|
69
|
-
const NOOP_HANDLE = {
|
|
70
|
-
setAttribute: () => {},
|
|
71
|
-
recordError: () => {}
|
|
72
|
-
};
|
|
73
|
-
/** The zero-cost default when no telemetry is configured. */
|
|
74
|
-
const NOOP_TELEMETRY = { span: (_name, _attributes, fn) => fn(NOOP_HANDLE) };
|
|
75
|
-
//#endregion
|
|
76
|
-
//#region src/server.ts
|
|
77
|
-
const MAX_FAILURES = 10;
|
|
78
|
-
function createDrainSignal() {
|
|
79
|
-
const listeners = /* @__PURE__ */ new Set();
|
|
80
|
-
return {
|
|
81
|
-
version: 0,
|
|
82
|
-
closed: false,
|
|
83
|
-
notify() {
|
|
84
|
-
this.version += 1;
|
|
85
|
-
for (const listener of listeners) listener();
|
|
86
|
-
listeners.clear();
|
|
87
|
-
},
|
|
88
|
-
wait(version) {
|
|
89
|
-
if (this.version !== version) return {
|
|
90
|
-
promise: Promise.resolve(),
|
|
91
|
-
cancel: () => {}
|
|
92
|
-
};
|
|
93
|
-
let resolve;
|
|
94
|
-
const promise = new Promise((done) => {
|
|
95
|
-
resolve = done;
|
|
96
|
-
});
|
|
97
|
-
listeners.add(resolve);
|
|
98
|
-
return {
|
|
99
|
-
promise,
|
|
100
|
-
cancel: () => listeners.delete(resolve)
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
function dispatchAbortEvent(hub, event) {
|
|
106
|
-
let relevant = false;
|
|
107
|
-
for (const subscription of hub.subscriptions) {
|
|
108
|
-
if (!subscription.types.has(event.type)) continue;
|
|
109
|
-
relevant = true;
|
|
110
|
-
if (subscription.matches(event)) subscription.controller.abort();
|
|
111
|
-
}
|
|
112
|
-
if (relevant) hub.seen.set(event.index, event);
|
|
113
|
-
}
|
|
114
|
-
function pruneAbortHub(hub) {
|
|
115
|
-
let minimum = Infinity;
|
|
116
|
-
const types = /* @__PURE__ */ new Set();
|
|
117
|
-
for (const subscription of hub.subscriptions) {
|
|
118
|
-
minimum = Math.min(minimum, subscription.triggerIndex);
|
|
119
|
-
for (const type of subscription.types) types.add(type);
|
|
120
|
-
}
|
|
121
|
-
for (const [index, event] of hub.seen) if (index <= minimum || !types.has(event.type)) hub.seen.delete(index);
|
|
122
|
-
}
|
|
123
|
-
/**
|
|
124
|
-
* Deadlines tighten the claim/watchdog window; they never decide whether a
|
|
125
|
-
* handler starts. A platform timeout therefore follows the same path as any
|
|
126
|
-
* other process death: the in-flight event remains pending and is retried.
|
|
127
|
-
*/
|
|
128
|
-
function nextClaimWindow() {
|
|
129
|
-
const nowMs = Date.now();
|
|
130
|
-
const deadlineMs = invocationDeadlineMs();
|
|
131
|
-
const deadlineTtlMs = deadlineMs === null ? DRAIN_TIMINGS.claimTtlMs : deadlineMs - nowMs;
|
|
132
|
-
const deadlineCapped = deadlineMs !== null && deadlineTtlMs <= DRAIN_TIMINGS.claimTtlMs;
|
|
133
|
-
const ttlMs = Math.max(1, Math.min(DRAIN_TIMINGS.claimTtlMs, deadlineTtlMs));
|
|
134
|
-
const expiresAtMs = nowMs + ttlMs;
|
|
135
|
-
return {
|
|
136
|
-
ttlMs,
|
|
137
|
-
expiresAtMs,
|
|
138
|
-
recoveryAtMs: expiresAtMs + DRAIN_TIMINGS.recoveryGraceMs,
|
|
139
|
-
deadlineCapped
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
function recoverySlot(dueAt) {
|
|
143
|
-
return Math.ceil(dueAt / 1e3) * 1e3;
|
|
144
|
-
}
|
|
145
|
-
function currentRecoverySlot(now) {
|
|
146
|
-
return Math.floor(now / 1e3) * 1e3;
|
|
147
|
-
}
|
|
148
|
-
/**
|
|
149
|
-
* The namespace separator between machine name and session id in
|
|
150
|
-
* storage. Machines sharing one log backend (the dev-default sqlite
|
|
151
|
-
* file, a shared Postgres) must not collide on session ids; the machine
|
|
152
|
-
* name is what makes multi-machine apps unambiguous (a2-api.md §1), so
|
|
153
|
-
* it prefixes every storage key.
|
|
154
|
-
*/
|
|
155
|
-
const NS = "";
|
|
156
|
-
const devDefaultLog = retryableLazy(() => import("./log-sqlite.js").then((m) => m.sqlite()));
|
|
157
|
-
function environment() {
|
|
158
|
-
const env = typeof process === "undefined" ? void 0 : process.env?.["NODE_ENV"];
|
|
159
|
-
if (env === "test") return "test";
|
|
160
|
-
if (env === "production") return "production";
|
|
161
|
-
return "development";
|
|
162
|
-
}
|
|
163
|
-
function describeError(err) {
|
|
164
|
-
if (err instanceof Error) return err.stack ?? `${err.name}: ${err.message}`;
|
|
165
|
-
return String(err);
|
|
166
|
-
}
|
|
167
|
-
/** Implement a contract: bind its vocabulary to storage and reactions. */
|
|
168
|
-
function createServer(options) {
|
|
169
|
-
const serverContract = options?.contract;
|
|
170
|
-
if (serverContract === null || typeof serverContract !== "object" || typeof serverContract.name !== "string" || serverContract.events === null) throw new TypeError("createServer expects options with a contract (see a2.contract)");
|
|
171
|
-
const name = serverContract.name;
|
|
172
|
-
const defs = serverContract.events;
|
|
173
|
-
let makeLog;
|
|
174
|
-
if (options.log) {
|
|
175
|
-
const explicit = options.log;
|
|
176
|
-
makeLog = () => Promise.resolve(explicit);
|
|
177
|
-
} else {
|
|
178
|
-
const env = environment();
|
|
179
|
-
if (env === "production") {
|
|
180
|
-
const error = () => new A2Error("LOG_NOT_CONFIGURED", `the server for '${name}' has no log configured and NODE_ENV is 'production' — pass an explicit log backend (e.g. postgres from 'experimental-a2/log-postgres')`);
|
|
181
|
-
if (process.env["NEXT_PHASE"] !== "phase-production-build") throw error();
|
|
182
|
-
makeLog = () => Promise.reject(error());
|
|
183
|
-
} else makeLog = env === "test" ? () => import("./log-memory.js").then((m) => m.memory()) : devDefaultLog.get;
|
|
184
|
-
}
|
|
185
|
-
const resolveLog = retryableLazy(makeLog).get;
|
|
186
|
-
const telemetry = options.telemetry ?? NOOP_TELEMETRY;
|
|
187
|
-
const recovery = options.recovery;
|
|
188
|
-
const handlers = /* @__PURE__ */ new Map();
|
|
189
|
-
for (const [type, entry] of Object.entries(options.handlers ?? {})) {
|
|
190
|
-
if (entry === void 0) continue;
|
|
191
|
-
if (!Object.hasOwn(defs, type)) throw new TypeError(`contract '${name}' has no event type '${type}' — cannot register a handler for it`);
|
|
192
|
-
const handler = typeof entry === "function" ? entry : entry?.handler;
|
|
193
|
-
if (typeof handler !== "function") throw new TypeError(`handler for '${type}' must be a function`);
|
|
194
|
-
let abortOn = null;
|
|
195
|
-
const spec = typeof entry === "function" ? void 0 : entry.abortOn;
|
|
196
|
-
if (spec !== void 0) {
|
|
197
|
-
abortOn = /* @__PURE__ */ new Map();
|
|
198
|
-
const pairs = Array.isArray(spec) ? spec.map((abortType) => [abortType, true]) : Object.entries(spec);
|
|
199
|
-
for (const [abortType, matcher] of pairs) {
|
|
200
|
-
if (matcher === void 0) continue;
|
|
201
|
-
if (!Object.hasOwn(defs, abortType)) throw new TypeError(`contract '${name}' has no event type '${String(abortType)}' — cannot abort on it`);
|
|
202
|
-
abortOn.set(String(abortType), matcher);
|
|
203
|
-
}
|
|
204
|
-
if (abortOn.size === 0) abortOn = null;
|
|
205
|
-
}
|
|
206
|
-
const lane = typeof entry === "function" ? null : entry.lane ?? null;
|
|
207
|
-
if (lane !== null && typeof lane !== "function" && (typeof lane !== "string" || lane.length === 0)) throw new TypeError(`lane for '${type}' must be a non-empty string or a function`);
|
|
208
|
-
handlers.set(type, {
|
|
209
|
-
handler,
|
|
210
|
-
abortOn,
|
|
211
|
-
lane
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
const dormantSignal = new AbortController().signal;
|
|
215
|
-
const prefix = `${name}${NS}`;
|
|
216
|
-
const nsId = (sessionId) => `${prefix}${sessionId}`;
|
|
217
|
-
const stripNs = (stored) => stored.slice(prefix.length);
|
|
218
|
-
const toPublic = (row) => ({
|
|
219
|
-
id: row.id,
|
|
220
|
-
type: row.type,
|
|
221
|
-
payload: row.payload,
|
|
222
|
-
index: row.index,
|
|
223
|
-
sessionId: stripNs(row.sessionId),
|
|
224
|
-
createdAt: row.createdAt
|
|
225
|
-
});
|
|
226
|
-
const inFlight = /* @__PURE__ */ new Set();
|
|
227
|
-
const track = (p) => {
|
|
228
|
-
inFlight.add(p);
|
|
229
|
-
const drop = () => {
|
|
230
|
-
inFlight.delete(p);
|
|
231
|
-
};
|
|
232
|
-
p.then(drop, drop);
|
|
233
|
-
};
|
|
234
|
-
const recoveryArmSlots = /* @__PURE__ */ new Map();
|
|
235
|
-
/**
|
|
236
|
-
* Start an optional recovery arm without putting it on the execution path.
|
|
237
|
-
* The returned raw promise lets the initial append report failure; the
|
|
238
|
-
* tracked copy is always rejection-safe for heartbeat callers.
|
|
239
|
-
*/
|
|
240
|
-
const startRecoveryArm = (sessionId, dueAt) => {
|
|
241
|
-
if (!recovery) return null;
|
|
242
|
-
const slottedDueAt = recoverySlot(dueAt);
|
|
243
|
-
const slotKey = JSON.stringify([sessionId, slottedDueAt]);
|
|
244
|
-
const existing = recoveryArmSlots.get(slotKey);
|
|
245
|
-
if (existing) return existing.promise;
|
|
246
|
-
const raw = Promise.resolve().then(() => recovery.arm({
|
|
247
|
-
contract: name,
|
|
248
|
-
sessionId,
|
|
249
|
-
dueAt: slottedDueAt
|
|
250
|
-
}));
|
|
251
|
-
const forget = setTimeout(() => {
|
|
252
|
-
if (recoveryArmSlots.get(slotKey)?.promise === raw) recoveryArmSlots.delete(slotKey);
|
|
253
|
-
}, Math.max(0, slottedDueAt - Date.now()) + 1e3);
|
|
254
|
-
forget.unref?.();
|
|
255
|
-
recoveryArmSlots.set(slotKey, {
|
|
256
|
-
promise: raw,
|
|
257
|
-
forget
|
|
258
|
-
});
|
|
259
|
-
raw.then(void 0, () => {
|
|
260
|
-
const current = recoveryArmSlots.get(slotKey);
|
|
261
|
-
if (current?.promise !== raw) return;
|
|
262
|
-
clearTimeout(current.forget);
|
|
263
|
-
recoveryArmSlots.delete(slotKey);
|
|
264
|
-
});
|
|
265
|
-
const safe = raw.catch(() => {});
|
|
266
|
-
track(safe);
|
|
267
|
-
platformWaitUntil(safe);
|
|
268
|
-
return raw;
|
|
269
|
-
};
|
|
270
|
-
const scheduledDrains = /* @__PURE__ */ new Map();
|
|
271
|
-
const scheduleDrain = (sessionId, recoveryDueAt) => {
|
|
272
|
-
const existing = scheduledDrains.get(sessionId);
|
|
273
|
-
if (existing && !existing.signal.closed) {
|
|
274
|
-
existing.signal.notify();
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
if (existing) scheduledDrains.delete(sessionId);
|
|
278
|
-
const signal = createDrainSignal();
|
|
279
|
-
const p = new Promise((resolve) => {
|
|
280
|
-
queueMicrotask(() => {
|
|
281
|
-
drainSession(sessionId, recoveryDueAt, signal).then(() => resolve()).catch(() => resolve());
|
|
282
|
-
});
|
|
283
|
-
}).finally(() => {
|
|
284
|
-
if (scheduledDrains.get(sessionId)?.promise === p) scheduledDrains.delete(sessionId);
|
|
285
|
-
});
|
|
286
|
-
scheduledDrains.set(sessionId, {
|
|
287
|
-
signal,
|
|
288
|
-
promise: p
|
|
289
|
-
});
|
|
290
|
-
track(p);
|
|
291
|
-
platformWaitUntil(p);
|
|
292
|
-
};
|
|
293
|
-
const validateEvents = (sessionId, events) => {
|
|
294
|
-
if (events.length === 0) throw new TypeError("append requires at least one event");
|
|
295
|
-
return events.map((e) => {
|
|
296
|
-
const schema = Object.hasOwn(defs, e.type) ? defs[e.type] : void 0;
|
|
297
|
-
if (!schema) throw new A2Error("UNKNOWN_EVENT_TYPE", `contract '${name}' has no event type '${String(e.type)}'`);
|
|
298
|
-
const result = validateSync(schema, e.payload, `event '${e.type}'`);
|
|
299
|
-
if (result.issues) throw new A2Error("INVALID_PAYLOAD", `invalid payload for event '${e.type}' on machine '${name}'`, { details: result.issues });
|
|
300
|
-
const validated = {
|
|
301
|
-
type: e.type,
|
|
302
|
-
payload: result.value
|
|
303
|
-
};
|
|
304
|
-
if (e.id !== void 0) validated.id = e.id;
|
|
305
|
-
const registration = handlers.get(e.type);
|
|
306
|
-
if (!registration) validated.settled = true;
|
|
307
|
-
else if (registration.lane !== null) {
|
|
308
|
-
const lane = typeof registration.lane === "function" ? registration.lane({
|
|
309
|
-
sessionId,
|
|
310
|
-
event: {
|
|
311
|
-
type: e.type,
|
|
312
|
-
payload: structuredClone(result.value),
|
|
313
|
-
...e.id === void 0 ? {} : { id: e.id }
|
|
314
|
-
}
|
|
315
|
-
}) : registration.lane;
|
|
316
|
-
if (typeof lane !== "string" || lane.length === 0) throw new TypeError(`lane for '${e.type}' must resolve to a non-empty string`);
|
|
317
|
-
validated.lane = lane;
|
|
318
|
-
}
|
|
319
|
-
return validated;
|
|
320
|
-
});
|
|
321
|
-
};
|
|
322
|
-
/**
|
|
323
|
-
* Validate + write. `onAppended` runs inside the telemetry span, so
|
|
324
|
-
* the drain it schedules is created in the append's active context —
|
|
325
|
-
* with a real tracer, causal work remains connected.
|
|
326
|
-
*/
|
|
327
|
-
const appendCore = (sessionId, events, source, mode, onAppended, cause, generatedIds) => telemetry.span("a2.append", {
|
|
328
|
-
"a2.contract": name,
|
|
329
|
-
"a2.session_id": sessionId,
|
|
330
|
-
"a2.append.source": source,
|
|
331
|
-
"a2.append.mode": mode,
|
|
332
|
-
"a2.append.types": events.map((e) => String(e.type)).join(","),
|
|
333
|
-
"a2.append.count": events.length
|
|
334
|
-
}, async (span) => {
|
|
335
|
-
if (mode === "dispatch" && !recovery) throw new TypeError("append.dispatch requires a configured recovery adapter");
|
|
336
|
-
assertSessionId(sessionId);
|
|
337
|
-
const validated = validateEvents(sessionId, events);
|
|
338
|
-
if (cause) for (const [index, event] of validated.entries()) event.cause = generatedIds?.[index] ? {
|
|
339
|
-
...cause,
|
|
340
|
-
batchSize: events.length
|
|
341
|
-
} : cause;
|
|
342
|
-
const log = await resolveLog();
|
|
343
|
-
let rows;
|
|
344
|
-
let shouldHealSession;
|
|
345
|
-
try {
|
|
346
|
-
const result = await log.append(nsId(sessionId), validated);
|
|
347
|
-
rows = result.events;
|
|
348
|
-
shouldHealSession = result.hasPending;
|
|
349
|
-
} catch (err) {
|
|
350
|
-
throw asLogUnavailable(err);
|
|
351
|
-
}
|
|
352
|
-
const appended = rows.map(toPublic);
|
|
353
|
-
const initialRecoveryAt = shouldHealSession && recovery && source === "external" && mode === "inline" ? recoverySlot(nextClaimWindow().recoveryAtMs) : void 0;
|
|
354
|
-
const initialArm = initialRecoveryAt !== void 0 ? startRecoveryArm(sessionId, initialRecoveryAt) : null;
|
|
355
|
-
const dispatchArm = shouldHealSession && mode === "dispatch" ? startRecoveryArm(sessionId, currentRecoverySlot(Date.now())) : null;
|
|
356
|
-
if (shouldHealSession && mode === "inline") onAppended(appended, initialRecoveryAt);
|
|
357
|
-
if (dispatchArm) await dispatchArm;
|
|
358
|
-
if (initialArm) {
|
|
359
|
-
let timeout = null;
|
|
360
|
-
try {
|
|
361
|
-
await Promise.race([initialArm, new Promise((_, reject) => {
|
|
362
|
-
timeout = setTimeout(() => {
|
|
363
|
-
reject(/* @__PURE__ */ new Error(`a2 recovery arm timed out after ${DRAIN_TIMINGS.recoveryArmTimeoutMs}ms`));
|
|
364
|
-
}, DRAIN_TIMINGS.recoveryArmTimeoutMs);
|
|
365
|
-
})]);
|
|
366
|
-
} catch (err) {
|
|
367
|
-
span.setAttribute("a2.append.armed", false);
|
|
368
|
-
span.recordError(err);
|
|
369
|
-
} finally {
|
|
370
|
-
if (timeout) clearTimeout(timeout);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
return appended;
|
|
374
|
-
});
|
|
375
|
-
const readAll = async (sessionId) => {
|
|
376
|
-
const log = await resolveLog();
|
|
377
|
-
try {
|
|
378
|
-
return await log.read(nsId(sessionId));
|
|
379
|
-
} catch (err) {
|
|
380
|
-
throw asLogUnavailable(err);
|
|
381
|
-
}
|
|
382
|
-
};
|
|
383
|
-
const readState = async (sessionId, reducer) => {
|
|
384
|
-
const log = await resolveLog();
|
|
385
|
-
return telemetry.span("a2.state", {
|
|
386
|
-
"a2.contract": name,
|
|
387
|
-
"a2.session_id": sessionId,
|
|
388
|
-
"a2.state.reducer": reducer.name
|
|
389
|
-
}, async (span) => {
|
|
390
|
-
let state = cloneInitial(reducer.initialState);
|
|
391
|
-
let index = 0;
|
|
392
|
-
let snapshotOutcome = "miss";
|
|
393
|
-
let stateRead;
|
|
394
|
-
try {
|
|
395
|
-
stateRead = await log.readState(nsId(sessionId), reducer.name);
|
|
396
|
-
} catch {
|
|
397
|
-
try {
|
|
398
|
-
stateRead = {
|
|
399
|
-
snapshot: null,
|
|
400
|
-
events: await log.read(nsId(sessionId))
|
|
401
|
-
};
|
|
402
|
-
} catch (err) {
|
|
403
|
-
throw asLogUnavailable(err);
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
const snap = stateRead.snapshot;
|
|
407
|
-
let rows = stateRead.events;
|
|
408
|
-
if (snap) {
|
|
409
|
-
if (reducer.stateSchema) {
|
|
410
|
-
const result = validateSync(reducer.stateSchema, snap.state, "the stateSchema");
|
|
411
|
-
if (result.issues) snapshotOutcome = "rejected";
|
|
412
|
-
else {
|
|
413
|
-
state = result.value;
|
|
414
|
-
index = snap.index;
|
|
415
|
-
snapshotOutcome = "hit";
|
|
416
|
-
}
|
|
417
|
-
} else {
|
|
418
|
-
state = snap.state;
|
|
419
|
-
index = snap.index;
|
|
420
|
-
snapshotOutcome = "hit";
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
span.setAttribute("a2.state.snapshot", snapshotOutcome);
|
|
424
|
-
if (snapshotOutcome === "rejected") try {
|
|
425
|
-
rows = await log.read(nsId(sessionId));
|
|
426
|
-
} catch (err) {
|
|
427
|
-
throw asLogUnavailable(err);
|
|
428
|
-
}
|
|
429
|
-
for (const row of rows) {
|
|
430
|
-
state = reducer.fold(state, toPublic(row));
|
|
431
|
-
index = row.index;
|
|
432
|
-
}
|
|
433
|
-
span.setAttribute("a2.state.folded", rows.length);
|
|
434
|
-
span.setAttribute("a2.state.index", index);
|
|
435
|
-
if (rows.length > 0) {
|
|
436
|
-
const snapshotState = cloneInitial(state);
|
|
437
|
-
const write = Promise.resolve().then(() => log.putSnapshot(nsId(sessionId), reducer.name, index, snapshotState)).catch(() => {});
|
|
438
|
-
track(write);
|
|
439
|
-
platformWaitUntil(write);
|
|
440
|
-
}
|
|
441
|
-
return {
|
|
442
|
-
state,
|
|
443
|
-
index
|
|
444
|
-
};
|
|
445
|
-
});
|
|
446
|
-
};
|
|
447
|
-
const makeSession = (id, append) => ({
|
|
448
|
-
id,
|
|
449
|
-
append,
|
|
450
|
-
history: async () => (await readAll(id)).map(toPublic),
|
|
451
|
-
state: (reducer) => readState(id, reducer),
|
|
452
|
-
stream: (opts) => {
|
|
453
|
-
const log = resolveLog();
|
|
454
|
-
const startAt = opts?.startAt ?? 0;
|
|
455
|
-
const outer = async function* () {
|
|
456
|
-
const inner = (await log).stream(nsId(id), { startAt });
|
|
457
|
-
for await (const event of inner) yield {
|
|
458
|
-
id: event.id,
|
|
459
|
-
type: event.type,
|
|
460
|
-
payload: event.payload,
|
|
461
|
-
index: event.index,
|
|
462
|
-
sessionId: id,
|
|
463
|
-
createdAt: event.createdAt
|
|
464
|
-
};
|
|
465
|
-
};
|
|
466
|
-
return outer();
|
|
467
|
-
}
|
|
468
|
-
});
|
|
469
|
-
const makeCtx = (trigger, attempt, signal) => {
|
|
470
|
-
const publicSessionId = stripNs(trigger.sessionId);
|
|
471
|
-
const append = async (appendName, ...events) => {
|
|
472
|
-
assertAppendName(appendName);
|
|
473
|
-
const generatedIds = events.map((event) => event.id === void 0);
|
|
474
|
-
const withIds = await Promise.all(events.map(async (event, item) => {
|
|
475
|
-
if (event.id !== void 0) return event;
|
|
476
|
-
const id = await deterministicEventId(trigger.id, appendName, item);
|
|
477
|
-
return {
|
|
478
|
-
...event,
|
|
479
|
-
id
|
|
480
|
-
};
|
|
481
|
-
}));
|
|
482
|
-
return appendCore(publicSessionId, withIds, "handler", "inline", (_rows, recoveryDueAt) => scheduleDrain(publicSessionId, recoveryDueAt), {
|
|
483
|
-
index: trigger.index,
|
|
484
|
-
attempt
|
|
485
|
-
}, generatedIds);
|
|
486
|
-
};
|
|
487
|
-
return {
|
|
488
|
-
event: toPublic(trigger),
|
|
489
|
-
attempt,
|
|
490
|
-
session: makeSession(publicSessionId, append),
|
|
491
|
-
signal
|
|
492
|
-
};
|
|
493
|
-
};
|
|
494
|
-
const abortHubs = /* @__PURE__ */ new Map();
|
|
495
|
-
const createAbortHub = (log, ns, startAt) => {
|
|
496
|
-
const hub = {
|
|
497
|
-
subscriptions: /* @__PURE__ */ new Set(),
|
|
498
|
-
seen: /* @__PURE__ */ new Map(),
|
|
499
|
-
startAt,
|
|
500
|
-
ready: Promise.resolve(),
|
|
501
|
-
watch: null,
|
|
502
|
-
error: null,
|
|
503
|
-
iterator: null,
|
|
504
|
-
stopped: false
|
|
505
|
-
};
|
|
506
|
-
hub.ready = (async () => {
|
|
507
|
-
const existing = await log.read(ns, { afterIndex: startAt });
|
|
508
|
-
for (const event of existing) dispatchAbortEvent(hub, event);
|
|
509
|
-
const tail = existing.at(-1)?.index ?? startAt;
|
|
510
|
-
const iterator = log.stream(ns, { startAt: tail })[Symbol.asyncIterator]();
|
|
511
|
-
hub.iterator = iterator;
|
|
512
|
-
hub.watch = (async () => {
|
|
513
|
-
for (;;) {
|
|
514
|
-
const { value, done } = await iterator.next();
|
|
515
|
-
if (done || value === void 0 || hub.stopped) return;
|
|
516
|
-
dispatchAbortEvent(hub, value);
|
|
517
|
-
}
|
|
518
|
-
})().catch((error) => {
|
|
519
|
-
hub.error = error;
|
|
520
|
-
});
|
|
521
|
-
})().catch((error) => {
|
|
522
|
-
hub.error = error;
|
|
523
|
-
if (abortHubs.get(ns) === hub) abortHubs.delete(ns);
|
|
524
|
-
throw error;
|
|
525
|
-
});
|
|
526
|
-
abortHubs.set(ns, hub);
|
|
527
|
-
return hub;
|
|
528
|
-
};
|
|
529
|
-
const subscribeAbort = async (log, ns, trigger, abortOn, attempt) => {
|
|
530
|
-
let hub = abortHubs.get(ns);
|
|
531
|
-
if (!hub) hub = createAbortHub(log, ns, trigger.index);
|
|
532
|
-
const activeTypes = /* @__PURE__ */ new Set();
|
|
533
|
-
for (const active of hub.subscriptions) for (const type of active.types) activeTypes.add(type);
|
|
534
|
-
const publicTrigger = toPublic(trigger);
|
|
535
|
-
const subscription = {
|
|
536
|
-
controller: new AbortController(),
|
|
537
|
-
triggerIndex: trigger.index,
|
|
538
|
-
types: new Set(abortOn.keys()),
|
|
539
|
-
matches: (event) => {
|
|
540
|
-
if (event.index <= trigger.index) return false;
|
|
541
|
-
const matcher = abortOn.get(event.type);
|
|
542
|
-
if (matcher === void 0) return false;
|
|
543
|
-
return matcher === true ? true : matcher(toPublic(event), publicTrigger, { attempt });
|
|
544
|
-
}
|
|
545
|
-
};
|
|
546
|
-
hub.subscriptions.add(subscription);
|
|
547
|
-
if (trigger.index < hub.startAt || [...subscription.types].some((type) => !activeTypes.has(type))) {
|
|
548
|
-
const earlier = await log.read(ns, { afterIndex: trigger.index });
|
|
549
|
-
hub.startAt = Math.min(hub.startAt, trigger.index);
|
|
550
|
-
for (const event of earlier) dispatchAbortEvent(hub, event);
|
|
551
|
-
}
|
|
552
|
-
for (const event of hub.seen.values()) if (subscription.matches(event)) subscription.controller.abort();
|
|
553
|
-
await hub.ready;
|
|
554
|
-
if (hub.error !== null) throw hub.error;
|
|
555
|
-
return {
|
|
556
|
-
signal: subscription.controller.signal,
|
|
557
|
-
monitor: async (operation) => {
|
|
558
|
-
const outcome = await Promise.race([operation.then((value) => ({
|
|
559
|
-
type: "completed",
|
|
560
|
-
value
|
|
561
|
-
}), (error) => ({
|
|
562
|
-
type: "handler-failed",
|
|
563
|
-
error
|
|
564
|
-
})), hub.watch.then(() => hub.error === null ? { type: "monitor-closed" } : {
|
|
565
|
-
type: "monitor-failed",
|
|
566
|
-
error: hub.error
|
|
567
|
-
})]);
|
|
568
|
-
if (outcome.type === "completed") return outcome.value;
|
|
569
|
-
if (outcome.type === "handler-failed") throw outcome.error;
|
|
570
|
-
if (outcome.type === "monitor-failed") {
|
|
571
|
-
for (const active of hub.subscriptions) active.controller.abort(outcome.error);
|
|
572
|
-
await operation.catch(() => {});
|
|
573
|
-
throw outcome.error;
|
|
574
|
-
}
|
|
575
|
-
await operation.catch(() => {});
|
|
576
|
-
throw new Error("abort monitor closed while handlers were active");
|
|
577
|
-
},
|
|
578
|
-
close: async () => {
|
|
579
|
-
hub.subscriptions.delete(subscription);
|
|
580
|
-
if (hub.subscriptions.size > 0) {
|
|
581
|
-
pruneAbortHub(hub);
|
|
582
|
-
return;
|
|
583
|
-
}
|
|
584
|
-
hub.stopped = true;
|
|
585
|
-
abortHubs.delete(ns);
|
|
586
|
-
await hub.ready;
|
|
587
|
-
await hub.iterator?.return?.();
|
|
588
|
-
await hub.watch;
|
|
589
|
-
}
|
|
590
|
-
};
|
|
591
|
-
};
|
|
592
|
-
const returnedEvents = async (sessionId, trigger, result) => {
|
|
593
|
-
if (result === void 0) return [];
|
|
594
|
-
const events = Array.isArray(result) ? [...result] : [result];
|
|
595
|
-
if (events.length === 0) return [];
|
|
596
|
-
const withIds = await Promise.all(events.map(async (event, item) => ({
|
|
597
|
-
...event,
|
|
598
|
-
id: event.id ?? await deterministicReturnedEventId(trigger.id, item)
|
|
599
|
-
})));
|
|
600
|
-
return validateEvents(sessionId, withIds).map((event) => ({
|
|
601
|
-
...event,
|
|
602
|
-
id: event.id,
|
|
603
|
-
cause: {
|
|
604
|
-
index: trigger.index,
|
|
605
|
-
attempt: trigger.attemptCount
|
|
606
|
-
}
|
|
607
|
-
}));
|
|
608
|
-
};
|
|
609
|
-
const drainSession = (sessionId, recoveryDueAt, signal) => telemetry.span("a2.drain", {
|
|
610
|
-
"a2.contract": name,
|
|
611
|
-
"a2.session_id": sessionId
|
|
612
|
-
}, async (span) => {
|
|
613
|
-
const log = await resolveLog();
|
|
614
|
-
const ns = nsId(sessionId);
|
|
615
|
-
const holder = crypto.randomUUID();
|
|
616
|
-
const active = /* @__PURE__ */ new Map();
|
|
617
|
-
const excluded = /* @__PURE__ */ new Set();
|
|
618
|
-
let processed = 0;
|
|
619
|
-
let sawFailure = false;
|
|
620
|
-
let recoveryArm;
|
|
621
|
-
let heartbeat = null;
|
|
622
|
-
let renewal = null;
|
|
623
|
-
let stopRenewing = false;
|
|
624
|
-
const alignRecoveryAt = (minimumDueAt) => {
|
|
625
|
-
if (recoveryDueAt === void 0) return minimumDueAt;
|
|
626
|
-
return recoveryDueAt + Math.max(0, Math.ceil((minimumDueAt - recoveryDueAt) / DRAIN_TIMINGS.claimHeartbeatMs)) * DRAIN_TIMINGS.claimHeartbeatMs;
|
|
627
|
-
};
|
|
628
|
-
const requestRecovery = (dueAt) => {
|
|
629
|
-
const arm = startRecoveryArm(sessionId, alignRecoveryAt(dueAt));
|
|
630
|
-
if (arm) recoveryArm = arm;
|
|
631
|
-
};
|
|
632
|
-
const renew = () => {
|
|
633
|
-
if (stopRenewing || renewal || active.size === 0) return renewal ?? Promise.resolve();
|
|
634
|
-
const window = nextClaimWindow();
|
|
635
|
-
requestRecovery(window.recoveryAtMs);
|
|
636
|
-
const indexes = [...active.keys()];
|
|
637
|
-
const operation = log.renewClaims({
|
|
638
|
-
sessionId: ns,
|
|
639
|
-
holder,
|
|
640
|
-
indexes,
|
|
641
|
-
ttlMs: window.ttlMs,
|
|
642
|
-
...window.deadlineCapped ? { expiresAtMs: window.expiresAtMs } : {}
|
|
643
|
-
}).then(() => {
|
|
644
|
-
if (window.deadlineCapped) stopRenewing = true;
|
|
645
|
-
}, () => void 0);
|
|
646
|
-
let tracked;
|
|
647
|
-
tracked = operation.finally(() => {
|
|
648
|
-
if (renewal === tracked) renewal = null;
|
|
649
|
-
});
|
|
650
|
-
renewal = tracked;
|
|
651
|
-
return tracked;
|
|
652
|
-
};
|
|
653
|
-
const runEvent = async (event) => {
|
|
654
|
-
const registration = handlers.get(event.type);
|
|
655
|
-
return telemetry.span("a2.event", {
|
|
656
|
-
"a2.contract": name,
|
|
657
|
-
"a2.session_id": sessionId,
|
|
658
|
-
"a2.event.type": event.type,
|
|
659
|
-
"a2.event.index": event.index,
|
|
660
|
-
"a2.event.id": event.id,
|
|
661
|
-
"a2.event.attempt": event.attemptCount,
|
|
662
|
-
"a2.event.handled": registration !== void 0,
|
|
663
|
-
...event.lane === null ? {} : { "a2.event.lane": event.lane }
|
|
664
|
-
}, async (eventSpan) => {
|
|
665
|
-
let subscription;
|
|
666
|
-
let handlerSignal = dormantSignal;
|
|
667
|
-
let returned;
|
|
668
|
-
try {
|
|
669
|
-
if (registration?.abortOn) {
|
|
670
|
-
subscription = await subscribeAbort(log, ns, event, registration.abortOn, event.attemptCount);
|
|
671
|
-
handlerSignal = subscription.signal;
|
|
672
|
-
}
|
|
673
|
-
const operation = registration?.handler(makeCtx(event, event.attemptCount, handlerSignal));
|
|
674
|
-
const result = operation ? await (subscription ? subscription.monitor(operation) : operation) : void 0;
|
|
675
|
-
if (subscription) {
|
|
676
|
-
await subscription.close();
|
|
677
|
-
subscription = void 0;
|
|
678
|
-
}
|
|
679
|
-
returned = await returnedEvents(sessionId, event, result);
|
|
680
|
-
} catch (err) {
|
|
681
|
-
eventSpan.recordError(err);
|
|
682
|
-
const failure = await log.failAttempt({
|
|
683
|
-
sessionId: ns,
|
|
684
|
-
index: event.index,
|
|
685
|
-
attempt: event.attemptCount,
|
|
686
|
-
error: describeError(err),
|
|
687
|
-
maxFailures: MAX_FAILURES
|
|
688
|
-
});
|
|
689
|
-
eventSpan.setAttribute("a2.event.outcome", failure.outcome);
|
|
690
|
-
return {
|
|
691
|
-
status: failure.outcome,
|
|
692
|
-
opensLane: false,
|
|
693
|
-
appendsEvents: false
|
|
694
|
-
};
|
|
695
|
-
} finally {
|
|
696
|
-
await subscription?.close();
|
|
697
|
-
}
|
|
698
|
-
await renewal;
|
|
699
|
-
const completion = await log.completeAttempt({
|
|
700
|
-
sessionId: ns,
|
|
701
|
-
index: event.index,
|
|
702
|
-
attempt: event.attemptCount,
|
|
703
|
-
events: returned
|
|
704
|
-
});
|
|
705
|
-
eventSpan.setAttribute("a2.event.outcome", completion.outcome === "completed" ? "processed" : completion.outcome);
|
|
706
|
-
if (completion.outcome === "superseded") return {
|
|
707
|
-
status: "superseded",
|
|
708
|
-
opensLane: false,
|
|
709
|
-
appendsEvents: false
|
|
710
|
-
};
|
|
711
|
-
if (handlerSignal.aborted) eventSpan.setAttribute("a2.event.aborted", true);
|
|
712
|
-
return {
|
|
713
|
-
status: "processed",
|
|
714
|
-
opensLane: event.lane !== null,
|
|
715
|
-
appendsEvents: returned.length > 0
|
|
716
|
-
};
|
|
717
|
-
});
|
|
718
|
-
};
|
|
719
|
-
let claimAgain = false;
|
|
720
|
-
const start = (event) => {
|
|
721
|
-
const execution = runEvent(event).then((eventOutcome) => {
|
|
722
|
-
active.delete(event.index);
|
|
723
|
-
if (eventOutcome.status === "processed") processed += 1;
|
|
724
|
-
if (eventOutcome.status === "failed") {
|
|
725
|
-
excluded.add(event.index);
|
|
726
|
-
sawFailure = true;
|
|
727
|
-
}
|
|
728
|
-
claimAgain ||= eventOutcome.opensLane || eventOutcome.appendsEvents;
|
|
729
|
-
return eventOutcome;
|
|
730
|
-
});
|
|
731
|
-
active.set(event.index, execution);
|
|
732
|
-
if (!heartbeat && !stopRenewing) {
|
|
733
|
-
heartbeat = setInterval(() => void renew(), DRAIN_TIMINGS.claimHeartbeatMs);
|
|
734
|
-
heartbeat.unref?.();
|
|
735
|
-
}
|
|
736
|
-
};
|
|
737
|
-
let outcome = "settled";
|
|
738
|
-
let shouldClaim = true;
|
|
739
|
-
try {
|
|
740
|
-
for (;;) {
|
|
741
|
-
const signalVersion = signal?.version ?? 0;
|
|
742
|
-
let claim;
|
|
743
|
-
if (shouldClaim) {
|
|
744
|
-
const window = nextClaimWindow();
|
|
745
|
-
claim = await log.claimAvailable({
|
|
746
|
-
sessionId: ns,
|
|
747
|
-
holder,
|
|
748
|
-
ttlMs: window.ttlMs,
|
|
749
|
-
...window.deadlineCapped ? { expiresAtMs: window.expiresAtMs } : {},
|
|
750
|
-
...excluded.size === 0 ? {} : { excludeIndexes: [...excluded] }
|
|
751
|
-
});
|
|
752
|
-
if (claim.outcome === "claimed") {
|
|
753
|
-
requestRecovery(window.recoveryAtMs);
|
|
754
|
-
if (window.deadlineCapped) stopRenewing = true;
|
|
755
|
-
for (const event of claim.events) start(event);
|
|
756
|
-
shouldClaim = false;
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
if (active.size > 0) {
|
|
760
|
-
const wake = signal?.wait(signalVersion);
|
|
761
|
-
await Promise.race([...active.values(), ...wake ? [wake.promise] : []]);
|
|
762
|
-
wake?.cancel();
|
|
763
|
-
shouldClaim = claimAgain || active.size === 0 || signal !== void 0 && signal.version !== signalVersion;
|
|
764
|
-
claimAgain = false;
|
|
765
|
-
continue;
|
|
766
|
-
}
|
|
767
|
-
if (!shouldClaim) {
|
|
768
|
-
shouldClaim = true;
|
|
769
|
-
continue;
|
|
770
|
-
}
|
|
771
|
-
if (claim === void 0) continue;
|
|
772
|
-
if (signal && signal.version !== signalVersion) continue;
|
|
773
|
-
if (claim.outcome === "busy") {
|
|
774
|
-
requestRecovery(claim.retryAt.getTime() + DRAIN_TIMINGS.recoveryGraceMs);
|
|
775
|
-
outcome = "busy";
|
|
776
|
-
} else if (sawFailure) outcome = "stalled";
|
|
777
|
-
if (signal) signal.closed = true;
|
|
778
|
-
break;
|
|
779
|
-
}
|
|
780
|
-
} finally {
|
|
781
|
-
if (signal) signal.closed = true;
|
|
782
|
-
stopRenewing = true;
|
|
783
|
-
if (heartbeat) clearInterval(heartbeat);
|
|
784
|
-
await renewal;
|
|
785
|
-
await Promise.allSettled(active.values());
|
|
786
|
-
}
|
|
787
|
-
span.setAttribute("a2.drain.processed", processed);
|
|
788
|
-
span.setAttribute("a2.drain.outcome", outcome);
|
|
789
|
-
return {
|
|
790
|
-
settled: outcome === "settled",
|
|
791
|
-
outcome,
|
|
792
|
-
processed,
|
|
793
|
-
...recoveryArm ? { recoveryArm } : {}
|
|
794
|
-
};
|
|
795
|
-
});
|
|
796
|
-
const appendExternal = async (sessionId, mode, events) => {
|
|
797
|
-
const pushed = events.filter((event) => Reflect.get(event, "~a2.pushed") === true);
|
|
798
|
-
if (pushed.length > 0) await options.validatePush?.({
|
|
799
|
-
sessionId,
|
|
800
|
-
events: pushed
|
|
801
|
-
});
|
|
802
|
-
return appendCore(sessionId, events, "external", mode, (_rows, recoveryDueAt) => scheduleDrain(sessionId, recoveryDueAt));
|
|
803
|
-
};
|
|
804
|
-
const self = {
|
|
805
|
-
contract: serverContract,
|
|
806
|
-
session(id) {
|
|
807
|
-
assertSessionId(id);
|
|
808
|
-
const appendInline = async (...events) => appendExternal(id, "inline", events);
|
|
809
|
-
const appendDispatch = async (...events) => appendExternal(id, "dispatch", events);
|
|
810
|
-
const append = Object.assign(appendInline, { dispatch: appendDispatch });
|
|
811
|
-
return makeSession(id, append);
|
|
812
|
-
},
|
|
813
|
-
async drain(sessionId) {
|
|
814
|
-
assertSessionId(sessionId);
|
|
815
|
-
const { settled } = await drainSession(sessionId);
|
|
816
|
-
return { settled };
|
|
817
|
-
}
|
|
818
|
-
};
|
|
819
|
-
serverInternals.set(self, {
|
|
820
|
-
settle: async () => {
|
|
821
|
-
while (inFlight.size > 0) await Promise.allSettled(inFlight);
|
|
822
|
-
},
|
|
823
|
-
recoveryDrain: (sessionId, opts) => {
|
|
824
|
-
assertSessionId(sessionId);
|
|
825
|
-
return drainSession(sessionId, opts?.recoveryDueAt);
|
|
826
|
-
}
|
|
827
|
-
});
|
|
828
|
-
serverInspection.set(self, {
|
|
829
|
-
async listSessions(inspectionOptions) {
|
|
830
|
-
const log = await resolveLog();
|
|
831
|
-
if (!log.inspect) throw new TypeError("this log backend does not support inspection");
|
|
832
|
-
const page = await log.inspect.listSessions({
|
|
833
|
-
prefix: `${name}${NS}`,
|
|
834
|
-
limit: inspectionOptions.limit,
|
|
835
|
-
...inspectionOptions.cursor !== void 0 ? { cursor: inspectionOptions.cursor } : {}
|
|
836
|
-
});
|
|
837
|
-
return {
|
|
838
|
-
cursor: page.cursor,
|
|
839
|
-
sessions: page.sessions.map((session) => ({
|
|
840
|
-
...session,
|
|
841
|
-
sessionId: stripNs(session.sessionId)
|
|
842
|
-
}))
|
|
843
|
-
};
|
|
844
|
-
},
|
|
845
|
-
async readSession(sessionId) {
|
|
846
|
-
assertSessionId(sessionId);
|
|
847
|
-
const log = await resolveLog();
|
|
848
|
-
if (!log.inspect) throw new TypeError("this log backend does not support inspection");
|
|
849
|
-
const ns = nsId(sessionId);
|
|
850
|
-
const [events, snapshots] = await Promise.all([log.read(ns), log.inspect.listSnapshots(ns)]);
|
|
851
|
-
return {
|
|
852
|
-
events: events.map((event) => ({
|
|
853
|
-
...event,
|
|
854
|
-
sessionId
|
|
855
|
-
})),
|
|
856
|
-
snapshots
|
|
857
|
-
};
|
|
858
|
-
}
|
|
859
|
-
});
|
|
860
|
-
return self;
|
|
861
|
-
}
|
|
862
|
-
function assertSessionId(id) {
|
|
863
|
-
if (typeof id !== "string" || id.length === 0) throw new TypeError("session id must be a non-empty string");
|
|
864
|
-
if (id.includes(NS)) throw new TypeError("session id contains a reserved control character");
|
|
865
|
-
}
|
|
866
|
-
function assertAppendName(name) {
|
|
867
|
-
if (typeof name !== "string" || name.length === 0) throw new TypeError("handler append name must be a non-empty string");
|
|
868
|
-
}
|
|
869
|
-
function cloneInitial(initial) {
|
|
870
|
-
try {
|
|
871
|
-
return structuredClone(initial);
|
|
872
|
-
} catch {
|
|
873
|
-
return initial;
|
|
874
|
-
}
|
|
875
|
-
}
|
|
876
|
-
//#endregion
|
|
877
|
-
export { createServer as t };
|