@helipod/runtime-embedded 0.1.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/dist/index.d.ts +722 -0
- package/dist/index.js +1143 -0
- package/dist/index.js.map +1 -0
- package/package.json +55 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1143 @@
|
|
|
1
|
+
// src/runtime.ts
|
|
2
|
+
import { namespaceForPath } from "@helipod/component";
|
|
3
|
+
import { FunctionNotFoundError } from "@helipod/errors";
|
|
4
|
+
import { decodeStorageTableId, decodeDocumentId, encodeInternalDocumentId, shardIdForKeyValue, DEFAULT_SHARD } from "@helipod/id-codec";
|
|
5
|
+
import { writtenTablesFromRanges, serializeKeyRange } from "@helipod/index-key-codec";
|
|
6
|
+
import { jsonToConvex, convexToJson as convexToJson2 } from "@helipod/values";
|
|
7
|
+
import { MonotonicTimestampOracle } from "@helipod/docstore";
|
|
8
|
+
import { SingleWriterTransactor, ShardedTransactor } from "@helipod/transactor";
|
|
9
|
+
import { QueryRuntime } from "@helipod/query-engine";
|
|
10
|
+
import { InlineUdfExecutor, mutation } from "@helipod/executor";
|
|
11
|
+
import { SyncProtocolHandler } from "@helipod/sync";
|
|
12
|
+
|
|
13
|
+
// src/write-fanout.ts
|
|
14
|
+
var InMemoryWriteFanoutAdapter = class {
|
|
15
|
+
listeners = /* @__PURE__ */ new Set();
|
|
16
|
+
published = [];
|
|
17
|
+
publish(payload) {
|
|
18
|
+
this.published.push(payload);
|
|
19
|
+
for (const listener of this.listeners) listener(payload);
|
|
20
|
+
}
|
|
21
|
+
subscribe(listener) {
|
|
22
|
+
this.listeners.add(listener);
|
|
23
|
+
return () => this.listeners.delete(listener);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
var EmbeddedWriteFanout = class {
|
|
27
|
+
constructor(adapter, originId) {
|
|
28
|
+
this.adapter = adapter;
|
|
29
|
+
this.originId = originId;
|
|
30
|
+
}
|
|
31
|
+
adapter;
|
|
32
|
+
originId;
|
|
33
|
+
publish(delta) {
|
|
34
|
+
this.adapter.publish({
|
|
35
|
+
commitTs: Number(delta.commitTs),
|
|
36
|
+
tables: delta.writtenTables,
|
|
37
|
+
ranges: delta.writtenRanges,
|
|
38
|
+
originId: this.originId,
|
|
39
|
+
origin: delta.origin,
|
|
40
|
+
shardId: delta.shardId,
|
|
41
|
+
writtenDocs: delta.writtenDocs
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
/** Subscribe to deltas from OTHER origins (ignores our own — the Tier 2 self-loop guard). */
|
|
45
|
+
subscribe(listener) {
|
|
46
|
+
return this.adapter.subscribe((payload) => {
|
|
47
|
+
if (payload.originId !== this.originId) listener(payload);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// src/loopback.ts
|
|
53
|
+
import { parseClientMessage } from "@helipod/sync";
|
|
54
|
+
var LoopbackServerSocket = class {
|
|
55
|
+
bufferedAmount = 0;
|
|
56
|
+
listeners = /* @__PURE__ */ new Set();
|
|
57
|
+
open = true;
|
|
58
|
+
send(data) {
|
|
59
|
+
if (!this.open) return;
|
|
60
|
+
const msg = JSON.parse(data);
|
|
61
|
+
for (const listener of this.listeners) listener(msg);
|
|
62
|
+
}
|
|
63
|
+
close() {
|
|
64
|
+
this.open = false;
|
|
65
|
+
}
|
|
66
|
+
addListener(listener) {
|
|
67
|
+
this.listeners.add(listener);
|
|
68
|
+
return () => this.listeners.delete(listener);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
function createLoopbackConnection(handler, sessionId) {
|
|
72
|
+
const socket = new LoopbackServerSocket();
|
|
73
|
+
handler.connect(sessionId, socket);
|
|
74
|
+
return {
|
|
75
|
+
sessionId,
|
|
76
|
+
async send(message) {
|
|
77
|
+
const raw = typeof message === "string" ? message : JSON.stringify(message);
|
|
78
|
+
parseClientMessage(raw);
|
|
79
|
+
await handler.handleMessage(sessionId, raw);
|
|
80
|
+
},
|
|
81
|
+
onMessage(listener) {
|
|
82
|
+
return socket.addListener(listener);
|
|
83
|
+
},
|
|
84
|
+
close() {
|
|
85
|
+
socket.close();
|
|
86
|
+
handler.disconnect(sessionId);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// src/client-dedup.ts
|
|
92
|
+
import {
|
|
93
|
+
CommitGuardRejection,
|
|
94
|
+
isRetryableError,
|
|
95
|
+
isHelipodError
|
|
96
|
+
} from "@helipod/errors";
|
|
97
|
+
import { convexToJson } from "@helipod/values";
|
|
98
|
+
var DEDUP_META_CLIENT_ID = "clientId";
|
|
99
|
+
var DEDUP_META_SEQ = "seq";
|
|
100
|
+
var DEDUP_META_IDENTITY = "identity";
|
|
101
|
+
function identityKey(identity) {
|
|
102
|
+
return identity ?? "";
|
|
103
|
+
}
|
|
104
|
+
function replayFromRecord(rec) {
|
|
105
|
+
if (rec.verdict === "applied") {
|
|
106
|
+
return {
|
|
107
|
+
verdict: "applied",
|
|
108
|
+
commitTs: Number(rec.commitTs),
|
|
109
|
+
...rec.hasValue ? { value: rec.value } : { valueMissing: true }
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
return { verdict: "failed", commitTs: Number(rec.commitTs), code: rec.errorCode ?? "MUTATION_FAILED" };
|
|
113
|
+
}
|
|
114
|
+
async function classifyDedup(store, identity, dedup) {
|
|
115
|
+
const id = identityKey(identity);
|
|
116
|
+
const rec = await store.getClientVerdict(id, dedup.clientId, dedup.seq);
|
|
117
|
+
if (rec) return replayFromRecord(rec);
|
|
118
|
+
const floor = await store.getClientFloor(id, dedup.clientId);
|
|
119
|
+
if (floor !== null && dedup.seq <= floor) return { verdict: "stale", code: "STALE_CLIENT" };
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
async function classifyForConnect(store, identity, clientId, seq) {
|
|
123
|
+
const replay = await classifyDedup(store, identity, { clientId, seq });
|
|
124
|
+
if (replay === null) return { clientId, seq, verdict: "unknown" };
|
|
125
|
+
return { clientId, seq, ...replay };
|
|
126
|
+
}
|
|
127
|
+
function dedupCommitMeta(identity, dedup, base) {
|
|
128
|
+
return {
|
|
129
|
+
...base ?? {},
|
|
130
|
+
[DEDUP_META_IDENTITY]: identityKey(identity),
|
|
131
|
+
[DEDUP_META_CLIENT_ID]: dedup.clientId,
|
|
132
|
+
[DEDUP_META_SEQ]: String(dedup.seq)
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
async function handleDedupError(store, identity, dedup, e) {
|
|
136
|
+
const id = identityKey(identity);
|
|
137
|
+
if (e instanceof CommitGuardRejection && e.rejectionCode === "CLIENT_MUTATION_DUP") {
|
|
138
|
+
const rec = await store.getClientVerdict(id, dedup.clientId, dedup.seq);
|
|
139
|
+
return rec ? replayFromRecord(rec) : null;
|
|
140
|
+
}
|
|
141
|
+
if (!isRetryableError(e)) {
|
|
142
|
+
const code = isHelipodError(e) ? e.code : "MUTATION_FAILED";
|
|
143
|
+
await store.recordClientVerdict(id, dedup.clientId, dedup.seq, { verdict: "failed", commitTs: 0n, errorCode: code });
|
|
144
|
+
}
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
async function recordZeroWriteApplied(store, identity, dedup, commitTs, value) {
|
|
148
|
+
await store.recordClientVerdict(identityKey(identity), dedup.clientId, dedup.seq, {
|
|
149
|
+
verdict: "applied",
|
|
150
|
+
commitTs,
|
|
151
|
+
value: convexToJson(value)
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
async function fillWriteMutationValue(store, identity, dedup, value) {
|
|
155
|
+
try {
|
|
156
|
+
await store.updateClientVerdictValue(identityKey(identity), dedup.clientId, dedup.seq, convexToJson(value));
|
|
157
|
+
} catch {
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function clientReceiptsGuard() {
|
|
161
|
+
const INSERT = `INSERT INTO client_mutations (identity, client_id, seq, verdict, commit_ts, value_json, error_code, created_at)`;
|
|
162
|
+
return (q, units) => {
|
|
163
|
+
const pg = q;
|
|
164
|
+
if (typeof pg.query === "function") {
|
|
165
|
+
return (async () => {
|
|
166
|
+
for (let i = 0; i < units.length; i++) {
|
|
167
|
+
const dedup = readUnitDedup(units[i]);
|
|
168
|
+
if (!dedup) continue;
|
|
169
|
+
try {
|
|
170
|
+
await pg.query(
|
|
171
|
+
`${INSERT} VALUES ($1, $2, $3, 'applied', $4, NULL, NULL, $5)`,
|
|
172
|
+
[dedup.identity, dedup.clientId, BigInt(dedup.seq), units[i].ts, BigInt(Date.now())]
|
|
173
|
+
);
|
|
174
|
+
} catch (e) {
|
|
175
|
+
throw toDupOrRethrow(e, i, dedup);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
})();
|
|
179
|
+
}
|
|
180
|
+
const sq = q;
|
|
181
|
+
for (let i = 0; i < units.length; i++) {
|
|
182
|
+
const dedup = readUnitDedup(units[i]);
|
|
183
|
+
if (!dedup) continue;
|
|
184
|
+
try {
|
|
185
|
+
sq.run(
|
|
186
|
+
`${INSERT} VALUES (?, ?, ?, 'applied', ?, NULL, NULL, ?)`,
|
|
187
|
+
dedup.identity,
|
|
188
|
+
dedup.clientId,
|
|
189
|
+
dedup.seq,
|
|
190
|
+
units[i].ts,
|
|
191
|
+
Date.now()
|
|
192
|
+
);
|
|
193
|
+
} catch (e) {
|
|
194
|
+
throw toDupOrRethrow(e, i, dedup);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return void 0;
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function readUnitDedup(unit) {
|
|
201
|
+
const meta = unit.meta;
|
|
202
|
+
const clientId = meta?.[DEDUP_META_CLIENT_ID];
|
|
203
|
+
const seqStr = meta?.[DEDUP_META_SEQ];
|
|
204
|
+
if (clientId === void 0 || seqStr === void 0) return null;
|
|
205
|
+
return { identity: meta[DEDUP_META_IDENTITY] ?? "", clientId, seq: Number(seqStr) };
|
|
206
|
+
}
|
|
207
|
+
function toDupOrRethrow(e, unitIndex, dedup) {
|
|
208
|
+
const code = String(e.code ?? "");
|
|
209
|
+
const message = String(e?.message ?? "");
|
|
210
|
+
const isUniqueViolation = code === "23505" || code.startsWith("SQLITE_CONSTRAINT") || /unique constraint failed/i.test(message);
|
|
211
|
+
if (isUniqueViolation) {
|
|
212
|
+
return new CommitGuardRejection(unitIndex, "CLIENT_MUTATION_DUP", `client=${dedup.clientId} seq=${dedup.seq}`, {
|
|
213
|
+
cause: e
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
return e;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// src/runtime.ts
|
|
220
|
+
var DEPLOYMENT_ID_GLOBAL_KEY = "fleet:deploymentId";
|
|
221
|
+
function isInternalPath(path) {
|
|
222
|
+
return path.split(":").some((seg) => seg.startsWith("_"));
|
|
223
|
+
}
|
|
224
|
+
function rebuildTableNumberToName(map, tableNumbers) {
|
|
225
|
+
map.clear();
|
|
226
|
+
for (const [name, num] of Object.entries(tableNumbers)) map.set(num, name);
|
|
227
|
+
}
|
|
228
|
+
function replayToRunResult(replay) {
|
|
229
|
+
const out = { replayed: true, verdict: replay.verdict };
|
|
230
|
+
if (replay.commitTs !== void 0) out.commitTs = replay.commitTs;
|
|
231
|
+
if (replay.value !== void 0) out.value = jsonToConvex(replay.value);
|
|
232
|
+
if (replay.valueMissing) out.valueMissing = true;
|
|
233
|
+
if (replay.code !== void 0) out.code = replay.code;
|
|
234
|
+
return out;
|
|
235
|
+
}
|
|
236
|
+
function translateTableIds(tableIds, tableNumberToName) {
|
|
237
|
+
return tableIds.map((id) => {
|
|
238
|
+
try {
|
|
239
|
+
return tableNumberToName.get(decodeStorageTableId(id)) ?? id;
|
|
240
|
+
} catch {
|
|
241
|
+
return id;
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
function fireCommitSubs(commitSubs, inv) {
|
|
246
|
+
for (const cb of commitSubs) {
|
|
247
|
+
try {
|
|
248
|
+
cb(inv);
|
|
249
|
+
} catch (e) {
|
|
250
|
+
console.error("[runtime] driver onCommit callback threw:", e);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
var EmbeddedRuntime = class _EmbeddedRuntime {
|
|
255
|
+
constructor(store, executor, handler, writeFanoutAdapter, modules, systemModules, adminModules, componentNames, contextProviders, policyRegistry, policyProviders, relationRegistry, drivers, timers, fireDue, tableNumberToName, oracle, queryOracle, transactor, driverCtx, driversStarted, writeRouter, numShards, catalog, commitSubs) {
|
|
256
|
+
this.store = store;
|
|
257
|
+
this.executor = executor;
|
|
258
|
+
this.handler = handler;
|
|
259
|
+
this.writeFanoutAdapter = writeFanoutAdapter;
|
|
260
|
+
this.modules = modules;
|
|
261
|
+
this.systemModules = systemModules;
|
|
262
|
+
this.adminModules = adminModules;
|
|
263
|
+
this.componentNames = componentNames;
|
|
264
|
+
this.contextProviders = contextProviders;
|
|
265
|
+
this.policyRegistry = policyRegistry;
|
|
266
|
+
this.policyProviders = policyProviders;
|
|
267
|
+
this.relationRegistry = relationRegistry;
|
|
268
|
+
this.drivers = drivers;
|
|
269
|
+
this.timers = timers;
|
|
270
|
+
this.fireDue = fireDue;
|
|
271
|
+
this.tableNumberToName = tableNumberToName;
|
|
272
|
+
this.oracle = oracle;
|
|
273
|
+
this.queryOracle = queryOracle;
|
|
274
|
+
this.transactor = transactor;
|
|
275
|
+
this.driverCtx = driverCtx;
|
|
276
|
+
this.driversStarted = driversStarted;
|
|
277
|
+
this.writeRouter = writeRouter;
|
|
278
|
+
this.numShards = numShards;
|
|
279
|
+
this.catalog = catalog;
|
|
280
|
+
this.commitSubs = commitSubs;
|
|
281
|
+
}
|
|
282
|
+
store;
|
|
283
|
+
executor;
|
|
284
|
+
handler;
|
|
285
|
+
writeFanoutAdapter;
|
|
286
|
+
modules;
|
|
287
|
+
systemModules;
|
|
288
|
+
adminModules;
|
|
289
|
+
componentNames;
|
|
290
|
+
contextProviders;
|
|
291
|
+
policyRegistry;
|
|
292
|
+
policyProviders;
|
|
293
|
+
relationRegistry;
|
|
294
|
+
drivers;
|
|
295
|
+
timers;
|
|
296
|
+
fireDue;
|
|
297
|
+
tableNumberToName;
|
|
298
|
+
oracle;
|
|
299
|
+
queryOracle;
|
|
300
|
+
transactor;
|
|
301
|
+
driverCtx;
|
|
302
|
+
driversStarted;
|
|
303
|
+
writeRouter;
|
|
304
|
+
numShards;
|
|
305
|
+
catalog;
|
|
306
|
+
commitSubs;
|
|
307
|
+
sessionCounter = 0;
|
|
308
|
+
static async create(options) {
|
|
309
|
+
await options.store.setupSchema();
|
|
310
|
+
if (!options.externalReceiptsGuard) options.store.addCommitGuard(clientReceiptsGuard());
|
|
311
|
+
let deploymentId = await options.store.getGlobal(DEPLOYMENT_ID_GLOBAL_KEY);
|
|
312
|
+
if (deploymentId === null) {
|
|
313
|
+
try {
|
|
314
|
+
await options.store.writeGlobalIfAbsent(DEPLOYMENT_ID_GLOBAL_KEY, crypto.randomUUID());
|
|
315
|
+
deploymentId = await options.store.getGlobal(DEPLOYMENT_ID_GLOBAL_KEY);
|
|
316
|
+
} catch {
|
|
317
|
+
deploymentId = null;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
const resolvedDeploymentId = deploymentId ?? "";
|
|
321
|
+
if (options.queryStore) await options.queryStore.setupSchema();
|
|
322
|
+
const adapter = options.fanoutAdapter ?? new InMemoryWriteFanoutAdapter();
|
|
323
|
+
const fanout = new EmbeddedWriteFanout(adapter, options.originId ?? "embedded");
|
|
324
|
+
const startTs = await options.store.maxTimestamp();
|
|
325
|
+
let transactor;
|
|
326
|
+
let oracle;
|
|
327
|
+
if ((options.numShards ?? 1) > 1) {
|
|
328
|
+
const sharded = new ShardedTransactor(options.store, { fanout, groupCommit: options.groupCommit ?? false });
|
|
329
|
+
sharded.observeTimestamp(startTs);
|
|
330
|
+
transactor = sharded;
|
|
331
|
+
oracle = sharded;
|
|
332
|
+
} else {
|
|
333
|
+
const singleOracle = new MonotonicTimestampOracle(startTs);
|
|
334
|
+
transactor = new SingleWriterTransactor(options.store, singleOracle, { fanout, groupCommit: options.groupCommit ?? false });
|
|
335
|
+
oracle = singleOracle;
|
|
336
|
+
}
|
|
337
|
+
const queryRuntime = new QueryRuntime(options.store);
|
|
338
|
+
let queryPath;
|
|
339
|
+
let queryOracle;
|
|
340
|
+
if (options.queryStore) {
|
|
341
|
+
const queryStartTs = await options.queryStore.maxTimestamp();
|
|
342
|
+
const qOracle = new MonotonicTimestampOracle(queryStartTs);
|
|
343
|
+
const queryTransactor = new SingleWriterTransactor(options.queryStore, qOracle);
|
|
344
|
+
queryPath = { transactor: queryTransactor, queryRuntime: new QueryRuntime(options.queryStore) };
|
|
345
|
+
queryOracle = qOracle;
|
|
346
|
+
}
|
|
347
|
+
const numShards = options.numShards ?? 1;
|
|
348
|
+
let executorRef;
|
|
349
|
+
const invoke = async (path, args, opts) => {
|
|
350
|
+
const fn = modules[path];
|
|
351
|
+
if (!fn) throw new FunctionNotFoundError(`unknown function: ${path}`);
|
|
352
|
+
return executorRef.run(fn, jsonToConvex(args), {
|
|
353
|
+
path,
|
|
354
|
+
namespace: namespaceForPath(path, componentNames),
|
|
355
|
+
contextProviders,
|
|
356
|
+
policyRegistry,
|
|
357
|
+
policyProviders,
|
|
358
|
+
relationRegistry,
|
|
359
|
+
functionKind,
|
|
360
|
+
identity: opts?.identity ?? null,
|
|
361
|
+
numShards
|
|
362
|
+
});
|
|
363
|
+
};
|
|
364
|
+
const executor = new InlineUdfExecutor({ transactor, queryRuntime, catalog: options.catalog, logSink: options.logSink, now: options.now, invoke, writeRouter: options.writeRouter, queryPath, globalStore: options.globalStore });
|
|
365
|
+
executorRef = executor;
|
|
366
|
+
for (const step of options.bootSteps ?? []) {
|
|
367
|
+
const bootFn = mutation(async (ctx) => {
|
|
368
|
+
await step.run({ db: ctx.db, now: ctx.now() });
|
|
369
|
+
return null;
|
|
370
|
+
});
|
|
371
|
+
await executor.run(bootFn, {}, { path: `_boot:${step.name}`, namespace: step.name, identity: null, numShards, localOnly: true });
|
|
372
|
+
}
|
|
373
|
+
const componentNames = options.componentNames ?? /* @__PURE__ */ new Set();
|
|
374
|
+
const contextProviders = options.contextProviders ?? [];
|
|
375
|
+
const policyRegistry = options.policyRegistry ?? /* @__PURE__ */ new Map();
|
|
376
|
+
const policyProviders = options.policyProviders ?? [];
|
|
377
|
+
const relationRegistry = options.relationRegistry;
|
|
378
|
+
const modules = { ...options.modules };
|
|
379
|
+
const systemModules = { ...options.systemModules ?? {} };
|
|
380
|
+
const adminModules = { ...options.adminModules ?? {} };
|
|
381
|
+
const functionKind = (path) => modules[path]?.type;
|
|
382
|
+
const resolve = (path) => {
|
|
383
|
+
if (isInternalPath(path)) throw new FunctionNotFoundError(`unknown function: ${path}`);
|
|
384
|
+
const fn = modules[path];
|
|
385
|
+
if (!fn) throw new FunctionNotFoundError(`unknown function: ${path}`);
|
|
386
|
+
return fn;
|
|
387
|
+
};
|
|
388
|
+
const syncExecutor = {
|
|
389
|
+
async runQuery(path, args, identity) {
|
|
390
|
+
const r = await executor.run(resolve(path), jsonToConvex(args), { path, namespace: namespaceForPath(path, componentNames), contextProviders, policyRegistry, policyProviders, relationRegistry, functionKind, identity: identity ?? null, numShards });
|
|
391
|
+
return {
|
|
392
|
+
value: r.value,
|
|
393
|
+
tables: writtenTablesFromRanges(r.readRanges),
|
|
394
|
+
readRanges: r.readRanges.map(serializeKeyRange),
|
|
395
|
+
globalTables: r.globalTables ?? [],
|
|
396
|
+
...r.diffableRange ? { diffableRange: r.diffableRange } : {},
|
|
397
|
+
...r.diffablePage ? { diffablePage: r.diffablePage } : {}
|
|
398
|
+
};
|
|
399
|
+
},
|
|
400
|
+
async runMutation(path, args, identity, origin, dedup) {
|
|
401
|
+
const fn = resolve(path);
|
|
402
|
+
const willForward = dedup !== void 0 && !!options.writeRouter && !options.writeRouter.isLocalWriter(DEFAULT_SHARD);
|
|
403
|
+
const classifyLocally = dedup !== void 0 && !willForward;
|
|
404
|
+
if (classifyLocally) {
|
|
405
|
+
const pre = await classifyDedup(options.store, identity ?? null, dedup);
|
|
406
|
+
if (pre) return replayToRunResult(pre);
|
|
407
|
+
}
|
|
408
|
+
const runOpts = {
|
|
409
|
+
path,
|
|
410
|
+
namespace: namespaceForPath(path, componentNames),
|
|
411
|
+
contextProviders,
|
|
412
|
+
policyRegistry,
|
|
413
|
+
policyProviders,
|
|
414
|
+
relationRegistry,
|
|
415
|
+
functionKind,
|
|
416
|
+
identity: identity ?? null,
|
|
417
|
+
numShards,
|
|
418
|
+
origin,
|
|
419
|
+
dedup,
|
|
420
|
+
commitMeta: classifyLocally ? dedupCommitMeta(identity ?? null, dedup) : void 0
|
|
421
|
+
};
|
|
422
|
+
let r;
|
|
423
|
+
try {
|
|
424
|
+
r = await executor.run(fn, jsonToConvex(args), runOpts);
|
|
425
|
+
} catch (e) {
|
|
426
|
+
if (classifyLocally) {
|
|
427
|
+
const replay = await handleDedupError(options.store, identity ?? null, dedup, e);
|
|
428
|
+
if (replay) return replayToRunResult(replay);
|
|
429
|
+
}
|
|
430
|
+
throw e;
|
|
431
|
+
}
|
|
432
|
+
if (r.clientReplay) return replayToRunResult(r.clientReplay);
|
|
433
|
+
if (classifyLocally && !r.committed) {
|
|
434
|
+
await recordZeroWriteApplied(options.store, identity ?? null, dedup, r.commitTs, r.value);
|
|
435
|
+
} else if (classifyLocally && r.oplog !== null) {
|
|
436
|
+
await fillWriteMutationValue(options.store, identity ?? null, dedup, r.value);
|
|
437
|
+
}
|
|
438
|
+
return {
|
|
439
|
+
replayed: false,
|
|
440
|
+
value: r.value,
|
|
441
|
+
tables: r.oplog?.writtenTables ?? [],
|
|
442
|
+
writeRanges: r.oplog?.writtenRanges ?? [],
|
|
443
|
+
// `commitTs` (B2b, T2): a LOCAL commit reports it via `r.oplog.commitTs`; a FORWARDED
|
|
444
|
+
// mutation has no local oplog (null), but the executor's forward branch already threads
|
|
445
|
+
// the owner's real commitTs onto `r.commitTs` itself — fall back to that.
|
|
446
|
+
commitTs: Number(r.oplog?.commitTs ?? r.commitTs ?? 0n),
|
|
447
|
+
// G4 fleet fallback signal: a committed write with NO local oplog was forwarded to another
|
|
448
|
+
// shard's owner — its origin tag can't reach this node's `doNotifyWrites`.
|
|
449
|
+
forwarded: r.committed && r.oplog === null
|
|
450
|
+
};
|
|
451
|
+
},
|
|
452
|
+
async runAdminQuery(path, args) {
|
|
453
|
+
const fn = adminModules[path];
|
|
454
|
+
if (!fn) throw new Error(`unknown admin function: ${path}`);
|
|
455
|
+
const r = await executor.run(fn, jsonToConvex(args), { path, privileged: true, numShards });
|
|
456
|
+
return {
|
|
457
|
+
value: r.value,
|
|
458
|
+
tables: writtenTablesFromRanges(r.readRanges),
|
|
459
|
+
readRanges: r.readRanges.map(serializeKeyRange),
|
|
460
|
+
globalTables: r.globalTables ?? [],
|
|
461
|
+
...r.diffableRange ? { diffableRange: r.diffableRange } : {},
|
|
462
|
+
...r.diffablePage ? { diffablePage: r.diffablePage } : {}
|
|
463
|
+
};
|
|
464
|
+
},
|
|
465
|
+
async runAction(path, args, identity) {
|
|
466
|
+
const fn = resolve(path);
|
|
467
|
+
if (fn.type !== "action") throw new Error(`${path} is not an action`);
|
|
468
|
+
if (options.writeRouter && !options.writeRouter.isLocalWriter(DEFAULT_SHARD)) {
|
|
469
|
+
const res = await options.writeRouter.forward("action", path, args, identity ?? null, DEFAULT_SHARD);
|
|
470
|
+
return { value: jsonToConvex(res.value) };
|
|
471
|
+
}
|
|
472
|
+
const r = await executor.run(fn, jsonToConvex(args), { path, namespace: namespaceForPath(path, componentNames), contextProviders, policyRegistry, policyProviders, relationRegistry, functionKind, identity: identity ?? null, numShards });
|
|
473
|
+
return { value: r.value };
|
|
474
|
+
},
|
|
475
|
+
// ── Receipted Outbox resume-handshake support (verdict §(e)) ──────────────────────────────
|
|
476
|
+
// Both reads/writes go to the AUTHORITATIVE receipts store (the primary), NOT `options.store`:
|
|
477
|
+
// on a fleet sync node the latter is the replica, which carries no receipts (see
|
|
478
|
+
// `receiptsStore`'s doc comment). Unset → `options.store`, byte-identical off the fleet.
|
|
479
|
+
async classifyClientMutation(identity, clientId, seq) {
|
|
480
|
+
return classifyForConnect(options.receiptsStore ?? options.store, identity ?? null, clientId, seq);
|
|
481
|
+
},
|
|
482
|
+
async pruneClientMutations(identity, clientId, ackedThrough) {
|
|
483
|
+
await (options.receiptsStore ?? options.store).pruneClientMutations(identity ?? "", clientId, { ackedThrough });
|
|
484
|
+
},
|
|
485
|
+
deploymentId() {
|
|
486
|
+
return resolvedDeploymentId;
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
const handler = new SyncProtocolHandler(syncExecutor, {
|
|
490
|
+
autoNotifyOnMutation: false,
|
|
491
|
+
verifyAdmin: options.verifyAdmin,
|
|
492
|
+
// The DO host (Slice 3) sets this so the handler arms no `setInterval` sweep / ping heartbeat;
|
|
493
|
+
// unset everywhere else (byte-identical to before this option existed).
|
|
494
|
+
disableBackgroundTimers: options.disableSyncBackgroundTimers
|
|
495
|
+
});
|
|
496
|
+
const queue = [];
|
|
497
|
+
let draining = false;
|
|
498
|
+
const drain = async () => {
|
|
499
|
+
if (draining) return;
|
|
500
|
+
draining = true;
|
|
501
|
+
try {
|
|
502
|
+
while (queue.length > 0) {
|
|
503
|
+
const inv = queue.shift();
|
|
504
|
+
if (options.beforeNotify) {
|
|
505
|
+
try {
|
|
506
|
+
await options.beforeNotify(BigInt(inv.commitTs));
|
|
507
|
+
} catch (e) {
|
|
508
|
+
console.error("[runtime] beforeNotify hook threw:", e);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
await handler.notifyWrites(inv, inv.origin);
|
|
512
|
+
}
|
|
513
|
+
} finally {
|
|
514
|
+
draining = false;
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
const commitSubs = /* @__PURE__ */ new Set();
|
|
518
|
+
const timers = /* @__PURE__ */ new Map();
|
|
519
|
+
let timerSeq = 0;
|
|
520
|
+
const wakeHost = options.wakeHost;
|
|
521
|
+
let armedAt = null;
|
|
522
|
+
const rearm = () => {
|
|
523
|
+
if (!wakeHost) return;
|
|
524
|
+
let min = null;
|
|
525
|
+
for (const t of timers.values()) if (min === null || t.atMs < min) min = t.atMs;
|
|
526
|
+
if (min === armedAt) return;
|
|
527
|
+
armedAt = min;
|
|
528
|
+
wakeHost.armWake(min);
|
|
529
|
+
};
|
|
530
|
+
const fireDueTimers = () => {
|
|
531
|
+
const t0 = options.now?.() ?? Date.now();
|
|
532
|
+
armedAt = null;
|
|
533
|
+
const due = [];
|
|
534
|
+
for (const [h, t] of timers) {
|
|
535
|
+
if (t.atMs <= t0) {
|
|
536
|
+
due.push(t);
|
|
537
|
+
timers.delete(h);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
try {
|
|
541
|
+
for (const t of due) {
|
|
542
|
+
try {
|
|
543
|
+
t.cb();
|
|
544
|
+
} catch (e) {
|
|
545
|
+
console.error("[runtime] driver timer callback threw:", e);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
} finally {
|
|
549
|
+
rearm();
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
const backstopMs = options.backstopMs ?? ((d) => d);
|
|
553
|
+
const driverCtx = {
|
|
554
|
+
runFunction: async (path, args) => {
|
|
555
|
+
const fn = modules[path];
|
|
556
|
+
if (!fn) throw new Error(`driver: unknown function ${path}`);
|
|
557
|
+
const ns = namespaceForPath(path, componentNames);
|
|
558
|
+
const res = await executor.run(fn, jsonToConvex(args), {
|
|
559
|
+
path,
|
|
560
|
+
namespace: ns,
|
|
561
|
+
contextProviders,
|
|
562
|
+
policyRegistry,
|
|
563
|
+
policyProviders,
|
|
564
|
+
relationRegistry,
|
|
565
|
+
functionKind,
|
|
566
|
+
identity: null,
|
|
567
|
+
privileged: true,
|
|
568
|
+
numShards,
|
|
569
|
+
// A driver runs on the writer that owns its control tables and must read-its-own-writes:
|
|
570
|
+
// force its queries onto the PRIMARY, never a hybrid node's lagging replica queryPath, so
|
|
571
|
+
// a just-enqueued scheduler job is visible on the very next peek. No-op off a hybrid.
|
|
572
|
+
primaryRead: true
|
|
573
|
+
});
|
|
574
|
+
return res.value;
|
|
575
|
+
},
|
|
576
|
+
onCommit: (cb) => {
|
|
577
|
+
commitSubs.add(cb);
|
|
578
|
+
return () => commitSubs.delete(cb);
|
|
579
|
+
},
|
|
580
|
+
setTimer: (atMs, cb) => {
|
|
581
|
+
const h = ++timerSeq;
|
|
582
|
+
const t = { atMs, cb };
|
|
583
|
+
if (!wakeHost) {
|
|
584
|
+
t.timeout = setTimeout(() => {
|
|
585
|
+
timers.delete(h);
|
|
586
|
+
cb();
|
|
587
|
+
}, Math.max(0, atMs - (options.now?.() ?? Date.now())));
|
|
588
|
+
}
|
|
589
|
+
timers.set(h, t);
|
|
590
|
+
rearm();
|
|
591
|
+
return h;
|
|
592
|
+
},
|
|
593
|
+
clearTimer: (h) => {
|
|
594
|
+
const t = timers.get(h);
|
|
595
|
+
if (t) {
|
|
596
|
+
if (t.timeout !== void 0) clearTimeout(t.timeout);
|
|
597
|
+
timers.delete(h);
|
|
598
|
+
rearm();
|
|
599
|
+
}
|
|
600
|
+
},
|
|
601
|
+
now: () => options.now?.() ?? Date.now(),
|
|
602
|
+
backstopMs,
|
|
603
|
+
// Same resolver as `create()`'s local `functionKind` closure (used elsewhere for
|
|
604
|
+
// `ComponentContext.functionKind`) — reused here, not recomputed, so a driver's path
|
|
605
|
+
// validation (e.g. `@helipod/triggers`' boot-time handler check) and every other kind
|
|
606
|
+
// lookup in this runtime always agree.
|
|
607
|
+
functionKind,
|
|
608
|
+
readLog: async (opts) => {
|
|
609
|
+
const store = options.store;
|
|
610
|
+
const afterTs = BigInt(Math.trunc(opts.afterTs));
|
|
611
|
+
const stable = options.stablePrefix ? await options.stablePrefix() : null;
|
|
612
|
+
const bound = stable ?? await store.maxTimestamp();
|
|
613
|
+
if (bound <= afterTs) return { changes: [], maxScannedTs: Number(afterTs) };
|
|
614
|
+
if (opts.limit === 0) return { changes: [], maxScannedTs: Number(bound) };
|
|
615
|
+
const limit = opts.limit;
|
|
616
|
+
const scanned = [];
|
|
617
|
+
for await (const e of store.load_documents(
|
|
618
|
+
{ minInclusive: afterTs + 1n, maxExclusive: bound + 1n },
|
|
619
|
+
"asc",
|
|
620
|
+
limit
|
|
621
|
+
)) {
|
|
622
|
+
scanned.push(e);
|
|
623
|
+
}
|
|
624
|
+
let maxScannedTs;
|
|
625
|
+
let rows;
|
|
626
|
+
const limitHit = limit !== void 0 && scanned.length === limit;
|
|
627
|
+
if (!limitHit) {
|
|
628
|
+
maxScannedTs = bound;
|
|
629
|
+
rows = scanned;
|
|
630
|
+
} else {
|
|
631
|
+
const lastTs = scanned[scanned.length - 1].ts;
|
|
632
|
+
if (lastTs > afterTs + 1n) {
|
|
633
|
+
maxScannedTs = lastTs - 1n;
|
|
634
|
+
rows = scanned.filter((e) => e.ts < lastTs);
|
|
635
|
+
} else {
|
|
636
|
+
maxScannedTs = lastTs;
|
|
637
|
+
rows = [];
|
|
638
|
+
for await (const e of store.load_documents(
|
|
639
|
+
{ minInclusive: lastTs, maxExclusive: lastTs + 1n },
|
|
640
|
+
"asc"
|
|
641
|
+
)) {
|
|
642
|
+
rows.push(e);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
const tableFilter = opts.tables ? new Set(opts.tables) : null;
|
|
647
|
+
const changes = [];
|
|
648
|
+
for (const e of rows) {
|
|
649
|
+
const name = tableNumberToName.get(e.id.tableNumber);
|
|
650
|
+
if (name === void 0 || name.includes("/") || name.startsWith("_")) continue;
|
|
651
|
+
if (tableFilter && !tableFilter.has(name)) continue;
|
|
652
|
+
const op = e.value === null ? "delete" : e.prev_ts === null ? "insert" : "update";
|
|
653
|
+
const newDoc = e.value === null ? null : convexToJson2(e.value.value);
|
|
654
|
+
let oldDoc = null;
|
|
655
|
+
if (e.prev_ts !== null) {
|
|
656
|
+
const prev = await store.get(e.id, e.prev_ts);
|
|
657
|
+
oldDoc = prev === null ? null : convexToJson2(prev.value.value);
|
|
658
|
+
}
|
|
659
|
+
const idStr = encodeInternalDocumentId(e.id);
|
|
660
|
+
const tsNum = Number(e.ts);
|
|
661
|
+
changes.push({
|
|
662
|
+
table: name,
|
|
663
|
+
id: idStr,
|
|
664
|
+
op,
|
|
665
|
+
newDoc,
|
|
666
|
+
oldDoc,
|
|
667
|
+
ts: tsNum,
|
|
668
|
+
changeId: `${name}:${idStr}:${tsNum}`
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
return { changes, maxScannedTs: Number(maxScannedTs) };
|
|
672
|
+
},
|
|
673
|
+
// M2c Task 6: `handler` (the `SyncProtocolHandler` instance) is already constructed above
|
|
674
|
+
// (`const handler = new SyncProtocolHandler(...)`) by the time this `driverCtx` object is
|
|
675
|
+
// built, so both hooks are plain delegations — no chicken-and-egg problem the way a
|
|
676
|
+
// boot-layer-constructed poller would have (`runtime.handler` doesn't exist until
|
|
677
|
+
// `createEmbeddedRuntime` RETURNS; this closure runs before that, with `handler` already live).
|
|
678
|
+
notifyWrites: (inv) => handler.notifyWrites(inv),
|
|
679
|
+
subscribedGlobalTables: () => handler.subscribedGlobalTables(),
|
|
680
|
+
// M2c fix: plain delegation, same reasoning as the two hooks above — `handler` is already
|
|
681
|
+
// constructed by the time this `driverCtx` object is built.
|
|
682
|
+
onGlobalSubscribe: (cb) => handler.onGlobalSubscribe(cb)
|
|
683
|
+
};
|
|
684
|
+
const tableNumberToName = /* @__PURE__ */ new Map();
|
|
685
|
+
rebuildTableNumberToName(tableNumberToName, options.tableNumbers ?? {});
|
|
686
|
+
const namesForCommit = (tableIds) => translateTableIds(tableIds, tableNumberToName);
|
|
687
|
+
adapter.subscribe((payload) => {
|
|
688
|
+
handler.registerOriginResponseGate(payload.commitTs, payload.origin);
|
|
689
|
+
queue.push({
|
|
690
|
+
tables: payload.tables,
|
|
691
|
+
ranges: payload.ranges,
|
|
692
|
+
commitTs: payload.commitTs,
|
|
693
|
+
origin: payload.origin,
|
|
694
|
+
writtenDocs: payload.writtenDocs
|
|
695
|
+
});
|
|
696
|
+
void drain();
|
|
697
|
+
if (commitSubs.size > 0) {
|
|
698
|
+
fireCommitSubs(commitSubs, { tables: namesForCommit(payload.tables), ranges: payload.ranges, commitTs: payload.commitTs });
|
|
699
|
+
}
|
|
700
|
+
});
|
|
701
|
+
if ((options.drivers?.length ?? 0) > 0 && !options.tableNumbers) {
|
|
702
|
+
console.warn(
|
|
703
|
+
"[runtime] drivers registered but no `tableNumbers` provided \u2014 driver reactive wake (onCommit) will not match table-name filters; drivers will fall back to their own timers only."
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
const drivers = options.drivers ?? [];
|
|
707
|
+
let driversStarted = false;
|
|
708
|
+
if (!options.deferDrivers) {
|
|
709
|
+
for (const d of drivers) await d.start(driverCtx);
|
|
710
|
+
driversStarted = true;
|
|
711
|
+
}
|
|
712
|
+
return new _EmbeddedRuntime(
|
|
713
|
+
options.store,
|
|
714
|
+
executor,
|
|
715
|
+
handler,
|
|
716
|
+
adapter,
|
|
717
|
+
modules,
|
|
718
|
+
systemModules,
|
|
719
|
+
adminModules,
|
|
720
|
+
componentNames,
|
|
721
|
+
contextProviders,
|
|
722
|
+
policyRegistry,
|
|
723
|
+
policyProviders,
|
|
724
|
+
relationRegistry,
|
|
725
|
+
drivers,
|
|
726
|
+
timers,
|
|
727
|
+
fireDueTimers,
|
|
728
|
+
tableNumberToName,
|
|
729
|
+
oracle,
|
|
730
|
+
queryOracle,
|
|
731
|
+
transactor,
|
|
732
|
+
driverCtx,
|
|
733
|
+
driversStarted,
|
|
734
|
+
options.writeRouter,
|
|
735
|
+
numShards,
|
|
736
|
+
options.catalog,
|
|
737
|
+
commitSubs
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Resolves a target path's REAL registered kind against the live `this.modules` map — same
|
|
742
|
+
* resolver shape as `create()`'s local `functionKind` closure, but bound to the instance so it
|
|
743
|
+
* stays correct across `setModules` hot-swaps. See `ComponentContext.functionKind`'s doc
|
|
744
|
+
* comment (packages/executor/src/executor.ts).
|
|
745
|
+
*/
|
|
746
|
+
functionKind = (path) => this.modules[path]?.type;
|
|
747
|
+
/** Hot-swap the function map (dev reload) without disturbing the store/transactor. */
|
|
748
|
+
setModules(modules) {
|
|
749
|
+
for (const key of Object.keys(this.modules)) delete this.modules[key];
|
|
750
|
+
Object.assign(this.modules, modules);
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* Rebuild the tableNumber→name map after an additive deploy so driver commit fan-out
|
|
754
|
+
* (`namesForCommit` in `create()`, which closed over this same `Map` instance) keeps
|
|
755
|
+
* translating newly-added tables' encoded storage ids to their full names correctly.
|
|
756
|
+
* Additive deploys keep existing numbers, so this only ever adds entries in practice.
|
|
757
|
+
*/
|
|
758
|
+
setTableNumbers(tableNumbers) {
|
|
759
|
+
rebuildTableNumberToName(this.tableNumberToName, tableNumbers);
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* The FOREIGN-COMMIT driver wake for a multi-writer fleet hybrid (Fleet B3, trigger-wake gap
|
|
763
|
+
* fix). `commitSubs` (driver `onCommit` wakes) normally fires only from `create()`'s
|
|
764
|
+
* `adapter.subscribe` callback — the LOCAL commit fan-out. In an opt-in multi-writer fleet, a
|
|
765
|
+
* co-writer's commit reaches THIS node only through the hybrid-tailer `invalidationSink`
|
|
766
|
+
* (`ee/packages/fleet/src/node.ts`), which calls `handler.notifyWrites` directly — bypassing
|
|
767
|
+
* `adapter.subscribe` entirely, so a driver here (e.g. `@helipod/triggers`) never woke on a
|
|
768
|
+
* foreign writer's commit and instead slept up to its own wall-clock beat. Delivery was always
|
|
769
|
+
* guaranteed (the durable cursor over the log) — this is a LATENCY fix, not a correctness one.
|
|
770
|
+
*
|
|
771
|
+
* The caller (the fleet's `invalidationSink`) invokes this after `notifyWrites`, passing the
|
|
772
|
+
* SAME derived invalidation. Translates `inv.tables` with the identical `translateTableIds`
|
|
773
|
+
* helper the local path uses, so a driver's `t.startsWith("scheduler/")`-style filter matches
|
|
774
|
+
* either source the same way. A local commit and a foreign commit that happen to touch the same
|
|
775
|
+
* table may both wake a driver for what is conceptually "the same" change window — harmless,
|
|
776
|
+
* since driver wakes are level-triggered (a driver re-checks its own state, it doesn't trust the
|
|
777
|
+
* wake payload as the sole source of truth).
|
|
778
|
+
*
|
|
779
|
+
* A no-op on any node with no registered drivers (`commitSubs` empty) — cheap to call
|
|
780
|
+
* unconditionally from every fleet node, writer-ish or not.
|
|
781
|
+
*/
|
|
782
|
+
notifyExternalCommit(inv) {
|
|
783
|
+
if (this.commitSubs.size === 0) return;
|
|
784
|
+
fireCommitSubs(this.commitSubs, { tables: translateTableIds(inv.tables, this.tableNumberToName), ranges: inv.ranges, commitTs: inv.commitTs });
|
|
785
|
+
}
|
|
786
|
+
/**
|
|
787
|
+
* Live view of the registered app+component function paths. Reads the same mutable `modules`
|
|
788
|
+
* map `setModules` hot-swaps in place, so counts stay correct across a dev reload or deploy
|
|
789
|
+
* (the boot-time snapshot the server used to cache went stale after the first hot-swap).
|
|
790
|
+
*/
|
|
791
|
+
functionPaths() {
|
|
792
|
+
return Object.keys(this.modules);
|
|
793
|
+
}
|
|
794
|
+
/** Live view of the registered table names (mirrors `functionPaths`, reads the live map). */
|
|
795
|
+
tableNames() {
|
|
796
|
+
return [...this.tableNumberToName.values()];
|
|
797
|
+
}
|
|
798
|
+
/** Open an in-process connection an unmodified client can talk to. */
|
|
799
|
+
connect(sessionId) {
|
|
800
|
+
return createLoopbackConnection(this.handler, sessionId ?? `session-${++this.sessionCounter}`);
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* Synthesizes a `UdfResult` for a write forwarded to the writer node: `forward` only returns
|
|
804
|
+
* the function's JSON result, not read ranges / an oplog, so those come back empty/zero here.
|
|
805
|
+
* Shared by `run()` and `runAction()` — both route through the same `WriteRouter`.
|
|
806
|
+
*/
|
|
807
|
+
forwardedResult(value) {
|
|
808
|
+
return { value: jsonToConvex(value), logs: [], committed: true, commitTs: 0n, readRanges: [], oplog: null };
|
|
809
|
+
}
|
|
810
|
+
/** Directly invoke a function (for HTTP routes / the CLI `run` command). A MUTATION routes
|
|
811
|
+
* per-shard inside the executor (`executor.run` forwards a shard this node doesn't own); an
|
|
812
|
+
* ACTION forwards wholesale here to the default-shard holder; queries always run locally. */
|
|
813
|
+
async run(path, args, opts) {
|
|
814
|
+
if (isInternalPath(path)) throw new FunctionNotFoundError(`unknown function: ${path}`);
|
|
815
|
+
const fn = this.modules[path];
|
|
816
|
+
if (!fn) throw new FunctionNotFoundError(`unknown function: ${path}`);
|
|
817
|
+
if (fn.type === "action" && this.writeRouter && !this.writeRouter.isLocalWriter(DEFAULT_SHARD)) {
|
|
818
|
+
const res = await this.writeRouter.forward("action", path, args, opts?.identity ?? null, DEFAULT_SHARD);
|
|
819
|
+
return this.forwardedResult(res.value);
|
|
820
|
+
}
|
|
821
|
+
if (fn.type === "mutation" && opts?.dedup) {
|
|
822
|
+
return this.runMutationClassified(fn, path, args, opts.identity ?? null, opts.dedup, opts.commitMeta);
|
|
823
|
+
}
|
|
824
|
+
return this.executor.run(fn, jsonToConvex(args), {
|
|
825
|
+
path,
|
|
826
|
+
namespace: namespaceForPath(path, this.componentNames),
|
|
827
|
+
contextProviders: this.contextProviders,
|
|
828
|
+
policyRegistry: this.policyRegistry,
|
|
829
|
+
policyProviders: this.policyProviders,
|
|
830
|
+
relationRegistry: this.relationRegistry,
|
|
831
|
+
functionKind: this.functionKind,
|
|
832
|
+
identity: opts?.identity ?? null,
|
|
833
|
+
numShards: this.numShards,
|
|
834
|
+
// Fleet B3, D3 (effectively-once forwarding): opaque commit metadata threaded straight through
|
|
835
|
+
// to `Transactor.runInTransaction`'s `commitMeta` — meaningful only for a mutation that
|
|
836
|
+
// actually commits (see `RunOptions.commitMeta`'s doc comment). `packages/cli`'s `/_fleet/run`
|
|
837
|
+
// handler is the one caller that sets this, carrying a forwarded write's idempotency key.
|
|
838
|
+
commitMeta: opts?.commitMeta
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* The OWNER-side dedup classification for a mutation reaching `run()` with a dedup key (the fleet
|
|
843
|
+
* `/_fleet/run` forward path). Shares the exact classify → run-with-guard → replay-on-collision
|
|
844
|
+
* logic the sync `runMutation` uses; a replay is surfaced as a `UdfResult` with `clientReplay` set
|
|
845
|
+
* (the `/_fleet/run` handler serializes it to a replay body). `base` is any pre-existing commit
|
|
846
|
+
* meta (e.g. fleet's `idempotencyKey`) — the dedup keys merge onto it (disjoint keys).
|
|
847
|
+
*/
|
|
848
|
+
async runMutationClassified(fn, path, args, identity, dedup, base) {
|
|
849
|
+
const pre = await classifyDedup(this.store, identity, dedup);
|
|
850
|
+
if (pre) return this.replayResult(pre);
|
|
851
|
+
const commitMeta = dedupCommitMeta(identity, dedup, base);
|
|
852
|
+
let r;
|
|
853
|
+
try {
|
|
854
|
+
r = await this.executor.run(fn, jsonToConvex(args), {
|
|
855
|
+
path,
|
|
856
|
+
namespace: namespaceForPath(path, this.componentNames),
|
|
857
|
+
contextProviders: this.contextProviders,
|
|
858
|
+
policyRegistry: this.policyRegistry,
|
|
859
|
+
policyProviders: this.policyProviders,
|
|
860
|
+
relationRegistry: this.relationRegistry,
|
|
861
|
+
functionKind: this.functionKind,
|
|
862
|
+
identity,
|
|
863
|
+
numShards: this.numShards,
|
|
864
|
+
commitMeta
|
|
865
|
+
});
|
|
866
|
+
} catch (e) {
|
|
867
|
+
const replay = await handleDedupError(this.store, identity, dedup, e);
|
|
868
|
+
if (replay) return this.replayResult(replay);
|
|
869
|
+
throw e;
|
|
870
|
+
}
|
|
871
|
+
if (!r.committed) {
|
|
872
|
+
await recordZeroWriteApplied(this.store, identity, dedup, r.commitTs, r.value);
|
|
873
|
+
} else if (r.oplog !== null) {
|
|
874
|
+
await fillWriteMutationValue(this.store, identity, dedup, r.value);
|
|
875
|
+
}
|
|
876
|
+
return r;
|
|
877
|
+
}
|
|
878
|
+
/** Wrap a {@link ClientReplay} as a `UdfResult` carrying `clientReplay` — no commit happened. */
|
|
879
|
+
replayResult(replay) {
|
|
880
|
+
return {
|
|
881
|
+
value: jsonToConvex(replay.value ?? null),
|
|
882
|
+
logs: [],
|
|
883
|
+
committed: false,
|
|
884
|
+
commitTs: replay.commitTs !== void 0 ? BigInt(replay.commitTs) : 0n,
|
|
885
|
+
readRanges: [],
|
|
886
|
+
oplog: null,
|
|
887
|
+
clientReplay: replay
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
/** Directly invoke an action (for HTTP routes / the CLI `run` command). Public gate: blocks
|
|
891
|
+
* `_`-prefixed paths. Routes through `writeRouter` when set and this node isn't the writer. */
|
|
892
|
+
async runAction(path, args, opts) {
|
|
893
|
+
if (isInternalPath(path)) throw new FunctionNotFoundError(`unknown function: ${path}`);
|
|
894
|
+
const fn = this.modules[path];
|
|
895
|
+
if (!fn) throw new FunctionNotFoundError(`unknown function: ${path}`);
|
|
896
|
+
if (fn.type !== "action") throw new Error(`${path} is not an action`);
|
|
897
|
+
if (this.writeRouter && !this.writeRouter.isLocalWriter(DEFAULT_SHARD)) {
|
|
898
|
+
const res = await this.writeRouter.forward("action", path, args, opts?.identity ?? null, DEFAULT_SHARD);
|
|
899
|
+
return this.forwardedResult(res.value);
|
|
900
|
+
}
|
|
901
|
+
return this.executor.run(fn, jsonToConvex(args), {
|
|
902
|
+
path,
|
|
903
|
+
namespace: namespaceForPath(path, this.componentNames),
|
|
904
|
+
contextProviders: this.contextProviders,
|
|
905
|
+
policyRegistry: this.policyRegistry,
|
|
906
|
+
policyProviders: this.policyProviders,
|
|
907
|
+
relationRegistry: this.relationRegistry,
|
|
908
|
+
functionKind: this.functionKind,
|
|
909
|
+
identity: opts?.identity ?? null,
|
|
910
|
+
numShards: this.numShards
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
/** Directly invoke an httpAction (for the public HTTP router). Passes the raw `Request` through
|
|
914
|
+
* untouched and returns the handler's `Response`. Public gate: blocks `_`-prefixed paths. */
|
|
915
|
+
async runHttpAction(path, request, opts) {
|
|
916
|
+
if (isInternalPath(path)) throw new FunctionNotFoundError(`unknown function: ${path}`);
|
|
917
|
+
const fn = this.modules[path];
|
|
918
|
+
if (!fn) throw new FunctionNotFoundError(`unknown function: ${path}`);
|
|
919
|
+
if (fn.type !== "httpAction") throw new Error(`${path} is not an httpAction`);
|
|
920
|
+
const result = await this.executor.run(fn, request, {
|
|
921
|
+
path,
|
|
922
|
+
namespace: namespaceForPath(path, this.componentNames),
|
|
923
|
+
contextProviders: this.contextProviders,
|
|
924
|
+
policyRegistry: this.policyRegistry,
|
|
925
|
+
policyProviders: this.policyProviders,
|
|
926
|
+
relationRegistry: this.relationRegistry,
|
|
927
|
+
functionKind: this.functionKind,
|
|
928
|
+
identity: opts?.identity ?? null,
|
|
929
|
+
numShards: this.numShards
|
|
930
|
+
});
|
|
931
|
+
return result.value;
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* The privileged built-in doc mutations whose target is a USER table (and thus may be sharded).
|
|
935
|
+
* These are the ONLY `runSystem` paths that need shard routing — every other `_system:*`/
|
|
936
|
+
* `_storage:*`/`_test:*` built-in writes UNSHARDED component-internal tables, which are owned by
|
|
937
|
+
* the default ring (INSERT from any ring; RMW on default), so they need no override.
|
|
938
|
+
*/
|
|
939
|
+
static DOC_MUTATION_PATHS = /* @__PURE__ */ new Set([
|
|
940
|
+
"_system:patchDocument",
|
|
941
|
+
"_system:deleteDocument",
|
|
942
|
+
"_system:insertDocument"
|
|
943
|
+
]);
|
|
944
|
+
/** Run a privileged built-in (`_system:*`) function. Trusted callers only (the admin API).
|
|
945
|
+
* For a doc mutation on a user table, the target document's OWNING shard is resolved and passed
|
|
946
|
+
* through so the privileged write lands on the same ring a user's sharded mutation of that doc
|
|
947
|
+
* would — one-doc-one-ring. `opts.shardId` lets a trusted caller override the resolution. */
|
|
948
|
+
async runSystem(path, args, opts) {
|
|
949
|
+
const fn = this.systemModules[path];
|
|
950
|
+
if (!fn) throw new FunctionNotFoundError(`unknown system function: ${path}`);
|
|
951
|
+
let shardId = opts?.shardId;
|
|
952
|
+
if (shardId === void 0 && this.numShards > 1 && _EmbeddedRuntime.DOC_MUTATION_PATHS.has(path)) {
|
|
953
|
+
shardId = await this.resolveDocMutationShard(path, args);
|
|
954
|
+
}
|
|
955
|
+
return this.executor.run(fn, jsonToConvex(args), {
|
|
956
|
+
path,
|
|
957
|
+
privileged: true,
|
|
958
|
+
numShards: this.numShards,
|
|
959
|
+
shardId,
|
|
960
|
+
// Fleet B3, D3: see `run()`'s doc comment above — the forwarded-`_system:*`-doc-mutation path
|
|
961
|
+
// (an admin dashboard edit landing on a non-owner) threads the same idempotency key through.
|
|
962
|
+
commitMeta: opts?.commitMeta
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* Resolve the owning shard for a privileged admin doc mutation so the write commits on the SAME
|
|
967
|
+
* ring a user's sharded mutation of that document would use (the one-doc-one-ring invariant).
|
|
968
|
+
* Without this, a dashboard edit of a sharded doc runs on the default ring and forks the doc's
|
|
969
|
+
* prev_ts chain against its home-shard writer — a permanent tailer halt + a silently-lost update.
|
|
970
|
+
*
|
|
971
|
+
* The shard-key field is IMMUTABLE after insert, so peeking the current doc's key value BEFORE the
|
|
972
|
+
* transaction is race-free (its shard can't have changed by the time the txn opens). An unsharded
|
|
973
|
+
* target (component tables, or app tables with no `.shardKey`) resolves to `"default"` — its RMW
|
|
974
|
+
* ring per the same invariant. Called only for `DOC_MUTATION_PATHS` when `numShards > 1`.
|
|
975
|
+
*/
|
|
976
|
+
async resolveDocMutationShard(path, args) {
|
|
977
|
+
const a = args;
|
|
978
|
+
if (path === "_system:insertDocument") {
|
|
979
|
+
const meta2 = this.catalog.getTable(a.table);
|
|
980
|
+
if (!meta2?.shardKey) return DEFAULT_SHARD;
|
|
981
|
+
const fields = a.fields;
|
|
982
|
+
return shardIdForKeyValue(fields?.[meta2.shardKey], this.numShards);
|
|
983
|
+
}
|
|
984
|
+
const internalId = decodeDocumentId(a.id);
|
|
985
|
+
const meta = this.catalog.getTableByNumber(internalId.tableNumber);
|
|
986
|
+
if (!meta?.shardKey) return DEFAULT_SHARD;
|
|
987
|
+
const latest = await this.store.get(internalId);
|
|
988
|
+
if (!latest) return DEFAULT_SHARD;
|
|
989
|
+
const doc = latest.value.value;
|
|
990
|
+
return shardIdForKeyValue(doc[meta.shardKey], this.numShards);
|
|
991
|
+
}
|
|
992
|
+
/** Run a privileged admin built-in (`_admin:*`) once (e.g. for the HTTP fallback). Trusted callers only. */
|
|
993
|
+
async runAdmin(path, args) {
|
|
994
|
+
const fn = this.adminModules[path];
|
|
995
|
+
if (!fn) throw new FunctionNotFoundError(`unknown admin function: ${path}`);
|
|
996
|
+
return this.executor.run(fn, jsonToConvex(args), { path, privileged: true, numShards: this.numShards });
|
|
997
|
+
}
|
|
998
|
+
/**
|
|
999
|
+
* Run every driver timer that is due (`atMs <= now()`), drop it, and re-arm the host to the new
|
|
1000
|
+
* minimum — the firing half of the `EmbeddedRuntimeOptions.wakeHost` seam, called when the host's
|
|
1001
|
+
* alarm goes off (`serve`'s `POST /_admin/wake`). It cannot be a direct call: the alarm lives in
|
|
1002
|
+
* the host (a Durable Object), across a network boundary from this process.
|
|
1003
|
+
*
|
|
1004
|
+
* Safe and cheap on both paths through it, so a host never branches:
|
|
1005
|
+
* - the process was STOPPED (the common case) — the wake request BOOTS it, the drivers' own
|
|
1006
|
+
* `start()` re-derives everything from committed state, and this finds nothing due: a no-op.
|
|
1007
|
+
* - the process was RUNNING — this runs the actual pending callback.
|
|
1008
|
+
*
|
|
1009
|
+
* Also a no-op without a `wakeHost` (nothing arms a wake, and `setTimeout` already fired
|
|
1010
|
+
* everything due), so the route being registered on every deployment costs nothing.
|
|
1011
|
+
*/
|
|
1012
|
+
fireDueTimers() {
|
|
1013
|
+
this.fireDue();
|
|
1014
|
+
}
|
|
1015
|
+
/**
|
|
1016
|
+
* Stop the component drivers and clear their pending timers, resetting `driversStarted` so a later
|
|
1017
|
+
* `startDrivers()` can bring them back up. Shared by the driver-only `stopDriversOnly()` (B2b, D5)
|
|
1018
|
+
* and the full-shutdown `stopDrivers()` — the ONLY difference is whether the sync handler is also
|
|
1019
|
+
* disposed, so the two can never drift on how a driver is torn down.
|
|
1020
|
+
*/
|
|
1021
|
+
async stopDriversInternal() {
|
|
1022
|
+
for (const t of this.timers.values()) if (t.timeout !== void 0) clearTimeout(t.timeout);
|
|
1023
|
+
this.timers.clear();
|
|
1024
|
+
for (const d of this.drivers) await d.stop?.();
|
|
1025
|
+
this.driversStarted = false;
|
|
1026
|
+
}
|
|
1027
|
+
/**
|
|
1028
|
+
* Stop all component drivers and clear all pending driver timers. Call on runtime shutdown.
|
|
1029
|
+
* ALSO disposes the sync handler's background flush sweep — this is the full-teardown path.
|
|
1030
|
+
*/
|
|
1031
|
+
async stopDrivers() {
|
|
1032
|
+
await this.stopDriversInternal();
|
|
1033
|
+
this.handler.dispose();
|
|
1034
|
+
}
|
|
1035
|
+
/**
|
|
1036
|
+
* Driver-only stop (B2b, D5 — "drivers follow the default shard"): stop the scheduler/workflow/cron/
|
|
1037
|
+
* reaper drivers and clear their timers, WITHOUT disposing the sync handler. A fleet node that
|
|
1038
|
+
* relinquishes (or gracefully releases) the default shard keeps serving reads, subscriptions, and
|
|
1039
|
+
* mutations for every OTHER shard — only its drivers go quiet, because a different node now owns the
|
|
1040
|
+
* default ring the scheduler tables live on. Symmetric with `startDrivers()` and idempotent both
|
|
1041
|
+
* ways (the reset flag makes a later `startDrivers()` a real restart, and a second stop a no-op),
|
|
1042
|
+
* so callers never need to track whether drivers are currently running. Deliberately NEVER touches
|
|
1043
|
+
* `handler.dispose()` (the shipped `stopDrivers()` did — fatal on a default-relinquish, since the
|
|
1044
|
+
* node stays live).
|
|
1045
|
+
*/
|
|
1046
|
+
async stopDriversOnly() {
|
|
1047
|
+
await this.stopDriversInternal();
|
|
1048
|
+
}
|
|
1049
|
+
/**
|
|
1050
|
+
* Start component drivers deferred via `EmbeddedRuntimeOptions.deferDrivers`, OR restart them after
|
|
1051
|
+
* a `stopDriversOnly()` (B2b, D5). Idempotent — a second (or later) call is a no-op once drivers are
|
|
1052
|
+
* running, so callers don't need to track whether they've already called it (e.g. a fleet node
|
|
1053
|
+
* calling this on every default-shard acquisition attempt).
|
|
1054
|
+
*/
|
|
1055
|
+
async startDrivers() {
|
|
1056
|
+
if (this.driversStarted) return;
|
|
1057
|
+
this.driversStarted = true;
|
|
1058
|
+
for (const d of this.drivers) await d.start(this.driverCtx);
|
|
1059
|
+
}
|
|
1060
|
+
/**
|
|
1061
|
+
* Lets a non-writer fleet node advance its local timestamp oracle past timestamps it learns
|
|
1062
|
+
* from the writer's change stream, so its own next allocated timestamp (if/when it becomes
|
|
1063
|
+
* the writer) never collides with or precedes one it already observed.
|
|
1064
|
+
*
|
|
1065
|
+
* Fleet B3 (D1) routing: WITHOUT a `queryStore` (`queryOracle` undefined), this is the shipped
|
|
1066
|
+
* behavior — delegates straight to the WRITE oracle (`this.oracle`; a `ShardedTransactor` fans
|
|
1067
|
+
* it to every shard). WITH a `queryStore` configured, a hybrid node has a real query-path oracle
|
|
1068
|
+
* whose sole purpose IS tailer post-apply feeding — so this routes to `queryOracle` INSTEAD, and
|
|
1069
|
+
* the write oracle is left untouched by tailer observations (it advances only via this runtime's
|
|
1070
|
+
* own local commits, through `ShardWriter.commit`'s `oracle.publishCommitted`). Sharing one
|
|
1071
|
+
* oracle between the two would let a query snapshot ABOVE the replica's actual watermark and
|
|
1072
|
+
* read holes (D1's spec-review requirement) — this branch is what keeps them separate.
|
|
1073
|
+
*
|
|
1074
|
+
* This is the READ-PATH observer (tailer/follower freshness → query snapshot). Its write-path
|
|
1075
|
+
* counterpart, `observeWriteTimestamp` (below), always targets the WRITE oracle and exists
|
|
1076
|
+
* precisely because this method stopped feeding the write side once a hybrid has a query oracle.
|
|
1077
|
+
*/
|
|
1078
|
+
observeTimestamp(ts) {
|
|
1079
|
+
(this.queryOracle ?? this.oracle).observeTimestamp(ts);
|
|
1080
|
+
}
|
|
1081
|
+
/**
|
|
1082
|
+
* Advance the WRITE transactor's timestamp oracle(s) past `ts` — the write-path counterpart to
|
|
1083
|
+
* `observeTimestamp` above, and the pre-T1 semantics of what `observeTimestamp` used to do before
|
|
1084
|
+
* hybrid routing split the two. ALWAYS targets `this.oracle` (the write side), independent of
|
|
1085
|
+
* `queryStore`/`queryOracle` routing: a `ShardedTransactor` fans `ts` to every existing shard
|
|
1086
|
+
* oracle AND raises its `observedHighWater` floor (so a shard writer CREATED later seeds at-or-past
|
|
1087
|
+
* `ts`); the single-shard `MonotonicTimestampOracle` takes it directly.
|
|
1088
|
+
*
|
|
1089
|
+
* Purpose (distinct from `observeTimestamp`): re-floor the WRITE snapshot on a shard OWNERSHIP
|
|
1090
|
+
* CHANGE. On a hybrid node `observeTimestamp` feeds the QUERY oracle, so nothing feeds the write
|
|
1091
|
+
* oracle from foreign observations anymore. A shard this node held, RELEASED (its `ShardWriter`
|
|
1092
|
+
* stays in the transactor's Map — only the fleet epoch is dropped), and later RE-ACQUIRES would
|
|
1093
|
+
* keep that `ShardWriter`'s oracle frozen at this node's own last commit; the next mutation would
|
|
1094
|
+
* snapshot BELOW an interim owner's commits and an RMW handler would compute on stale state (the
|
|
1095
|
+
* durable chain stays intact via latest-`prev_ts`, but the update is semantically lost). The fleet
|
|
1096
|
+
* calls this on EVERY shard acquisition with the lease row's `frontier_ts` — which is >= every
|
|
1097
|
+
* prior commit on that shard by the fence invariant (the commit guard writes `frontier_ts =
|
|
1098
|
+
* GREATEST(frontier_ts, commitTs)` inside each commit txn) — so it is the exact correct floor.
|
|
1099
|
+
* Idempotent/monotone: a `ts` at or below the oracle's position is a no-op (harmless on a
|
|
1100
|
+
* fresh/never-released shard, and on every non-hybrid node where it is called uniformly too).
|
|
1101
|
+
*/
|
|
1102
|
+
observeWriteTimestamp(ts) {
|
|
1103
|
+
this.oracle.observeTimestamp(ts);
|
|
1104
|
+
}
|
|
1105
|
+
/**
|
|
1106
|
+
* Run `fn` under shard `shardId`'s commit mutex IFF that shard is idle right now — the seam a fleet
|
|
1107
|
+
* writer's idle-frontier closer uses to publish a shard's frontier atomically with respect to that
|
|
1108
|
+
* shard's own commits (see `ShardedTransactor.tryRunExclusiveOnShard`). A shard is idle only when
|
|
1109
|
+
* the commit mutex is free AND no group-commit batch is staged/flushing (Fleet B4: the flush runs
|
|
1110
|
+
* OFF the mutex, so mutex-freedom alone is not enough — a mid-flush batch has ts's drawn but rows
|
|
1111
|
+
* not yet landed, and must read as busy to keep the closer from publishing a frontier above them).
|
|
1112
|
+
* Returns `true` if `fn` ran, `false` if the shard is busy (skip; retry next beat). Total across
|
|
1113
|
+
* sharded/single-shard runtimes — the single-shard transactor ignores `shardId` and uses its one
|
|
1114
|
+
* writer.
|
|
1115
|
+
*/
|
|
1116
|
+
tryRunExclusiveOnShard(shardId, fn) {
|
|
1117
|
+
return this.transactor.tryRunExclusiveOnShard(shardId, fn);
|
|
1118
|
+
}
|
|
1119
|
+
/**
|
|
1120
|
+
* Group-commit counters (Fleet B4, T4 health) — total across the transactor, whichever shape it
|
|
1121
|
+
* is: `ShardedTransactor.groupCommitStats()` aggregates over every live shard,
|
|
1122
|
+
* `SingleWriterTransactor.groupCommitStats()` mirrors it for the one writer. Both are
|
|
1123
|
+
* structurally all-zero when `EmbeddedRuntimeOptions.groupCommit` is unset/false (the underlying
|
|
1124
|
+
* `ShardWriter` never touches these fields on the single-commit path) — callers need no separate
|
|
1125
|
+
* on/off branch. The fleet health seam (`@helipod/fleet`'s `node.ts`) reads this to derive
|
|
1126
|
+
* `flushesPerSec` between successive `/api/health` reads.
|
|
1127
|
+
*/
|
|
1128
|
+
groupCommitStats() {
|
|
1129
|
+
return this.transactor.groupCommitStats();
|
|
1130
|
+
}
|
|
1131
|
+
};
|
|
1132
|
+
function createEmbeddedRuntime(options) {
|
|
1133
|
+
return EmbeddedRuntime.create(options);
|
|
1134
|
+
}
|
|
1135
|
+
export {
|
|
1136
|
+
EmbeddedRuntime,
|
|
1137
|
+
EmbeddedWriteFanout,
|
|
1138
|
+
InMemoryWriteFanoutAdapter,
|
|
1139
|
+
clientReceiptsGuard,
|
|
1140
|
+
createEmbeddedRuntime,
|
|
1141
|
+
createLoopbackConnection
|
|
1142
|
+
};
|
|
1143
|
+
//# sourceMappingURL=index.js.map
|