@absolutejs/sync 2.2.2 → 2.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.
@@ -0,0 +1,3162 @@
1
+ // @bun
2
+ var __defProp = Object.defineProperty;
3
+ var __returnValue = (v) => v;
4
+ function __exportSetter(name, newValue) {
5
+ this[name] = __returnValue.bind(null, newValue);
6
+ }
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, {
10
+ get: all[name],
11
+ enumerable: true,
12
+ configurable: true,
13
+ set: __exportSetter.bind(all, name)
14
+ });
15
+ };
16
+ var __require = import.meta.require;
17
+
18
+ // src/platform.ts
19
+ import { Type } from "@sinclair/typebox";
20
+ import { Value } from "@sinclair/typebox/value";
21
+ import { Elysia as Elysia3 } from "elysia";
22
+
23
+ // node_modules/@absolutejs/telemetry/dist/index.js
24
+ var NOOP_SPAN_CONTEXT = {
25
+ spanId: "0000000000000000",
26
+ traceFlags: 0,
27
+ traceId: "00000000000000000000000000000000"
28
+ };
29
+ var noopSpan = {
30
+ addEvent: () => noopSpan,
31
+ end: () => {},
32
+ isRecording: () => false,
33
+ recordException: () => {},
34
+ setAttribute: () => noopSpan,
35
+ setAttributes: () => noopSpan,
36
+ setStatus: () => noopSpan,
37
+ spanContext: () => NOOP_SPAN_CONTEXT,
38
+ updateName: () => noopSpan
39
+ };
40
+ var startActiveSpanNoop = (_name, optionsOrFn, maybeFn) => {
41
+ const fn = typeof optionsOrFn === "function" ? optionsOrFn : maybeFn;
42
+ return fn(noopSpan);
43
+ };
44
+ var noopTracer = {
45
+ startActiveSpan: startActiveSpanNoop,
46
+ startSpan: () => noopSpan
47
+ };
48
+ var tracerOrNoop = (provider, name, version) => provider !== undefined ? provider.getTracer(name, version) : noopTracer;
49
+ var ABS_ATTRS = {
50
+ tenant: "abs.tenant",
51
+ shardId: "abs.shard.id",
52
+ engineId: "abs.engine.id",
53
+ collection: "abs.collection",
54
+ mutation: "abs.mutation",
55
+ mutationAttempt: "abs.mutation.attempt",
56
+ subscriptionId: "abs.subscription.id",
57
+ batchSize: "abs.batch.size",
58
+ clusterMessageOrigin: "abs.cluster.origin",
59
+ jobId: "abs.job.id",
60
+ jobKind: "abs.job.kind",
61
+ jobAttempt: "abs.job.attempt",
62
+ jobMaxAttempts: "abs.job.max_attempts",
63
+ workerId: "abs.worker.id",
64
+ runtimeKey: "abs.runtime.key",
65
+ runtimePid: "abs.runtime.pid",
66
+ runtimePort: "abs.runtime.port",
67
+ runtimeExitReason: "abs.runtime.exit_reason",
68
+ runtimeReadinessMs: "abs.runtime.readiness_ms",
69
+ routeShard: "abs.route.shard",
70
+ routeDecision: "abs.route.decision",
71
+ secretName: "abs.secret.name",
72
+ secretFingerprint: "abs.secret.fingerprint",
73
+ auditKind: "abs.audit.kind"
74
+ };
75
+
76
+ // src/engine/equiJoin.ts
77
+ var shallowEqual = (a, b) => {
78
+ if (a === b) {
79
+ return true;
80
+ }
81
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) {
82
+ return false;
83
+ }
84
+ const aKeys = Object.keys(a);
85
+ const bKeys = Object.keys(b);
86
+ return aKeys.length === bKeys.length && aKeys.every((key) => a[key] === b[key]);
87
+ };
88
+ var addToIndex = (index, joinValue, key) => {
89
+ let bucket = index.get(joinValue);
90
+ if (bucket === undefined) {
91
+ bucket = new Set;
92
+ index.set(joinValue, bucket);
93
+ }
94
+ bucket.add(key);
95
+ };
96
+ var removeFromIndex = (index, joinValue, key) => {
97
+ const bucket = index.get(joinValue);
98
+ if (bucket === undefined) {
99
+ return;
100
+ }
101
+ bucket.delete(key);
102
+ if (bucket.size === 0) {
103
+ index.delete(joinValue);
104
+ }
105
+ };
106
+ var createEquiJoin = (options) => {
107
+ const { leftKey, rightKey, leftOn, rightOn, select, selectUnmatched } = options;
108
+ const equals = options.equals ?? shallowEqual;
109
+ const lefts = new Map;
110
+ const rights = new Map;
111
+ const leftByJoin = new Map;
112
+ const rightByJoin = new Map;
113
+ const output = new Map;
114
+ const outByLeft = new Map;
115
+ const outKey = (lk, rk) => `${lk} ${rk}`;
116
+ const unmatchedKey = (lk) => `${lk} ~`;
117
+ const leftOutputs = (lk, left) => {
118
+ const result = new Map;
119
+ const rks = rightByJoin.get(leftOn(left));
120
+ if (rks !== undefined && rks.size > 0) {
121
+ for (const rk of rks) {
122
+ const right = rights.get(rk);
123
+ if (right !== undefined) {
124
+ result.set(outKey(lk, rk), select(left, right));
125
+ }
126
+ }
127
+ } else if (selectUnmatched !== undefined) {
128
+ result.set(unmatchedKey(lk), selectUnmatched(left));
129
+ }
130
+ return result;
131
+ };
132
+ const reconcileLeft = (lk, after) => {
133
+ const before = outByLeft.get(lk) ?? new Set;
134
+ const added = [];
135
+ const removed = [];
136
+ const changed = [];
137
+ for (const [ok, value] of after) {
138
+ const previous = output.get(ok);
139
+ if (previous === undefined) {
140
+ added.push(value);
141
+ } else if (!equals(previous, value)) {
142
+ changed.push(value);
143
+ }
144
+ output.set(ok, value);
145
+ }
146
+ for (const ok of before) {
147
+ if (!after.has(ok)) {
148
+ const previous = output.get(ok);
149
+ if (previous !== undefined) {
150
+ removed.push(previous);
151
+ output.delete(ok);
152
+ }
153
+ }
154
+ }
155
+ if (after.size === 0) {
156
+ outByLeft.delete(lk);
157
+ } else {
158
+ outByLeft.set(lk, new Set(after.keys()));
159
+ }
160
+ return { added, removed, changed };
161
+ };
162
+ const mergeInto = (target, diff) => {
163
+ target.added.push(...diff.added);
164
+ target.removed.push(...diff.removed);
165
+ target.changed.push(...diff.changed);
166
+ };
167
+ return {
168
+ hydrate: (left, right) => {
169
+ lefts.clear();
170
+ rights.clear();
171
+ leftByJoin.clear();
172
+ rightByJoin.clear();
173
+ output.clear();
174
+ outByLeft.clear();
175
+ for (const right_ of right) {
176
+ const rk = rightKey(right_);
177
+ rights.set(rk, right_);
178
+ addToIndex(rightByJoin, rightOn(right_), rk);
179
+ }
180
+ for (const left_ of left) {
181
+ const lk = leftKey(left_);
182
+ lefts.set(lk, left_);
183
+ addToIndex(leftByJoin, leftOn(left_), lk);
184
+ const outs = leftOutputs(lk, left_);
185
+ for (const [ok, value] of outs) {
186
+ output.set(ok, value);
187
+ }
188
+ if (outs.size > 0) {
189
+ outByLeft.set(lk, new Set(outs.keys()));
190
+ }
191
+ }
192
+ },
193
+ applyLeft: ({ op, row }) => {
194
+ const lk = leftKey(row);
195
+ const existing = lefts.get(lk);
196
+ if (existing !== undefined) {
197
+ removeFromIndex(leftByJoin, leftOn(existing), lk);
198
+ }
199
+ if (op === "delete") {
200
+ lefts.delete(lk);
201
+ } else {
202
+ lefts.set(lk, row);
203
+ addToIndex(leftByJoin, leftOn(row), lk);
204
+ }
205
+ const after = op === "delete" ? new Map : leftOutputs(lk, row);
206
+ return reconcileLeft(lk, after);
207
+ },
208
+ applyRight: ({ op, row }) => {
209
+ const rk = rightKey(row);
210
+ const existing = rights.get(rk);
211
+ const affected = new Set;
212
+ const addAffected = (joinValue) => {
213
+ for (const lk of leftByJoin.get(joinValue) ?? []) {
214
+ affected.add(lk);
215
+ }
216
+ };
217
+ if (existing !== undefined) {
218
+ addAffected(rightOn(existing));
219
+ removeFromIndex(rightByJoin, rightOn(existing), rk);
220
+ }
221
+ if (op === "delete") {
222
+ rights.delete(rk);
223
+ } else {
224
+ rights.set(rk, row);
225
+ addToIndex(rightByJoin, rightOn(row), rk);
226
+ addAffected(rightOn(row));
227
+ }
228
+ const diff = { added: [], removed: [], changed: [] };
229
+ for (const lk of affected) {
230
+ const left = lefts.get(lk);
231
+ if (left !== undefined) {
232
+ mergeInto(diff, reconcileLeft(lk, leftOutputs(lk, left)));
233
+ }
234
+ }
235
+ return diff;
236
+ },
237
+ rows: () => [...output.values()],
238
+ size: () => output.size
239
+ };
240
+ };
241
+
242
+ // src/engine/materializedView.ts
243
+ var emptyDiff = () => ({
244
+ added: [],
245
+ removed: [],
246
+ changed: []
247
+ });
248
+ var shallowEqual2 = (a, b) => {
249
+ if (a === b) {
250
+ return true;
251
+ }
252
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) {
253
+ return false;
254
+ }
255
+ const aKeys = Object.keys(a);
256
+ const bKeys = Object.keys(b);
257
+ if (aKeys.length !== bKeys.length) {
258
+ return false;
259
+ }
260
+ return aKeys.every((key) => a[key] === b[key]);
261
+ };
262
+ var isEmptyViewDiff = (diff) => diff.added.length === 0 && diff.removed.length === 0 && diff.changed.length === 0;
263
+ var createMaterializedView = (options) => {
264
+ const { key, match } = options;
265
+ const equals = options.equals ?? shallowEqual2;
266
+ const set = new Map;
267
+ return {
268
+ hydrate: (rows) => {
269
+ set.clear();
270
+ for (const row of rows) {
271
+ set.set(key(row), row);
272
+ }
273
+ },
274
+ reset: (rows) => {
275
+ const next = new Map;
276
+ const added = [];
277
+ const changed = [];
278
+ for (const row of rows) {
279
+ const rowKey = key(row);
280
+ next.set(rowKey, row);
281
+ const previous = set.get(rowKey);
282
+ if (previous === undefined) {
283
+ added.push(row);
284
+ } else if (!equals(previous, row)) {
285
+ changed.push(row);
286
+ }
287
+ }
288
+ const removed = [];
289
+ for (const [rowKey, previous] of set) {
290
+ if (!next.has(rowKey)) {
291
+ removed.push(previous);
292
+ }
293
+ }
294
+ set.clear();
295
+ for (const [rowKey, row] of next) {
296
+ set.set(rowKey, row);
297
+ }
298
+ return { added, removed, changed };
299
+ },
300
+ apply: ({ op, row }) => {
301
+ const rowKey = key(row);
302
+ const existing = set.get(rowKey);
303
+ if (op === "delete") {
304
+ if (existing === undefined) {
305
+ return emptyDiff();
306
+ }
307
+ set.delete(rowKey);
308
+ return { added: [], removed: [existing], changed: [] };
309
+ }
310
+ if (match(row)) {
311
+ set.set(rowKey, row);
312
+ return existing === undefined ? { added: [row], removed: [], changed: [] } : { added: [], removed: [], changed: [row] };
313
+ }
314
+ if (existing !== undefined) {
315
+ set.delete(rowKey);
316
+ return { added: [], removed: [existing], changed: [] };
317
+ }
318
+ return emptyDiff();
319
+ },
320
+ rows: () => [...set.values()],
321
+ size: () => set.size
322
+ };
323
+ };
324
+
325
+ // src/engine/retry.ts
326
+ var PG_RETRY_CODES = new Set(["40001", "40P01"]);
327
+ var isSerializationFailure = (error) => {
328
+ if (error === null || typeof error !== "object")
329
+ return false;
330
+ const code = error.code;
331
+ if (typeof code === "string" && PG_RETRY_CODES.has(code))
332
+ return true;
333
+ const cause = error.cause;
334
+ if (cause !== undefined)
335
+ return isSerializationFailure(cause);
336
+ return false;
337
+ };
338
+ var exponentialBackoff = (options = {}) => (attempt) => {
339
+ const base = options.baseMs ?? 25;
340
+ const factor = options.factor ?? 2;
341
+ const max = options.maxMs ?? 1000;
342
+ const jitter = options.jitter ?? 0.2;
343
+ const raw = Math.min(max, base * factor ** Math.max(0, attempt - 1));
344
+ const spread = raw * jitter;
345
+ return raw + (Math.random() * 2 - 1) * spread;
346
+ };
347
+
348
+ class RetriesExhaustedError extends Error {
349
+ attempts;
350
+ elapsedMs;
351
+ cause;
352
+ constructor(attempts, elapsedMs, cause) {
353
+ const message = cause instanceof Error ? cause.message : String(cause);
354
+ super(`retries exhausted after ${attempts} attempts (${elapsedMs}ms): ${message}`);
355
+ this.name = "RetriesExhaustedError";
356
+ this.attempts = attempts;
357
+ this.elapsedMs = elapsedMs;
358
+ this.cause = cause;
359
+ }
360
+ }
361
+
362
+ // src/engine/sandbox.ts
363
+ var isolatedJscModule;
364
+ var loadIsolatedJsc = async () => {
365
+ if (isolatedJscModule !== undefined)
366
+ return isolatedJscModule;
367
+ try {
368
+ isolatedJscModule = await import("@absolutejs/isolated-jsc");
369
+ return isolatedJscModule;
370
+ } catch (error) {
371
+ throw new Error('sandboxedHandler requires the optional peer "@absolutejs/isolated-jsc". Install it with: bun add @absolutejs/isolated-jsc', { cause: error });
372
+ }
373
+ };
374
+ var wrap = (source) => `
375
+ function (__callId, args, ctx) {
376
+ const userFn = (${source});
377
+ if (typeof userFn !== 'function') {
378
+ throw new Error(
379
+ 'sandboxedHandler must evaluate to (args, ctx, actions, unsafeHost) => result; got ' +
380
+ typeof userFn
381
+ );
382
+ }
383
+ const actions = {
384
+ insert: (table, data) => __dispatch(__callId, 'insert', table, data),
385
+ update: (table, data) => __dispatch(__callId, 'update', table, data),
386
+ delete: (table, row) => __dispatch(__callId, 'delete', table, row),
387
+ change: (collection, change) => __dispatch(__callId, 'change', collection, change),
388
+ now: () => __dispatch(__callId, 'now'),
389
+ fetch: (url, init) => __dispatch(__callId, 'fetch', url, init)
390
+ };
391
+ // Escape hatch \u2014 host fns the mutation explicitly opted in to.
392
+ // The Proxy means every property access is a host call; the
393
+ // engine throws if the property name isn't declared in the
394
+ // mutation's sandbox.unsafeHost map.
395
+ const unsafeHost = new Proxy({}, {
396
+ get: (_target, fnName) => {
397
+ if (typeof fnName !== 'string') return undefined;
398
+ return (...callArgs) =>
399
+ __dispatch(__callId, 'unsafeHost', fnName, callArgs);
400
+ }
401
+ });
402
+ return userFn(args, ctx, actions, unsafeHost);
403
+ }
404
+ `;
405
+ var compile = async (source, config, bridgeFetch) => {
406
+ const { Reference, createIsolatedRunner, resolveIsolatePolicy } = await loadIsolatedJsc();
407
+ const callMap = new Map;
408
+ const unsafeHost = config.unsafeHost;
409
+ const dispatch = new Reference((callId, op, ...rest) => {
410
+ const a = callMap.get(callId);
411
+ if (a === undefined) {
412
+ throw new Error(`__dispatch invoked for orphan callId ${String(callId)}`);
413
+ }
414
+ switch (op) {
415
+ case "insert":
416
+ return a.insert(rest[0], rest[1]);
417
+ case "update":
418
+ return a.update(rest[0], rest[1]);
419
+ case "delete":
420
+ return a.delete(rest[0], rest[1]);
421
+ case "change":
422
+ return a.change(rest[0], rest[1]);
423
+ case "now":
424
+ return a.now();
425
+ case "fetch":
426
+ return runBridgeFetch(bridgeFetch, rest[0], rest[1]);
427
+ case "unsafeHost": {
428
+ const fnName = rest[0];
429
+ const callArgs = rest[1] ?? [];
430
+ if (unsafeHost === undefined || typeof unsafeHost[fnName] !== "function") {
431
+ throw new Error(`sandboxedHandler called unsafeHost.${fnName}() but it was not declared in the mutation's sandbox.unsafeHost config. Declare it (and only the host fns you intend to expose) to opt in to the escape hatch.`);
432
+ }
433
+ return unsafeHost[fnName](...callArgs);
434
+ }
435
+ default:
436
+ throw new Error(`unknown sandbox action op: ${String(op)}`);
437
+ }
438
+ });
439
+ const timeoutMs = config.timeout ?? 5000;
440
+ const sourceToCall = wrap(source);
441
+ const policy = resolveIsolatePolicy("tenant-script", {
442
+ allowWorkerFallback: true,
443
+ backend: config.backend ?? "auto",
444
+ memoryLimit: config.memoryLimit ?? 32,
445
+ timeout: timeoutMs
446
+ });
447
+ const runner = createIsolatedRunner({
448
+ globals: { __dispatch: dispatch },
449
+ policy
450
+ });
451
+ await runner.precompile("sandboxedHandler", sourceToCall);
452
+ return {
453
+ callMap,
454
+ nextCallId: 1,
455
+ runner,
456
+ source: sourceToCall,
457
+ timeoutMs
458
+ };
459
+ };
460
+ var runBridgeFetch = async (config, url, init) => {
461
+ if (config === undefined) {
462
+ throw new Error("actions.fetch called but the engine has no `bridgeFetch` config \u2014 " + "pass `bridgeFetch: { ... }` to createSyncEngine.");
463
+ }
464
+ let parsed;
465
+ try {
466
+ parsed = new URL(url);
467
+ } catch {
468
+ throw new Error(`actions.fetch: invalid URL "${String(url)}"`);
469
+ }
470
+ const endpoint = config[parsed.hostname] ?? (Object.prototype.hasOwnProperty.call(config, "*") ? config["*"] : undefined);
471
+ if (endpoint === undefined) {
472
+ throw new Error(`actions.fetch: hostname "${parsed.hostname}" is not allowlisted in bridgeFetch config`);
473
+ }
474
+ const headers = { ...endpoint.headers ?? {} };
475
+ if (init?.headers !== undefined) {
476
+ const incoming = init.headers;
477
+ for (const [name, value] of Object.entries(incoming)) {
478
+ if (name.toLowerCase() === "authorization")
479
+ continue;
480
+ headers[name] = value;
481
+ }
482
+ }
483
+ if (endpoint.authorization !== undefined) {
484
+ let auth;
485
+ try {
486
+ auth = await endpoint.authorization();
487
+ } catch {
488
+ throw new Error("actions.fetch: authorization callback failed");
489
+ }
490
+ headers.Authorization = auth;
491
+ }
492
+ const response = await fetch(url, { ...init, headers });
493
+ const responseHeaders = {};
494
+ response.headers.forEach((value, name) => {
495
+ responseHeaders[name] = value;
496
+ });
497
+ const body = await response.text();
498
+ return {
499
+ body,
500
+ headers: responseHeaders,
501
+ ok: response.ok,
502
+ status: response.status,
503
+ statusText: response.statusText,
504
+ url: response.url
505
+ };
506
+ };
507
+ var makeSandboxedHandler = (source, config = {}, engineExtras) => {
508
+ let pending;
509
+ const metricsHook = engineExtras?.metricsHook;
510
+ const bridgeFetch = engineExtras?.bridgeFetch;
511
+ const getCompiled = async () => {
512
+ if (pending !== undefined) {
513
+ return pending;
514
+ }
515
+ pending = compile(source, config, bridgeFetch);
516
+ return pending;
517
+ };
518
+ return async (args, ctx, actions) => {
519
+ const compiled = await getCompiled();
520
+ const callId = compiled.nextCallId++;
521
+ compiled.callMap.set(callId, actions);
522
+ if (metricsHook === undefined) {
523
+ try {
524
+ return await compiled.runner.call("sandboxedHandler", compiled.source, [callId, args, ctx], { run: { timeout: compiled.timeoutMs } });
525
+ } catch (error) {
526
+ if (isIsolateDisposalError(error)) {
527
+ pending = undefined;
528
+ await disposeCompiled(compiled);
529
+ }
530
+ throw error;
531
+ } finally {
532
+ compiled.callMap.delete(callId);
533
+ }
534
+ }
535
+ const startedAt = performance.now();
536
+ const id = makeRandomId();
537
+ try {
538
+ const { result, metrics } = await compiled.runner.call("sandboxedHandler", compiled.source, [callId, args, ctx], { run: { timeout: compiled.timeoutMs }, withMetrics: true });
539
+ fireMetrics(metricsHook.onMetrics, {
540
+ backend: metrics.backend,
541
+ cpuMs: metrics.cpuMs,
542
+ durationMs: performance.now() - startedAt,
543
+ heapBytes: metrics.heapBytes,
544
+ id,
545
+ mutationName: metricsHook.mutationName,
546
+ ok: true,
547
+ timestamp: Date.now()
548
+ });
549
+ return result;
550
+ } catch (error) {
551
+ if (isIsolateDisposalError(error)) {
552
+ pending = undefined;
553
+ await disposeCompiled(compiled);
554
+ }
555
+ fireMetrics(metricsHook.onMetrics, {
556
+ cpuMs: 0,
557
+ durationMs: performance.now() - startedAt,
558
+ errorMessage: error instanceof Error ? error.message : String(error),
559
+ errorName: error instanceof Error ? error.name : "Error",
560
+ heapBytes: 0,
561
+ id,
562
+ mutationName: metricsHook.mutationName,
563
+ ok: false,
564
+ timestamp: Date.now()
565
+ });
566
+ throw error;
567
+ } finally {
568
+ compiled.callMap.delete(callId);
569
+ }
570
+ };
571
+ };
572
+ var makeRandomId = () => `hm_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;
573
+ var isIsolateDisposalError = (error) => error instanceof Error && (error.name === "TimeoutError" || error.name === "MemoryLimitError" || error.name === "IsolateDisposedError");
574
+ var disposeCompiled = async (compiled) => {
575
+ try {
576
+ await compiled.runner.dispose();
577
+ } catch {}
578
+ };
579
+ var fireMetrics = (hook, record) => {
580
+ let outcome;
581
+ try {
582
+ outcome = hook(record);
583
+ } catch {
584
+ return;
585
+ }
586
+ if (outcome instanceof Promise) {
587
+ outcome.catch(() => {});
588
+ }
589
+ };
590
+
591
+ // src/engine/pack.ts
592
+ class PackTableConflictError extends Error {
593
+ table;
594
+ existingPack;
595
+ newPack;
596
+ constructor(table, existingPack, newPack) {
597
+ super(`Pack "${newPack}" claims table "${table}", but "${existingPack}" already owns it. Use a tablePrefix on one of them.`);
598
+ this.name = "PackTableConflictError";
599
+ this.table = table;
600
+ this.existingPack = existingPack;
601
+ this.newPack = newPack;
602
+ }
603
+ }
604
+
605
+ class PackMissingDependencyError extends Error {
606
+ pack;
607
+ missingTable;
608
+ constructor(pack, missingTable) {
609
+ super(`Pack "${pack}" requires a reader for table "${missingTable}" but none is registered. Call engine.registerReader("${missingTable}", ...) before engine.registerPack.`);
610
+ this.name = "PackMissingDependencyError";
611
+ this.pack = pack;
612
+ this.missingTable = missingTable;
613
+ }
614
+ }
615
+ var defineSyncPack = (pack) => pack;
616
+
617
+ // src/engine/search.ts
618
+ var SEARCH_SCORE_FIELD = "_score";
619
+ var defineSearchCollection = (definition) => ({
620
+ ...definition,
621
+ kind: "search"
622
+ });
623
+
624
+ // src/engine/migrate.ts
625
+ class EngineFencedError extends Error {
626
+ reason;
627
+ constructor(reason) {
628
+ super(`[sync] Engine is fenced for migration: ${reason}`);
629
+ this.name = "EngineFencedError";
630
+ this.reason = reason;
631
+ }
632
+ }
633
+
634
+ // src/engine/syncEngine.ts
635
+ class UnauthorizedError extends Error {
636
+ constructor(subject) {
637
+ super(`Not authorized: ${subject}`);
638
+ this.name = "UnauthorizedError";
639
+ }
640
+ }
641
+
642
+ class AbortError extends Error {
643
+ constructor(reason) {
644
+ super(reason ?? "Aborted");
645
+ this.name = "AbortError";
646
+ }
647
+ }
648
+ var checkAborted = (signal) => {
649
+ if (signal?.aborted) {
650
+ throw new AbortError(signal.reason instanceof Error ? signal.reason.message : typeof signal.reason === "string" ? signal.reason : "Aborted");
651
+ }
652
+ };
653
+ var linkAbortToUnsubscribe = (signal, unsubscribe) => {
654
+ if (signal === undefined)
655
+ return;
656
+ if (signal.aborted) {
657
+ unsubscribe();
658
+ return;
659
+ }
660
+ const handler = () => {
661
+ try {
662
+ unsubscribe();
663
+ } catch {}
664
+ };
665
+ signal.addEventListener("abort", handler, { once: true });
666
+ };
667
+
668
+ class SchemaError extends Error {
669
+ constructor(table, fieldName) {
670
+ super(`Schema violation on "${table}": invalid field "${fieldName}"`);
671
+ this.name = "SchemaError";
672
+ }
673
+ }
674
+
675
+ class MissedChangesError extends Error {
676
+ requestedSince;
677
+ availableSince;
678
+ constructor(requestedSince, availableSince) {
679
+ super(`Change log no longer covers version ${requestedSince}; oldest available is ${availableSince}. ` + `Re-bootstrap and resume from ${availableSince}.`);
680
+ this.name = "MissedChangesError";
681
+ this.requestedSince = requestedSince;
682
+ this.availableSince = availableSince;
683
+ }
684
+ }
685
+
686
+ class CdcConsumerSlowError extends Error {
687
+ maxBuffer;
688
+ lastDeliveredVersion;
689
+ constructor(maxBuffer, lastDeliveredVersion) {
690
+ super(`CDC stream buffer overflowed (max ${maxBuffer}); consumer fell behind. ` + `Last delivered version: ${lastDeliveredVersion}. Resubscribe with since=${lastDeliveredVersion}.`);
691
+ this.name = "CdcConsumerSlowError";
692
+ this.maxBuffer = maxBuffer;
693
+ this.lastDeliveredVersion = lastDeliveredVersion;
694
+ }
695
+ }
696
+
697
+ class MutationQueueOverflowError extends Error {
698
+ queueLimit;
699
+ constructor(queueLimit) {
700
+ super(`Mutation queue overflowed (limit ${queueLimit}); the engine is at ` + `its mutationConcurrency cap and the waiting queue is full. ` + `Retry later or shed load at the gateway.`);
701
+ this.name = "MutationQueueOverflowError";
702
+ this.queueLimit = queueLimit;
703
+ }
704
+ }
705
+
706
+ class SubscriptionLimitError extends Error {
707
+ tenantKey;
708
+ limit;
709
+ active;
710
+ constructor(tenantKey, limit, active) {
711
+ super(`Tenant "${tenantKey}" is at the subscription cap ` + `(${active}/${limit}). Close an existing subscription before opening another.`);
712
+ this.name = "SubscriptionLimitError";
713
+ this.tenantKey = tenantKey;
714
+ this.limit = limit;
715
+ this.active = active;
716
+ }
717
+ }
718
+ var defaultKey = (row) => row.id;
719
+ var shallowEqual3 = (a, b) => {
720
+ if (a === b) {
721
+ return true;
722
+ }
723
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) {
724
+ return false;
725
+ }
726
+ const aKeys = Object.keys(a);
727
+ const bKeys = Object.keys(b);
728
+ return aKeys.length === bKeys.length && aKeys.every((k) => a[k] === b[k]);
729
+ };
730
+ var subKeyIds = new WeakMap;
731
+ var subKeyCounter = 0;
732
+ var stableValueKey = (value) => {
733
+ if (value === undefined)
734
+ return "u";
735
+ if (value === null)
736
+ return "n";
737
+ const tag = typeof value;
738
+ if (tag === "string")
739
+ return `s:${value}`;
740
+ if (tag === "number" || tag === "boolean" || tag === "bigint") {
741
+ return `${tag[0]}:${String(value)}`;
742
+ }
743
+ if (tag !== "object")
744
+ return `${tag[0]}:fn`;
745
+ try {
746
+ return `o:${JSON.stringify(value, (_k, v) => {
747
+ if (v === null || typeof v !== "object" || Array.isArray(v))
748
+ return v;
749
+ const record = v;
750
+ const sorted = {};
751
+ for (const key of Object.keys(record).sort()) {
752
+ sorted[key] = record[key];
753
+ }
754
+ return sorted;
755
+ })}`;
756
+ } catch {
757
+ const obj = value;
758
+ let id = subKeyIds.get(obj);
759
+ if (id === undefined) {
760
+ subKeyCounter += 1;
761
+ id = `i${subKeyCounter}`;
762
+ subKeyIds.set(obj, id);
763
+ }
764
+ return `i:${id}`;
765
+ }
766
+ };
767
+ var stableSubKey = (collection, params, ctx) => `${collection}|${stableValueKey(params)}|${stableValueKey(ctx)}`;
768
+ var equalsIgnoringScore = (a, b) => {
769
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) {
770
+ return a === b;
771
+ }
772
+ const strip = (value) => Object.keys(value).filter((k) => k !== SEARCH_SCORE_FIELD);
773
+ const aKeys = strip(a);
774
+ const bKeys = strip(b);
775
+ return aKeys.length === bKeys.length && aKeys.every((k) => a[k] === b[k]);
776
+ };
777
+ var createSyncEngine = (options = {}) => {
778
+ const registry = new Map;
779
+ const mutations = new Map;
780
+ const sandboxRunners = new Map;
781
+ const writers = new Map;
782
+ const readers = new Map;
783
+ const schedules = new Map;
784
+ const packTableOwners = new Map;
785
+ const registeredPacks = [];
786
+ const permissions = new Map;
787
+ for (const [table, rules] of Object.entries(options.permissions ?? {})) {
788
+ permissions.set(table, rules);
789
+ }
790
+ const readRuleFor = (table) => permissions.get(table)?.read;
791
+ const writeRuleFor = (table, op) => {
792
+ const rules = permissions.get(table);
793
+ return rules?.[op] ?? rules?.write;
794
+ };
795
+ const schemas = new Map;
796
+ for (const [table, schema] of Object.entries(options.schemas ?? {})) {
797
+ schemas.set(table, schema);
798
+ }
799
+ const crdtFields = new Map;
800
+ const validateWrite = (table, op, row) => {
801
+ const schema = schemas.get(table);
802
+ if (schema === undefined || typeof row !== "object" || row === null) {
803
+ return;
804
+ }
805
+ const record = row;
806
+ for (const [fieldName, validate] of Object.entries(schema.fields)) {
807
+ const present = fieldName in record;
808
+ if (op === "update" && !present) {
809
+ continue;
810
+ }
811
+ if (!validate(record[fieldName])) {
812
+ throw new SchemaError(table, fieldName);
813
+ }
814
+ }
815
+ };
816
+ const migrateRow = (table, row) => {
817
+ const migrate = schemas.get(table)?.migrate;
818
+ return migrate ? migrate(row) : row;
819
+ };
820
+ const reactiveSubs = new Set;
821
+ const searchSubs = new Set;
822
+ const searchIndexes = new Map;
823
+ const active = new Map;
824
+ const tableIndex = new Map;
825
+ const changeLogSize = options.changeLogSize ?? 1024;
826
+ const changeLogRetainMs = options.changeLogRetainMs ?? null;
827
+ const changeLog = [];
828
+ let version = 0;
829
+ const engineStartedAt = Date.now();
830
+ let mutationsCompleted = 0;
831
+ let mutationsFailed = 0;
832
+ let mutationsRetried = 0;
833
+ let mutationsInFlight = 0;
834
+ let connectedSources = 0;
835
+ let sourceChangesReceived = 0;
836
+ let sourceLastChangeAt = null;
837
+ const mutationWaiters = [];
838
+ let mutationsQueued = 0;
839
+ const activeFences = new Set;
840
+ const acquireMutationSlot = async () => {
841
+ const limit = options.mutationConcurrency;
842
+ if (limit === undefined) {
843
+ mutationsInFlight += 1;
844
+ return;
845
+ }
846
+ if (mutationsInFlight < limit && mutationWaiters.length === 0) {
847
+ mutationsInFlight += 1;
848
+ return;
849
+ }
850
+ const queueLimit = options.mutationQueueLimit;
851
+ if (queueLimit !== undefined && mutationsQueued >= queueLimit) {
852
+ throw new MutationQueueOverflowError(queueLimit);
853
+ }
854
+ mutationsQueued += 1;
855
+ try {
856
+ await new Promise((resolve) => {
857
+ mutationWaiters.push(resolve);
858
+ });
859
+ } finally {
860
+ mutationsQueued -= 1;
861
+ }
862
+ mutationsInFlight += 1;
863
+ };
864
+ const releaseMutationSlot = () => {
865
+ mutationsInFlight -= 1;
866
+ if (options.mutationConcurrency === undefined)
867
+ return;
868
+ const next = mutationWaiters.shift();
869
+ if (next !== undefined)
870
+ next();
871
+ };
872
+ const subscriptionsByTenant = new Map;
873
+ const acquireSubscriptionSlot = (ctx, args) => {
874
+ const cap = options.subscriptionLimit;
875
+ if (cap === undefined)
876
+ return;
877
+ const tenantKey = cap.key(ctx, args);
878
+ if (tenantKey === undefined)
879
+ return;
880
+ const active2 = subscriptionsByTenant.get(tenantKey) ?? 0;
881
+ if (active2 >= cap.max) {
882
+ throw new SubscriptionLimitError(tenantKey, cap.max, active2);
883
+ }
884
+ subscriptionsByTenant.set(tenantKey, active2 + 1);
885
+ return tenantKey;
886
+ };
887
+ const releaseSubscriptionSlot = (tenantKey) => {
888
+ if (tenantKey === undefined)
889
+ return;
890
+ const active2 = subscriptionsByTenant.get(tenantKey);
891
+ if (active2 === undefined || active2 <= 1) {
892
+ subscriptionsByTenant.delete(tenantKey);
893
+ } else {
894
+ subscriptionsByTenant.set(tenantKey, active2 - 1);
895
+ }
896
+ };
897
+ const reactiveCacheMax = options.reactiveCache?.max ?? 256;
898
+ const reactiveCacheTtlMs = options.reactiveCache?.ttlMs ?? 60000;
899
+ const cachedReruns = new Map;
900
+ const touchCacheEntry = (key, entry) => {
901
+ cachedReruns.delete(key);
902
+ cachedReruns.set(key, entry);
903
+ };
904
+ const readCacheEntry = (key) => {
905
+ if (reactiveCacheMax <= 0)
906
+ return;
907
+ const entry = cachedReruns.get(key);
908
+ if (entry === undefined)
909
+ return;
910
+ if (reactiveCacheTtlMs > 0 && entry.expiresAt < Date.now()) {
911
+ cachedReruns.delete(key);
912
+ return;
913
+ }
914
+ touchCacheEntry(key, entry);
915
+ return entry;
916
+ };
917
+ const writeCacheEntry = (entry) => {
918
+ if (reactiveCacheMax <= 0)
919
+ return;
920
+ cachedReruns.set(entry.rerunKey, entry);
921
+ while (cachedReruns.size > reactiveCacheMax) {
922
+ const oldest = cachedReruns.keys().next().value;
923
+ if (oldest === undefined)
924
+ break;
925
+ cachedReruns.delete(oldest);
926
+ }
927
+ };
928
+ const activityListeners = new Set;
929
+ const emitActivity = (event) => {
930
+ for (const listener of activityListeners) {
931
+ listener(event);
932
+ }
933
+ };
934
+ const streamSubscribers = new Set;
935
+ const runInTransaction = options.transaction;
936
+ const instanceId = options.instanceId ?? globalThis.crypto?.randomUUID?.() ?? `i${Math.random()}`;
937
+ let clusterBus;
938
+ const tracer = tracerOrNoop(options.tracerProvider, "@absolutejs/sync");
939
+ const importChangeLog = (snapshot) => {
940
+ if (version !== 0) {
941
+ throw new Error(`[sync] importChangeLog: engine already has version ${version}; ` + `restore must happen before any local writes commit.`);
942
+ }
943
+ if (snapshot.instanceId !== instanceId) {
944
+ throw new Error(`[sync] importChangeLog: snapshot instanceId "${snapshot.instanceId}" ` + `does not match this engine's instanceId "${instanceId}". ` + `Pass options.instanceId = "${snapshot.instanceId}" to createSyncEngine.`);
945
+ }
946
+ version = snapshot.version;
947
+ for (const entry of snapshot.entries) {
948
+ changeLog.push(entry);
949
+ }
950
+ while (changeLog.length > changeLogSize) {
951
+ changeLog.shift();
952
+ }
953
+ if (changeLogRetainMs !== null && changeLogRetainMs > 0) {
954
+ const cutoff = Date.now() - changeLogRetainMs;
955
+ while (changeLog.length > 0 && changeLog[0].at < cutoff) {
956
+ changeLog.shift();
957
+ }
958
+ }
959
+ return snapshot.entries.length;
960
+ };
961
+ if (options.initialChangeLog !== undefined) {
962
+ importChangeLog(options.initialChangeLog);
963
+ }
964
+ const broadcast = (changes, originVersion) => {
965
+ if (clusterBus !== undefined && changes.length > 0) {
966
+ clusterBus.publish({
967
+ changes,
968
+ origin: instanceId,
969
+ originVersion
970
+ });
971
+ }
972
+ };
973
+ const subsFor = (collection) => {
974
+ let set = active.get(collection);
975
+ if (set === undefined) {
976
+ set = new Set;
977
+ active.set(collection, set);
978
+ }
979
+ return set;
980
+ };
981
+ const addTableIndex = (table, name) => {
982
+ let set = tableIndex.get(table);
983
+ if (set === undefined) {
984
+ set = new Set;
985
+ tableIndex.set(table, set);
986
+ }
987
+ set.add(name);
988
+ };
989
+ const sideChange = (change, match) => change.op !== "delete" && match !== undefined && !match(change.row) ? { op: "delete", row: change.row } : change;
990
+ const EMPTY_DIFF = {
991
+ added: [],
992
+ removed: [],
993
+ changed: []
994
+ };
995
+ const subscriptionDiff = async (subscription, table, change) => {
996
+ if (subscription.kind === "graph") {
997
+ return subscription.instance.applyChange(table, change);
998
+ }
999
+ if (subscription.kind === "join") {
1000
+ const js = subscription.join;
1001
+ if (table === js.leftTable) {
1002
+ return js.op.applyLeft(sideChange(change, js.leftMatch));
1003
+ }
1004
+ if (table === js.rightTable) {
1005
+ return js.op.applyRight(sideChange(change, js.rightMatch));
1006
+ }
1007
+ return EMPTY_DIFF;
1008
+ }
1009
+ if (subscription.kind === "reactive") {
1010
+ return EMPTY_DIFF;
1011
+ }
1012
+ if (subscription.kind === "search") {
1013
+ return EMPTY_DIFF;
1014
+ }
1015
+ if (subscription.incremental) {
1016
+ try {
1017
+ return subscription.view.apply(change);
1018
+ } catch {
1019
+ return subscription.view.reset(await subscription.rehydrate());
1020
+ }
1021
+ }
1022
+ if (subscription.affects && !subscription.affects(change)) {
1023
+ return EMPTY_DIFF;
1024
+ }
1025
+ return subscription.view.reset(await subscription.rehydrate());
1026
+ };
1027
+ const subscriptionsForTable = function* (table) {
1028
+ const names = tableIndex.get(table);
1029
+ if (names === undefined) {
1030
+ return;
1031
+ }
1032
+ for (const name of names) {
1033
+ const set = active.get(name);
1034
+ if (set === undefined) {
1035
+ continue;
1036
+ }
1037
+ yield* set;
1038
+ }
1039
+ };
1040
+ const mergeViewDiffs = (diffs, key) => {
1041
+ const net = new Map;
1042
+ for (const diff of diffs) {
1043
+ for (const row of diff.removed) {
1044
+ const previous = net.get(key(row));
1045
+ if (previous?.state === "added") {
1046
+ net.delete(key(row));
1047
+ } else {
1048
+ net.set(key(row), { state: "removed", row });
1049
+ }
1050
+ }
1051
+ for (const row of diff.added) {
1052
+ const previous = net.get(key(row));
1053
+ net.set(key(row), {
1054
+ state: previous?.state === "removed" ? "changed" : "added",
1055
+ row
1056
+ });
1057
+ }
1058
+ for (const row of diff.changed) {
1059
+ const previous = net.get(key(row));
1060
+ net.set(key(row), {
1061
+ state: previous?.state === "added" ? "added" : "changed",
1062
+ row
1063
+ });
1064
+ }
1065
+ }
1066
+ const added = [];
1067
+ const changed = [];
1068
+ const removed = [];
1069
+ for (const { state, row } of net.values()) {
1070
+ if (state === "added") {
1071
+ added.push(row);
1072
+ } else if (state === "changed") {
1073
+ changed.push(row);
1074
+ } else {
1075
+ removed.push(row);
1076
+ }
1077
+ }
1078
+ return { added, changed, removed };
1079
+ };
1080
+ const depKey = (table, key) => `${table} ${key}`;
1081
+ const changedKeyFor = (table, change) => readers.get(table)?.key?.(change.row);
1082
+ const makeReadHandle = (ctx, readTables, readKeys, rangeDeps, applyRules = true) => {
1083
+ const readerFor = (table) => {
1084
+ const reader = readers.get(table);
1085
+ if (reader === undefined) {
1086
+ throw new Error(`No reader registered for table "${table}" \u2014 register one with engine.registerReader`);
1087
+ }
1088
+ return reader;
1089
+ };
1090
+ const ruleFor = (table) => applyRules ? readRuleFor(table) : undefined;
1091
+ return {
1092
+ all: async (table) => {
1093
+ readTables.add(table);
1094
+ const rows = [...await readerFor(table).all(ctx)].map((row) => migrateRow(table, row));
1095
+ const rule = ruleFor(table);
1096
+ return rule ? rows.filter((row) => rule(ctx, row)) : rows;
1097
+ },
1098
+ get: async (table, key) => {
1099
+ const reader = readerFor(table);
1100
+ if (reader.get === undefined) {
1101
+ throw new Error(`Reader for table "${table}" has no get(); use db.all() or add get`);
1102
+ }
1103
+ if (reader.key !== undefined) {
1104
+ readKeys.add(depKey(table, key));
1105
+ } else {
1106
+ readTables.add(table);
1107
+ }
1108
+ const raw = await reader.get(key, ctx);
1109
+ const row = raw === undefined ? undefined : migrateRow(table, raw);
1110
+ const rule = ruleFor(table);
1111
+ return rule && row !== undefined && !rule(ctx, row) ? undefined : row;
1112
+ },
1113
+ where: async (table, predicate) => {
1114
+ const reader = readerFor(table);
1115
+ const rule = ruleFor(table);
1116
+ const effective = rule ? (row) => predicate(row) && rule(ctx, row) : predicate;
1117
+ const matched = [...await reader.all(ctx)].map((row) => migrateRow(table, row)).filter(effective);
1118
+ if (reader.key !== undefined) {
1119
+ const key = reader.key;
1120
+ rangeDeps.push({
1121
+ table,
1122
+ predicate: effective,
1123
+ keys: new Set(matched.map(key))
1124
+ });
1125
+ } else {
1126
+ readTables.add(table);
1127
+ }
1128
+ return matched;
1129
+ }
1130
+ };
1131
+ };
1132
+ const writerFor = (table) => {
1133
+ const writer = writers.get(table);
1134
+ if (writer === undefined) {
1135
+ throw new Error(`No writer registered for table "${table}" \u2014 register one with engine.registerWriter, or use actions.change`);
1136
+ }
1137
+ return writer;
1138
+ };
1139
+ const readExisting = async (table, value, ctx) => {
1140
+ const reader = readers.get(table);
1141
+ if (reader?.get === undefined) {
1142
+ return;
1143
+ }
1144
+ const id = reader.key ? reader.key(value) : value.id;
1145
+ return id === undefined ? undefined : reader.get(id, ctx);
1146
+ };
1147
+ const authorizeWrite = async (table, op, value, ctx) => {
1148
+ const rule = writeRuleFor(table, op);
1149
+ if (rule === undefined) {
1150
+ return;
1151
+ }
1152
+ let subject = value;
1153
+ if (op !== "insert") {
1154
+ const existing = await readExisting(table, value, ctx);
1155
+ if (existing !== undefined) {
1156
+ subject = existing;
1157
+ }
1158
+ }
1159
+ if (!rule(ctx, subject)) {
1160
+ throw new UnauthorizedError(`${op} on table "${table}"`);
1161
+ }
1162
+ };
1163
+ const mergeCrdtFields = async (table, op, data, ctx) => {
1164
+ const fields = crdtFields.get(table);
1165
+ if (fields === undefined || data === null || typeof data !== "object") {
1166
+ return data;
1167
+ }
1168
+ const incoming = data;
1169
+ const existing = op === "update" ? await readExisting(table, data, ctx) : undefined;
1170
+ const base = existing !== null && typeof existing === "object" ? existing : undefined;
1171
+ const merged = { ...incoming };
1172
+ for (const [field, adapter] of Object.entries(fields)) {
1173
+ if (incoming[field] === undefined) {
1174
+ continue;
1175
+ }
1176
+ merged[field] = adapter.merge(base?.[field] ?? adapter.empty(), incoming[field]);
1177
+ }
1178
+ return merged;
1179
+ };
1180
+ const makeActions = (tx, ctx, enforce) => {
1181
+ const buffered = [];
1182
+ const actions = {
1183
+ change: (collection, change) => {
1184
+ buffered.push({
1185
+ table: collection,
1186
+ change
1187
+ });
1188
+ return Promise.resolve();
1189
+ },
1190
+ insert: async (table, data) => {
1191
+ validateWrite(table, "insert", data);
1192
+ if (enforce) {
1193
+ await authorizeWrite(table, "insert", data, ctx);
1194
+ }
1195
+ const merged = await mergeCrdtFields(table, "insert", data, ctx);
1196
+ const row = await writerFor(table).insert(merged, ctx, tx);
1197
+ buffered.push({ table, change: { op: "insert", row } });
1198
+ return row;
1199
+ },
1200
+ update: async (table, data) => {
1201
+ validateWrite(table, "update", data);
1202
+ if (enforce) {
1203
+ await authorizeWrite(table, "update", data, ctx);
1204
+ }
1205
+ const merged = await mergeCrdtFields(table, "update", data, ctx);
1206
+ const row = await writerFor(table).update(merged, ctx, tx);
1207
+ buffered.push({ table, change: { op: "update", row } });
1208
+ return row;
1209
+ },
1210
+ delete: async (table, row) => {
1211
+ if (enforce) {
1212
+ await authorizeWrite(table, "delete", row, ctx);
1213
+ }
1214
+ await writerFor(table).delete(row, ctx, tx);
1215
+ buffered.push({ table, change: { op: "delete", row } });
1216
+ },
1217
+ now: () => Date.now()
1218
+ };
1219
+ return { actions, buffered };
1220
+ };
1221
+ const diffRerun = (sub, rows, equals = shallowEqual3) => {
1222
+ const next = new Map;
1223
+ for (const row of rows) {
1224
+ next.set(sub.key(row), row);
1225
+ }
1226
+ const added = [];
1227
+ const removed = [];
1228
+ const changed = [];
1229
+ for (const [rowKey, row] of next) {
1230
+ const previous = sub.current.get(rowKey);
1231
+ if (previous === undefined) {
1232
+ added.push(row);
1233
+ } else if (!equals(previous, row)) {
1234
+ changed.push(row);
1235
+ }
1236
+ }
1237
+ for (const [rowKey, row] of sub.current) {
1238
+ if (!next.has(rowKey)) {
1239
+ removed.push(row);
1240
+ }
1241
+ }
1242
+ sub.current = next;
1243
+ return { added, removed, changed };
1244
+ };
1245
+ const inRange = (dep, change) => dep.table === change.table && (change.key !== undefined && dep.keys.has(change.key) || dep.predicate(change.row));
1246
+ const readSetOverlaps = (readTables, readKeys, rangeDeps, changes) => changes.some((change) => readTables.has(change.table) || change.key !== undefined && readKeys.has(depKey(change.table, change.key)) || rangeDeps.some((dep) => inRange(dep, change)));
1247
+ const isReactiveAffected = (sub, changes) => readSetOverlaps(sub.readTables, sub.readKeys, sub.rangeDeps, changes);
1248
+ const invalidateCacheForChanges = (changes) => {
1249
+ if (cachedReruns.size === 0)
1250
+ return;
1251
+ for (const [key, entry] of cachedReruns) {
1252
+ if (readSetOverlaps(entry.readTables, entry.readKeys, entry.rangeDeps, changes)) {
1253
+ cachedReruns.delete(key);
1254
+ }
1255
+ }
1256
+ };
1257
+ const reactivePairs = async (changes) => {
1258
+ invalidateCacheForChanges(changes);
1259
+ const pairs = [];
1260
+ const sharedRuns = new Map;
1261
+ for (const sub of reactiveSubs) {
1262
+ if (!isReactiveAffected(sub, changes)) {
1263
+ continue;
1264
+ }
1265
+ let runPromise = sharedRuns.get(sub.rerunKey);
1266
+ if (runPromise === undefined) {
1267
+ runPromise = sub.rerun();
1268
+ sharedRuns.set(sub.rerunKey, runPromise);
1269
+ }
1270
+ const { rows, readTables, readKeys, rangeDeps } = await runPromise;
1271
+ sub.readTables = readTables;
1272
+ sub.readKeys = readKeys;
1273
+ sub.rangeDeps = rangeDeps;
1274
+ const diff = diffRerun(sub, rows);
1275
+ if (!isEmptyViewDiff(diff)) {
1276
+ pairs.push([sub, diff]);
1277
+ }
1278
+ }
1279
+ for (const [key, runPromise] of sharedRuns) {
1280
+ runPromise.then(({ rows, readTables, readKeys, rangeDeps }) => {
1281
+ writeCacheEntry({
1282
+ expiresAt: Date.now() + reactiveCacheTtlMs,
1283
+ rangeDeps,
1284
+ readKeys,
1285
+ readTables,
1286
+ rerunKey: key,
1287
+ rows,
1288
+ version
1289
+ });
1290
+ }).catch(() => {});
1291
+ }
1292
+ return pairs;
1293
+ };
1294
+ const ensureSearchIndex = async (definition) => {
1295
+ let entry = searchIndexes.get(definition.name);
1296
+ if (entry === undefined) {
1297
+ entry = { index: definition.index(), definition, hydrated: false };
1298
+ searchIndexes.set(definition.name, entry);
1299
+ }
1300
+ if (!entry.hydrated) {
1301
+ for (const row of await definition.source()) {
1302
+ entry.index.add(row);
1303
+ }
1304
+ entry.hydrated = true;
1305
+ }
1306
+ return entry;
1307
+ };
1308
+ const searchPairs = (changes) => {
1309
+ const touched = new Set;
1310
+ for (const { table, change } of changes) {
1311
+ for (const entry of searchIndexes.values()) {
1312
+ if (!entry.hydrated || entry.definition.table !== table) {
1313
+ continue;
1314
+ }
1315
+ if (change.op === "delete") {
1316
+ entry.index.remove(entry.definition.key(change.row));
1317
+ } else {
1318
+ entry.index.add(change.row);
1319
+ }
1320
+ touched.add(entry.definition.name);
1321
+ }
1322
+ }
1323
+ const pairs = [];
1324
+ for (const sub of searchSubs) {
1325
+ if (!touched.has(sub.collection)) {
1326
+ continue;
1327
+ }
1328
+ const diff = diffRerun(sub, sub.rerun(), equalsIgnoringScore);
1329
+ if (!isEmptyViewDiff(diff)) {
1330
+ pairs.push([sub, diff]);
1331
+ }
1332
+ }
1333
+ return pairs;
1334
+ };
1335
+ const logChange = (changeVersion, entry) => {
1336
+ changeLog.push(entry);
1337
+ if (changeLog.length > changeLogSize) {
1338
+ changeLog.shift();
1339
+ }
1340
+ if (changeLogRetainMs !== null && changeLogRetainMs > 0) {
1341
+ const cutoff = entry.at - changeLogRetainMs;
1342
+ while (changeLog.length > 0 && changeLog[0].at < cutoff) {
1343
+ changeLog.shift();
1344
+ }
1345
+ }
1346
+ for (const subscriber of streamSubscribers) {
1347
+ subscriber(entry);
1348
+ }
1349
+ };
1350
+ const encodeCursor = (versions) => JSON.stringify(versions);
1351
+ const decodeCursor = (cursor) => {
1352
+ try {
1353
+ const parsed = JSON.parse(cursor);
1354
+ if (typeof parsed !== "object" || parsed === null)
1355
+ return null;
1356
+ const out = {};
1357
+ for (const [k, v] of Object.entries(parsed)) {
1358
+ if (typeof v === "number")
1359
+ out[k] = v;
1360
+ }
1361
+ return out;
1362
+ } catch {
1363
+ return null;
1364
+ }
1365
+ };
1366
+ const currentCursor = () => {
1367
+ const versions = { [instanceId]: version };
1368
+ for (let i = changeLog.length - 1;i >= 0; i--) {
1369
+ const entry = changeLog[i];
1370
+ if (versions[entry.origin] === undefined) {
1371
+ versions[entry.origin] = entry.originVersion;
1372
+ }
1373
+ }
1374
+ return encodeCursor(versions);
1375
+ };
1376
+ const applyChange = async (table, change, shouldBroadcast = true) => {
1377
+ version += 1;
1378
+ const changeVersion = version;
1379
+ const at = Date.now();
1380
+ logChange(changeVersion, {
1381
+ version: changeVersion,
1382
+ table,
1383
+ change,
1384
+ at,
1385
+ origin: instanceId,
1386
+ originVersion: changeVersion
1387
+ });
1388
+ emitActivity({
1389
+ type: "change",
1390
+ at,
1391
+ table,
1392
+ op: change.op,
1393
+ version: changeVersion
1394
+ });
1395
+ const emissions = [];
1396
+ for (const subscription of subscriptionsForTable(table)) {
1397
+ const diff = await subscriptionDiff(subscription, table, change);
1398
+ if (!isEmptyViewDiff(diff)) {
1399
+ emissions.push([subscription, diff]);
1400
+ }
1401
+ }
1402
+ emissions.push(...await reactivePairs([
1403
+ { table, key: changedKeyFor(table, change), row: change.row }
1404
+ ]));
1405
+ emissions.push(...searchPairs([{ table, change }]));
1406
+ const cursorForBatch = currentCursor();
1407
+ for (const [subscription, diff] of emissions) {
1408
+ subscription.onDiff(diff, changeVersion, cursorForBatch);
1409
+ }
1410
+ if (shouldBroadcast) {
1411
+ broadcast([{ table, change }], changeVersion);
1412
+ }
1413
+ };
1414
+ const applyChangeBatch = async (changes, shouldBroadcast = true, peerOrigin) => {
1415
+ if (changes.length === 0) {
1416
+ return;
1417
+ }
1418
+ version += 1;
1419
+ const batchVersion = version;
1420
+ const perSubscription = new Map;
1421
+ const reactiveChanges = [];
1422
+ const batchAt = Date.now();
1423
+ const batchOrigin = peerOrigin?.origin ?? instanceId;
1424
+ const batchOriginVersion = peerOrigin?.originVersion ?? batchVersion;
1425
+ for (const { table, change } of changes) {
1426
+ logChange(batchVersion, {
1427
+ version: batchVersion,
1428
+ table,
1429
+ change,
1430
+ at: batchAt,
1431
+ origin: batchOrigin,
1432
+ originVersion: batchOriginVersion
1433
+ });
1434
+ emitActivity({
1435
+ type: "change",
1436
+ at: batchAt,
1437
+ table,
1438
+ op: change.op,
1439
+ version: batchVersion
1440
+ });
1441
+ reactiveChanges.push({
1442
+ table,
1443
+ key: changedKeyFor(table, change),
1444
+ row: change.row
1445
+ });
1446
+ for (const subscription of subscriptionsForTable(table)) {
1447
+ const diff = await subscriptionDiff(subscription, table, change);
1448
+ const list = perSubscription.get(subscription);
1449
+ if (list === undefined) {
1450
+ perSubscription.set(subscription, [diff]);
1451
+ } else {
1452
+ list.push(diff);
1453
+ }
1454
+ }
1455
+ }
1456
+ const emissions = [];
1457
+ for (const [subscription, diffs] of perSubscription) {
1458
+ const merged = diffs.length === 1 ? diffs[0] : mergeViewDiffs(diffs, subscription.key);
1459
+ if (!isEmptyViewDiff(merged)) {
1460
+ emissions.push([subscription, merged]);
1461
+ }
1462
+ }
1463
+ emissions.push(...await reactivePairs(reactiveChanges));
1464
+ emissions.push(...searchPairs(changes));
1465
+ const cursorForBatch = currentCursor();
1466
+ for (const [subscription, diff] of emissions) {
1467
+ subscription.onDiff(diff, batchVersion, cursorForBatch);
1468
+ }
1469
+ if (shouldBroadcast) {
1470
+ broadcast(changes, batchVersion);
1471
+ }
1472
+ };
1473
+ const normalizeSince = (since) => {
1474
+ if (typeof since === "number") {
1475
+ return { [instanceId]: since };
1476
+ }
1477
+ return decodeCursor(since);
1478
+ };
1479
+ const canResume = (since, incremental) => {
1480
+ if (!incremental) {
1481
+ return false;
1482
+ }
1483
+ const sinceVec = normalizeSince(since);
1484
+ if (sinceVec === null) {
1485
+ return false;
1486
+ }
1487
+ const oldestPerOrigin = new Map;
1488
+ for (const entry of changeLog) {
1489
+ const current = oldestPerOrigin.get(entry.origin);
1490
+ if (current === undefined || entry.originVersion < current) {
1491
+ oldestPerOrigin.set(entry.origin, entry.originVersion);
1492
+ }
1493
+ }
1494
+ const oldestLogVersion = changeLog[0]?.version;
1495
+ for (const [origin, lastSeen] of Object.entries(sinceVec)) {
1496
+ if (origin === instanceId) {
1497
+ if (lastSeen >= version)
1498
+ continue;
1499
+ const oldestLocal = oldestPerOrigin.get(instanceId);
1500
+ if (oldestLocal !== undefined) {
1501
+ if (oldestLocal > lastSeen + 1)
1502
+ return false;
1503
+ continue;
1504
+ }
1505
+ if (oldestLogVersion !== undefined && oldestLogVersion > lastSeen + 1) {
1506
+ return false;
1507
+ }
1508
+ } else {
1509
+ const oldestPeer = oldestPerOrigin.get(origin);
1510
+ if (oldestPeer === undefined) {
1511
+ if (lastSeen > 0)
1512
+ return false;
1513
+ } else if (oldestPeer > lastSeen + 1) {
1514
+ return false;
1515
+ }
1516
+ }
1517
+ }
1518
+ return true;
1519
+ };
1520
+ const buildCatchup = (since, tables, key, match) => {
1521
+ const sinceVec = normalizeSince(since) ?? {};
1522
+ const latest = new Map;
1523
+ for (const entry of changeLog) {
1524
+ if (!tables.includes(entry.table))
1525
+ continue;
1526
+ const lastSeen = sinceVec[entry.origin];
1527
+ if (lastSeen !== undefined && entry.originVersion <= lastSeen)
1528
+ continue;
1529
+ const row = entry.change.row;
1530
+ const present = entry.change.op !== "delete" && match(row) ? "upsert" : "remove";
1531
+ latest.set(key(row), { op: present, row });
1532
+ }
1533
+ const changed = [];
1534
+ const removed = [];
1535
+ for (const { op, row } of latest.values()) {
1536
+ (op === "upsert" ? changed : removed).push(row);
1537
+ }
1538
+ return { added: [], removed, changed };
1539
+ };
1540
+ const subscribeJoin = async (collection, definition, params, ctx, onDiff, set) => {
1541
+ if (definition.authorize !== undefined) {
1542
+ const allowed = await definition.authorize(params, ctx);
1543
+ if (!allowed) {
1544
+ throw new UnauthorizedError(`subscribe to collection "${collection}"`);
1545
+ }
1546
+ }
1547
+ const { left, right } = definition;
1548
+ const op = createEquiJoin({
1549
+ leftKey: left.key,
1550
+ rightKey: right.key,
1551
+ leftOn: left.on,
1552
+ rightOn: right.on,
1553
+ select: definition.select
1554
+ });
1555
+ op.hydrate([...await left.hydrate(params, ctx)], [...await right.hydrate(params, ctx)]);
1556
+ const atVersion = version;
1557
+ const subscription = {
1558
+ kind: "join",
1559
+ collection,
1560
+ join: {
1561
+ op,
1562
+ leftTable: left.table,
1563
+ rightTable: right.table,
1564
+ leftMatch: left.match ? (row) => left.match(row, params, ctx) : undefined,
1565
+ rightMatch: right.match ? (row) => right.match(row, params, ctx) : undefined
1566
+ },
1567
+ key: definition.key,
1568
+ onDiff
1569
+ };
1570
+ set.add(subscription);
1571
+ return {
1572
+ initial: op.rows(),
1573
+ cursor: currentCursor(),
1574
+ version: atVersion,
1575
+ unsubscribe: () => {
1576
+ set.delete(subscription);
1577
+ }
1578
+ };
1579
+ };
1580
+ const subscribeGraph = async (collection, definition, params, ctx, onDiff, set) => {
1581
+ if (definition.authorize !== undefined) {
1582
+ const allowed = await definition.authorize(params, ctx);
1583
+ if (!allowed) {
1584
+ throw new UnauthorizedError(`subscribe to collection "${collection}"`);
1585
+ }
1586
+ }
1587
+ const instance = definition.query.instantiate(params, ctx);
1588
+ const initial = await instance.hydrate();
1589
+ const atVersion = version;
1590
+ const subscription = {
1591
+ kind: "graph",
1592
+ collection,
1593
+ instance,
1594
+ key: definition.key,
1595
+ onDiff
1596
+ };
1597
+ set.add(subscription);
1598
+ return {
1599
+ initial,
1600
+ cursor: currentCursor(),
1601
+ version: atVersion,
1602
+ unsubscribe: () => {
1603
+ set.delete(subscription);
1604
+ }
1605
+ };
1606
+ };
1607
+ const subscribeReactive = async (collection, definition, params, ctx, onDiff, set) => {
1608
+ if (definition.authorize !== undefined) {
1609
+ const allowed = await definition.authorize(params, ctx);
1610
+ if (!allowed) {
1611
+ throw new UnauthorizedError(`subscribe to collection "${collection}"`);
1612
+ }
1613
+ }
1614
+ const rerun = async () => {
1615
+ const readTables = new Set;
1616
+ const readKeys = new Set;
1617
+ const rangeDeps = [];
1618
+ const db = makeReadHandle(ctx, readTables, readKeys, rangeDeps);
1619
+ const rows = [...await definition.run({ ctx, db, params })];
1620
+ return { rangeDeps, readKeys, readTables, rows };
1621
+ };
1622
+ const rerunKey = stableSubKey(collection, params, ctx);
1623
+ const cached = readCacheEntry(rerunKey);
1624
+ const first = cached !== undefined ? {
1625
+ rangeDeps: cached.rangeDeps,
1626
+ readKeys: cached.readKeys,
1627
+ readTables: cached.readTables,
1628
+ rows: cached.rows
1629
+ } : await rerun();
1630
+ if (cached === undefined) {
1631
+ writeCacheEntry({
1632
+ expiresAt: Date.now() + reactiveCacheTtlMs,
1633
+ rangeDeps: first.rangeDeps,
1634
+ readKeys: first.readKeys,
1635
+ readTables: first.readTables,
1636
+ rerunKey,
1637
+ rows: first.rows,
1638
+ version
1639
+ });
1640
+ }
1641
+ const current = new Map;
1642
+ for (const row of first.rows) {
1643
+ current.set(definition.key(row), row);
1644
+ }
1645
+ const atVersion = version;
1646
+ const subscription = {
1647
+ kind: "reactive",
1648
+ collection,
1649
+ key: definition.key,
1650
+ rerun,
1651
+ rerunKey,
1652
+ current,
1653
+ readTables: first.readTables,
1654
+ readKeys: first.readKeys,
1655
+ rangeDeps: first.rangeDeps,
1656
+ onDiff
1657
+ };
1658
+ set.add(subscription);
1659
+ reactiveSubs.add(subscription);
1660
+ return {
1661
+ initial: first.rows,
1662
+ cursor: currentCursor(),
1663
+ version: atVersion,
1664
+ unsubscribe: () => {
1665
+ set.delete(subscription);
1666
+ reactiveSubs.delete(subscription);
1667
+ }
1668
+ };
1669
+ };
1670
+ const subscribeSearch = async (collection, definition, params, ctx, onDiff, set) => {
1671
+ const query = params;
1672
+ if (definition.authorize !== undefined) {
1673
+ const allowed = await definition.authorize(query, ctx);
1674
+ if (!allowed) {
1675
+ throw new UnauthorizedError(`subscribe to collection "${collection}"`);
1676
+ }
1677
+ }
1678
+ const entry = await ensureSearchIndex(definition);
1679
+ const limit = definition.limit ?? 20;
1680
+ const readRule = readRuleFor(definition.table);
1681
+ const rerun = () => {
1682
+ const candidates = entry.index.search(query, readRule ? limit * 5 : limit);
1683
+ const visible = readRule ? candidates.filter((hit) => readRule(ctx, hit.row)) : candidates;
1684
+ return visible.slice(0, limit).map((hit) => ({
1685
+ ...hit.row,
1686
+ [SEARCH_SCORE_FIELD]: hit.score
1687
+ }));
1688
+ };
1689
+ const initial = rerun();
1690
+ const current = new Map;
1691
+ for (const row of initial) {
1692
+ current.set(definition.key(row), row);
1693
+ }
1694
+ const atVersion = version;
1695
+ const subscription = {
1696
+ kind: "search",
1697
+ collection,
1698
+ key: definition.key,
1699
+ rerun,
1700
+ current,
1701
+ onDiff
1702
+ };
1703
+ set.add(subscription);
1704
+ searchSubs.add(subscription);
1705
+ return {
1706
+ initial,
1707
+ cursor: currentCursor(),
1708
+ version: atVersion,
1709
+ unsubscribe: () => {
1710
+ set.delete(subscription);
1711
+ searchSubs.delete(subscription);
1712
+ }
1713
+ };
1714
+ };
1715
+ const engine = {
1716
+ register: (collection) => {
1717
+ registry.set(collection.name, collection);
1718
+ for (const table of collection.tables ?? [collection.name]) {
1719
+ addTableIndex(table, collection.name);
1720
+ }
1721
+ },
1722
+ registerJoin: (collection) => {
1723
+ registry.set(collection.name, collection);
1724
+ addTableIndex(collection.left.table, collection.name);
1725
+ addTableIndex(collection.right.table, collection.name);
1726
+ },
1727
+ registerGraph: (collection) => {
1728
+ registry.set(collection.name, collection);
1729
+ for (const table of collection.query.tables()) {
1730
+ addTableIndex(table, collection.name);
1731
+ }
1732
+ },
1733
+ registerSearch: (collection) => {
1734
+ registry.set(collection.name, collection);
1735
+ },
1736
+ subscribe: async ({
1737
+ collection,
1738
+ params,
1739
+ ctx,
1740
+ onDiff,
1741
+ since,
1742
+ signal
1743
+ }) => {
1744
+ const subscribeSpan = tracer.startSpan("sync.subscribe", {
1745
+ attributes: {
1746
+ [ABS_ATTRS.engineId]: instanceId,
1747
+ [ABS_ATTRS.collection]: collection
1748
+ }
1749
+ });
1750
+ try {
1751
+ checkAborted(signal);
1752
+ const registered = registry.get(collection);
1753
+ if (registered === undefined) {
1754
+ throw new Error(`Unknown collection "${collection}"`);
1755
+ }
1756
+ const tenantSlot = acquireSubscriptionSlot(ctx, { collection });
1757
+ let slotHandedOff = false;
1758
+ try {
1759
+ const typedOnDiff = onDiff;
1760
+ const subscribeSet = subsFor(collection);
1761
+ const wrapReturn = (sub) => {
1762
+ checkAborted(signal);
1763
+ const innerUnsubscribe = sub.unsubscribe;
1764
+ let released = false;
1765
+ const wrappedUnsubscribe = () => {
1766
+ if (released)
1767
+ return;
1768
+ released = true;
1769
+ releaseSubscriptionSlot(tenantSlot);
1770
+ innerUnsubscribe();
1771
+ };
1772
+ const wrapped = {
1773
+ ...sub,
1774
+ unsubscribe: wrappedUnsubscribe
1775
+ };
1776
+ linkAbortToUnsubscribe(signal, wrappedUnsubscribe);
1777
+ slotHandedOff = true;
1778
+ return wrapped;
1779
+ };
1780
+ const registeredKind = registered.kind;
1781
+ if (registeredKind === "join") {
1782
+ const joined = await subscribeJoin(collection, registered, params, ctx, typedOnDiff, subscribeSet);
1783
+ return wrapReturn(joined);
1784
+ }
1785
+ if (registeredKind === "graph") {
1786
+ const graphed = await subscribeGraph(collection, registered, params, ctx, typedOnDiff, subscribeSet);
1787
+ return wrapReturn(graphed);
1788
+ }
1789
+ if (registeredKind === "reactive") {
1790
+ const reactived = await subscribeReactive(collection, registered, params, ctx, typedOnDiff, subscribeSet);
1791
+ return wrapReturn(reactived);
1792
+ }
1793
+ if (registeredKind === "search") {
1794
+ const searched = await subscribeSearch(collection, registered, params, ctx, typedOnDiff, subscribeSet);
1795
+ return wrapReturn(searched);
1796
+ }
1797
+ const definition = registered;
1798
+ if (definition.authorize !== undefined) {
1799
+ const allowed = await definition.authorize(params, ctx);
1800
+ if (!allowed) {
1801
+ throw new UnauthorizedError(`subscribe to collection "${collection}"`);
1802
+ }
1803
+ }
1804
+ const key = definition.key ?? defaultKey;
1805
+ const match = definition.match;
1806
+ const tables = definition.tables ?? [collection];
1807
+ const scopedTable = tables.length === 1 ? tables[0] : undefined;
1808
+ const readRule = scopedTable !== undefined ? readRuleFor(scopedTable) : undefined;
1809
+ const rehydrate = async () => {
1810
+ const raw = [
1811
+ ...await definition.hydrate(params, ctx)
1812
+ ];
1813
+ const rows = scopedTable !== undefined ? raw.map((row) => migrateRow(scopedTable, row)) : raw;
1814
+ return readRule ? rows.filter((row) => readRule(ctx, row)) : rows;
1815
+ };
1816
+ const incremental = match !== undefined && tables.length === 1;
1817
+ const boundAffects = !incremental && definition.affects ? (change) => definition.affects(change, params, ctx) : undefined;
1818
+ const boundMatch = incremental ? (row) => match(row, params, ctx) && (readRule ? readRule(ctx, row) : true) : () => true;
1819
+ const view = createMaterializedView({
1820
+ key,
1821
+ match: boundMatch
1822
+ });
1823
+ const resuming = since !== undefined && canResume(since, incremental);
1824
+ view.hydrate([...await rehydrate()]);
1825
+ const atVersion = version;
1826
+ const subscription = {
1827
+ kind: "view",
1828
+ collection,
1829
+ view,
1830
+ incremental,
1831
+ rehydrate,
1832
+ ...boundAffects ? { affects: boundAffects } : {},
1833
+ key,
1834
+ onDiff: typedOnDiff
1835
+ };
1836
+ subscribeSet.add(subscription);
1837
+ const unsubscribe = () => {
1838
+ subscribeSet.delete(subscription);
1839
+ };
1840
+ if (resuming) {
1841
+ return wrapReturn({
1842
+ initial: [],
1843
+ catchup: buildCatchup(since, tables, key, boundMatch),
1844
+ cursor: currentCursor(),
1845
+ version: atVersion,
1846
+ unsubscribe
1847
+ });
1848
+ }
1849
+ return wrapReturn({
1850
+ initial: view.rows(),
1851
+ cursor: currentCursor(),
1852
+ version: atVersion,
1853
+ unsubscribe
1854
+ });
1855
+ } catch (error) {
1856
+ if (!slotHandedOff)
1857
+ releaseSubscriptionSlot(tenantSlot);
1858
+ throw error;
1859
+ }
1860
+ } catch (spanError) {
1861
+ subscribeSpan.recordException(spanError);
1862
+ subscribeSpan.setStatus({
1863
+ code: 2,
1864
+ message: spanError instanceof Error ? spanError.message : String(spanError)
1865
+ });
1866
+ throw spanError;
1867
+ } finally {
1868
+ subscribeSpan.end();
1869
+ }
1870
+ },
1871
+ hydrate: async (collection, params, ctx, options2) => {
1872
+ const signal = options2?.signal;
1873
+ checkAborted(signal);
1874
+ const definition = registry.get(collection);
1875
+ if (definition === undefined) {
1876
+ throw new Error(`Unknown collection "${collection}"`);
1877
+ }
1878
+ if (definition.authorize !== undefined) {
1879
+ const allowed = await definition.authorize(params, ctx);
1880
+ checkAborted(signal);
1881
+ if (!allowed) {
1882
+ throw new UnauthorizedError(`hydrate collection "${collection}"`);
1883
+ }
1884
+ }
1885
+ const raw = [...await definition.hydrate(params, ctx)];
1886
+ checkAborted(signal);
1887
+ const tables = definition.tables ?? [collection];
1888
+ const scopedTable = tables.length === 1 ? tables[0] : undefined;
1889
+ const rows = scopedTable !== undefined ? raw.map((row) => migrateRow(scopedTable, row)) : raw;
1890
+ const readRule = scopedTable !== undefined ? readRuleFor(scopedTable) : undefined;
1891
+ return readRule ? rows.filter((row) => readRule(ctx, row)) : rows;
1892
+ },
1893
+ applyChange: (table, change) => applyChange(table, change),
1894
+ connectSource: async (source) => {
1895
+ await source.start((table, change) => {
1896
+ sourceChangesReceived += 1;
1897
+ sourceLastChangeAt = Date.now();
1898
+ return applyChange(table, change);
1899
+ });
1900
+ connectedSources += 1;
1901
+ let stopped = false;
1902
+ return async () => {
1903
+ if (!stopped) {
1904
+ stopped = true;
1905
+ connectedSources -= 1;
1906
+ }
1907
+ await source.stop();
1908
+ };
1909
+ },
1910
+ connectCluster: async (bus) => {
1911
+ const unsubscribe = await bus.subscribe((message) => {
1912
+ if (message.origin === instanceId) {
1913
+ return;
1914
+ }
1915
+ applyChangeBatch(message.changes, false, {
1916
+ origin: message.origin,
1917
+ originVersion: message.originVersion ?? 0
1918
+ });
1919
+ });
1920
+ clusterBus = bus;
1921
+ return async () => {
1922
+ clusterBus = undefined;
1923
+ await unsubscribe();
1924
+ };
1925
+ },
1926
+ subscriptionCount: (collection) => {
1927
+ if (collection !== undefined) {
1928
+ return active.get(collection)?.size ?? 0;
1929
+ }
1930
+ let total = 0;
1931
+ for (const set of active.values()) {
1932
+ total += set.size;
1933
+ }
1934
+ return total;
1935
+ },
1936
+ registerMutation: (mutation) => {
1937
+ if (mutation.handler === undefined && mutation.sandboxedHandler === undefined) {
1938
+ throw new Error(`Mutation "${mutation.name}" must define either \`handler\` or \`sandboxedHandler\``);
1939
+ }
1940
+ if (mutation.handler !== undefined && mutation.sandboxedHandler !== undefined) {
1941
+ throw new Error(`Mutation "${mutation.name}" defines both \`handler\` and \`sandboxedHandler\` \u2014 pick one`);
1942
+ }
1943
+ mutations.set(mutation.name, mutation);
1944
+ if (mutation.sandboxedHandler !== undefined) {
1945
+ sandboxRunners.set(mutation.name, makeSandboxedHandler(mutation.sandboxedHandler, mutation.sandbox, {
1946
+ bridgeFetch: options.bridgeFetch,
1947
+ metricsHook: options.handlerMetrics === undefined ? undefined : {
1948
+ mutationName: mutation.name,
1949
+ onMetrics: options.handlerMetrics
1950
+ }
1951
+ }));
1952
+ }
1953
+ },
1954
+ registerWriter: (table, writer) => {
1955
+ writers.set(table, writer);
1956
+ },
1957
+ registerReactive: (query) => {
1958
+ registry.set(query.name, query);
1959
+ },
1960
+ registerReader: (table, reader) => {
1961
+ readers.set(table, reader);
1962
+ },
1963
+ registerPermissions: (table, rules) => {
1964
+ permissions.set(table, rules);
1965
+ },
1966
+ registerSchema: (table, schema) => {
1967
+ schemas.set(table, schema);
1968
+ },
1969
+ registerCrdt: (table, fields) => {
1970
+ crdtFields.set(table, fields);
1971
+ const name = `${table}:merge`;
1972
+ mutations.set(name, {
1973
+ handler: async (args, ctx, actions) => {
1974
+ const existing = await readExisting(table, args, ctx);
1975
+ return existing === undefined ? actions.insert(table, args) : actions.update(table, args);
1976
+ },
1977
+ name
1978
+ });
1979
+ },
1980
+ migrate: (table, row) => migrateRow(table, row),
1981
+ runMutation: async (name, args, ctx) => {
1982
+ const span = tracer.startSpan("sync.runMutation", {
1983
+ attributes: {
1984
+ [ABS_ATTRS.engineId]: instanceId,
1985
+ [ABS_ATTRS.mutation]: name
1986
+ }
1987
+ });
1988
+ try {
1989
+ if (activeFences.size > 0) {
1990
+ const oldest = activeFences.values().next().value;
1991
+ throw new EngineFencedError(oldest.reason);
1992
+ }
1993
+ const mutation = mutations.get(name);
1994
+ if (mutation === undefined) {
1995
+ throw new Error(`Unknown mutation "${name}"`);
1996
+ }
1997
+ if (mutation.authorize !== undefined) {
1998
+ const allowed = await mutation.authorize(args, ctx);
1999
+ if (!allowed) {
2000
+ throw new UnauthorizedError(`run mutation "${name}"`);
2001
+ }
2002
+ }
2003
+ await acquireMutationSlot();
2004
+ const sandboxRunner = sandboxRunners.get(name);
2005
+ const invokeHandler = sandboxRunner !== undefined ? sandboxRunner : (a, c, actions) => Promise.resolve(mutation.handler(a, c, actions));
2006
+ const runHandler = async (tx) => {
2007
+ const { actions, buffered } = makeActions(tx, ctx, true);
2008
+ const result = await invokeHandler(args, ctx, actions);
2009
+ return { buffered, result };
2010
+ };
2011
+ const retry = mutation.retry;
2012
+ const maxAttempts = retry === undefined ? 1 : retry.maxAttempts ?? 5;
2013
+ const isRetryable = retry?.isRetryable ?? isSerializationFailure;
2014
+ const computeDelay = retry?.backoff ?? exponentialBackoff();
2015
+ const maxElapsedMs = retry?.maxElapsedMs ?? 30000;
2016
+ const startedAt = Date.now();
2017
+ let lastError;
2018
+ let attemptsMade = 0;
2019
+ try {
2020
+ for (let attempt = 1;attempt <= maxAttempts; attempt++) {
2021
+ attemptsMade = attempt;
2022
+ try {
2023
+ const { buffered, result } = runInTransaction !== undefined ? await runInTransaction((tx) => runHandler(tx)) : await runHandler(undefined);
2024
+ await applyChangeBatch(buffered);
2025
+ mutationsCompleted += 1;
2026
+ emitActivity({
2027
+ type: "mutation",
2028
+ at: Date.now(),
2029
+ name,
2030
+ status: "ok"
2031
+ });
2032
+ return result;
2033
+ } catch (error) {
2034
+ lastError = error;
2035
+ const elapsedMs = Date.now() - startedAt;
2036
+ const canRetry = attempt < maxAttempts && isRetryable(error) && elapsedMs < maxElapsedMs;
2037
+ if (!canRetry)
2038
+ break;
2039
+ mutationsRetried += 1;
2040
+ const rawDelay = computeDelay(attempt);
2041
+ const remaining = maxElapsedMs - elapsedMs;
2042
+ if (remaining <= 0)
2043
+ break;
2044
+ const delayMs = Math.max(0, Math.min(rawDelay, remaining));
2045
+ emitActivity({
2046
+ type: "mutationRetry",
2047
+ at: Date.now(),
2048
+ name,
2049
+ attempt,
2050
+ delayMs,
2051
+ errorName: error instanceof Error ? error.name : "Error",
2052
+ errorMessage: error instanceof Error ? error.message : String(error)
2053
+ });
2054
+ if (delayMs > 0) {
2055
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
2056
+ }
2057
+ }
2058
+ }
2059
+ mutationsFailed += 1;
2060
+ emitActivity({
2061
+ type: "mutation",
2062
+ at: Date.now(),
2063
+ name,
2064
+ status: "error"
2065
+ });
2066
+ if (attemptsMade > 1) {
2067
+ throw new RetriesExhaustedError(attemptsMade, Date.now() - startedAt, lastError);
2068
+ }
2069
+ throw lastError;
2070
+ } finally {
2071
+ releaseMutationSlot();
2072
+ }
2073
+ } catch (spanError) {
2074
+ span.recordException(spanError);
2075
+ span.setStatus({
2076
+ code: 2,
2077
+ message: spanError instanceof Error ? spanError.message : String(spanError)
2078
+ });
2079
+ throw spanError;
2080
+ } finally {
2081
+ span.end();
2082
+ }
2083
+ },
2084
+ runMutations: async (specs, ctx) => {
2085
+ if (specs.length === 0)
2086
+ return [];
2087
+ const resolved = specs.map((spec) => {
2088
+ const mutation = mutations.get(spec.name);
2089
+ if (mutation === undefined) {
2090
+ throw new Error(`Unknown mutation "${spec.name}"`);
2091
+ }
2092
+ return { args: spec.args, mutation, name: spec.name };
2093
+ });
2094
+ await acquireMutationSlot();
2095
+ const runBatch = async (tx) => {
2096
+ const results = [];
2097
+ const accumulated = [];
2098
+ for (const { args, mutation, name } of resolved) {
2099
+ if (mutation.authorize !== undefined) {
2100
+ const allowed = await mutation.authorize(args, ctx);
2101
+ if (!allowed) {
2102
+ throw new UnauthorizedError(`run mutation "${name}"`);
2103
+ }
2104
+ }
2105
+ const sandboxRunner = sandboxRunners.get(name);
2106
+ const invokeHandler = sandboxRunner !== undefined ? sandboxRunner : (a, c, actions2) => Promise.resolve(mutation.handler(a, c, actions2));
2107
+ const { actions, buffered } = makeActions(tx, ctx, true);
2108
+ const result = await invokeHandler(args, ctx, actions);
2109
+ results.push(result);
2110
+ accumulated.push(...buffered);
2111
+ }
2112
+ return { accumulated, results };
2113
+ };
2114
+ try {
2115
+ const { accumulated, results } = runInTransaction !== undefined ? await runInTransaction((tx) => runBatch(tx)) : await runBatch(undefined);
2116
+ await applyChangeBatch(accumulated);
2117
+ emitActivity({
2118
+ type: "mutationBatch",
2119
+ at: Date.now(),
2120
+ names: resolved.map((entry) => entry.name),
2121
+ status: "ok"
2122
+ });
2123
+ return results;
2124
+ } catch (error) {
2125
+ emitActivity({
2126
+ type: "mutationBatch",
2127
+ at: Date.now(),
2128
+ names: resolved.map((entry) => entry.name),
2129
+ status: "error"
2130
+ });
2131
+ throw error;
2132
+ } finally {
2133
+ releaseMutationSlot();
2134
+ }
2135
+ },
2136
+ registerSchedule: (schedule) => {
2137
+ schedules.set(schedule.name, schedule);
2138
+ },
2139
+ listSchedules: () => [...schedules.values()],
2140
+ runSchedule: async (name) => {
2141
+ const schedule = schedules.get(name);
2142
+ if (schedule === undefined) {
2143
+ throw new Error(`Unknown schedule "${name}"`);
2144
+ }
2145
+ const runHandler = async (tx) => {
2146
+ const { actions, buffered } = makeActions(tx, {}, false);
2147
+ const db = makeReadHandle({}, new Set, new Set, [], false);
2148
+ await schedule.run({ actions, db });
2149
+ return buffered;
2150
+ };
2151
+ const retry = schedule.retry;
2152
+ const maxAttempts = retry === undefined ? 1 : retry.maxAttempts ?? 5;
2153
+ const isRetryable = retry?.isRetryable ?? isSerializationFailure;
2154
+ const computeDelay = retry?.backoff ?? exponentialBackoff();
2155
+ const maxElapsedMs = retry?.maxElapsedMs ?? 30000;
2156
+ const startedAt = Date.now();
2157
+ let lastError;
2158
+ let attemptsMade = 0;
2159
+ for (let attempt = 1;attempt <= maxAttempts; attempt++) {
2160
+ attemptsMade = attempt;
2161
+ try {
2162
+ const buffered = runInTransaction !== undefined ? await runInTransaction((tx) => runHandler(tx)) : await runHandler(undefined);
2163
+ await applyChangeBatch(buffered);
2164
+ emitActivity({
2165
+ type: "schedule",
2166
+ at: Date.now(),
2167
+ name,
2168
+ status: "ok"
2169
+ });
2170
+ return;
2171
+ } catch (error) {
2172
+ lastError = error;
2173
+ const elapsedMs = Date.now() - startedAt;
2174
+ const canRetry = attempt < maxAttempts && isRetryable(error) && elapsedMs < maxElapsedMs;
2175
+ if (!canRetry)
2176
+ break;
2177
+ const rawDelay = computeDelay(attempt);
2178
+ const remaining = maxElapsedMs - elapsedMs;
2179
+ if (remaining <= 0)
2180
+ break;
2181
+ const delayMs = Math.max(0, Math.min(rawDelay, remaining));
2182
+ emitActivity({
2183
+ type: "scheduleRetry",
2184
+ at: Date.now(),
2185
+ name,
2186
+ attempt,
2187
+ delayMs,
2188
+ errorName: error instanceof Error ? error.name : "Error",
2189
+ errorMessage: error instanceof Error ? error.message : String(error)
2190
+ });
2191
+ if (delayMs > 0) {
2192
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
2193
+ }
2194
+ }
2195
+ }
2196
+ emitActivity({
2197
+ type: "schedule",
2198
+ at: Date.now(),
2199
+ name,
2200
+ status: "error"
2201
+ });
2202
+ if (attemptsMade > 1) {
2203
+ throw new RetriesExhaustedError(attemptsMade, Date.now() - startedAt, lastError);
2204
+ }
2205
+ throw lastError;
2206
+ },
2207
+ registerPack: (pack) => {
2208
+ for (const table of pack.ownsTables) {
2209
+ const existing = packTableOwners.get(table);
2210
+ if (existing !== undefined) {
2211
+ throw new PackTableConflictError(table, existing, pack.name);
2212
+ }
2213
+ }
2214
+ if (pack.requireDependencies === true) {
2215
+ for (const table of pack.readsTables ?? []) {
2216
+ if (!readers.has(table)) {
2217
+ throw new PackMissingDependencyError(pack.name, table);
2218
+ }
2219
+ }
2220
+ }
2221
+ if (pack.schemas !== undefined) {
2222
+ for (const [table, schema] of Object.entries(pack.schemas)) {
2223
+ engine.registerSchema(table, schema);
2224
+ }
2225
+ }
2226
+ if (pack.permissions !== undefined) {
2227
+ for (const [table, rules] of Object.entries(pack.permissions)) {
2228
+ engine.registerPermissions(table, rules);
2229
+ }
2230
+ }
2231
+ if (pack.readers !== undefined) {
2232
+ for (const [table, reader] of Object.entries(pack.readers)) {
2233
+ engine.registerReader(table, reader);
2234
+ }
2235
+ }
2236
+ if (pack.writers !== undefined) {
2237
+ for (const [table, writer] of Object.entries(pack.writers)) {
2238
+ engine.registerWriter(table, writer);
2239
+ }
2240
+ }
2241
+ if (pack.crdt !== undefined) {
2242
+ for (const [table, fields] of Object.entries(pack.crdt)) {
2243
+ engine.registerCrdt(table, fields);
2244
+ }
2245
+ }
2246
+ for (const collection of pack.collections ?? []) {
2247
+ engine.register(collection);
2248
+ }
2249
+ for (const collection of pack.joinCollections ?? []) {
2250
+ engine.registerJoin(collection);
2251
+ }
2252
+ for (const collection of pack.graphCollections ?? []) {
2253
+ engine.registerGraph(collection);
2254
+ }
2255
+ for (const collection of pack.searchCollections ?? []) {
2256
+ engine.registerSearch(collection);
2257
+ }
2258
+ for (const query of pack.reactiveQueries ?? []) {
2259
+ engine.registerReactive(query);
2260
+ }
2261
+ for (const mutation of pack.mutations ?? []) {
2262
+ engine.registerMutation(mutation);
2263
+ }
2264
+ for (const schedule of pack.schedules ?? []) {
2265
+ engine.registerSchedule(schedule);
2266
+ }
2267
+ for (const table of pack.ownsTables) {
2268
+ packTableOwners.set(table, pack.name);
2269
+ }
2270
+ registeredPacks.push({
2271
+ name: pack.name,
2272
+ version: pack.version,
2273
+ ownsTables: [...pack.ownsTables],
2274
+ readsTables: [...pack.readsTables ?? []]
2275
+ });
2276
+ },
2277
+ inspect: () => {
2278
+ const collections = [...registry.entries()].map(([name, def]) => {
2279
+ const kind = def.kind ?? "view";
2280
+ let tables = [];
2281
+ if (kind === "join") {
2282
+ const join = def;
2283
+ tables = [join.left.table, join.right.table];
2284
+ } else if (kind === "graph") {
2285
+ tables = def.query.tables();
2286
+ } else if (kind === "search") {
2287
+ tables = [
2288
+ def.table
2289
+ ];
2290
+ } else if (kind === "view") {
2291
+ tables = def.tables ?? [name];
2292
+ }
2293
+ return {
2294
+ name,
2295
+ kind,
2296
+ tables,
2297
+ subscriptions: active.get(name)?.size ?? 0
2298
+ };
2299
+ });
2300
+ const DEVTOOLS_RECENT = 50;
2301
+ return {
2302
+ version,
2303
+ collections,
2304
+ mutations: [...mutations.keys()],
2305
+ schedules: [...schedules.values()].map((schedule) => ({
2306
+ name: schedule.name,
2307
+ pattern: schedule.pattern
2308
+ })),
2309
+ readers: [...readers.keys()],
2310
+ writers: [...writers.keys()],
2311
+ recentChanges: changeLog.slice(-DEVTOOLS_RECENT).map((entry) => ({
2312
+ version: entry.version,
2313
+ table: entry.table,
2314
+ op: entry.change.op
2315
+ })),
2316
+ packs: registeredPacks.map((pack) => ({
2317
+ name: pack.name,
2318
+ version: pack.version,
2319
+ ownsTables: [...pack.ownsTables],
2320
+ readsTables: [...pack.readsTables]
2321
+ }))
2322
+ };
2323
+ },
2324
+ exportChangeLog: () => ({
2325
+ entries: changeLog.slice(),
2326
+ exportedAt: Date.now(),
2327
+ instanceId,
2328
+ version
2329
+ }),
2330
+ importChangeLog,
2331
+ replayTo: async ({ at, tables }) => {
2332
+ const filterTables = tables !== undefined ? new Set(tables) : undefined;
2333
+ const state = new Map;
2334
+ let asOfVersion = 0;
2335
+ let asOfAt = 0;
2336
+ const oldest = changeLog[0];
2337
+ const truncated = oldest !== undefined && oldest.version > 1 && oldest.at > at;
2338
+ for (const entry of changeLog) {
2339
+ if (entry.at > at)
2340
+ break;
2341
+ if (filterTables !== undefined && !filterTables.has(entry.table)) {
2342
+ continue;
2343
+ }
2344
+ let tableState = state.get(entry.table);
2345
+ if (tableState === undefined) {
2346
+ tableState = new Map;
2347
+ state.set(entry.table, tableState);
2348
+ }
2349
+ const reader = readers.get(entry.table);
2350
+ const key = reader?.key?.(entry.change.row) ?? entry.change.row?.id;
2351
+ if (key === undefined) {
2352
+ continue;
2353
+ }
2354
+ if (entry.change.op === "delete") {
2355
+ tableState.delete(key);
2356
+ } else {
2357
+ tableState.set(key, entry.change.row);
2358
+ }
2359
+ asOfVersion = entry.version;
2360
+ asOfAt = entry.at;
2361
+ }
2362
+ const rows = {};
2363
+ for (const [table, map] of state) {
2364
+ rows[table] = [...map.values()];
2365
+ }
2366
+ return { asOfAt, asOfVersion, rows, truncated };
2367
+ },
2368
+ fence: ({ reason }) => {
2369
+ const handle = {
2370
+ fencedAt: Date.now(),
2371
+ reason,
2372
+ lift: () => {
2373
+ activeFences.delete(handle);
2374
+ }
2375
+ };
2376
+ activeFences.add(handle);
2377
+ return handle;
2378
+ },
2379
+ exportSnapshot: async ({
2380
+ tables,
2381
+ ctx = {}
2382
+ } = {}) => {
2383
+ const tableFilter = tables !== undefined ? new Set(tables) : undefined;
2384
+ const rows = {};
2385
+ for (const [table, reader] of readers) {
2386
+ if (tableFilter !== undefined && !tableFilter.has(table)) {
2387
+ continue;
2388
+ }
2389
+ const iterable = await reader.all(ctx);
2390
+ rows[table] = [...iterable];
2391
+ }
2392
+ return {
2393
+ exportedAt: Date.now(),
2394
+ sourceInstanceId: instanceId,
2395
+ tables: rows,
2396
+ version
2397
+ };
2398
+ },
2399
+ importSnapshot: async (snapshot, { tables, onProgress, ctx = {} } = {}) => {
2400
+ const tableFilter = tables !== undefined ? new Set(tables) : undefined;
2401
+ const perTable = {};
2402
+ const skipped = [];
2403
+ let tablesImported = 0;
2404
+ let rowsImported = 0;
2405
+ for (const [table, snapshotRows] of Object.entries(snapshot.tables)) {
2406
+ if (tableFilter !== undefined && !tableFilter.has(table)) {
2407
+ continue;
2408
+ }
2409
+ const writer = writers.get(table);
2410
+ if (writer === undefined) {
2411
+ skipped.push(table);
2412
+ continue;
2413
+ }
2414
+ const total = snapshotRows.length;
2415
+ let done = 0;
2416
+ for (const row of snapshotRows) {
2417
+ await writer.insert(row, ctx, undefined);
2418
+ done += 1;
2419
+ rowsImported += 1;
2420
+ if (onProgress !== undefined) {
2421
+ onProgress(table, done, total);
2422
+ }
2423
+ }
2424
+ perTable[table] = done;
2425
+ if (done > 0)
2426
+ tablesImported += 1;
2427
+ }
2428
+ return {
2429
+ perTable,
2430
+ rowsImported,
2431
+ skipped,
2432
+ tablesImported
2433
+ };
2434
+ },
2435
+ metrics: () => {
2436
+ const now = Date.now();
2437
+ const byCollection = {};
2438
+ let totalSubscriptions = 0;
2439
+ for (const [name, subs] of active) {
2440
+ byCollection[name] = subs.size;
2441
+ totalSubscriptions += subs.size;
2442
+ }
2443
+ const oldest = changeLog[0];
2444
+ return {
2445
+ at: now,
2446
+ changeLog: {
2447
+ capacity: changeLogSize,
2448
+ entries: changeLog.length,
2449
+ oldestAgeMs: oldest ? now - oldest.at : null,
2450
+ oldestVersion: oldest ? oldest.version : null,
2451
+ retainMs: changeLogRetainMs
2452
+ },
2453
+ mutations: {
2454
+ completed: mutationsCompleted,
2455
+ failed: mutationsFailed,
2456
+ inFlight: mutationsInFlight,
2457
+ queued: mutationsQueued,
2458
+ retried: mutationsRetried
2459
+ },
2460
+ reactiveCache: {
2461
+ capacity: reactiveCacheMax,
2462
+ entries: cachedReruns.size
2463
+ },
2464
+ schedules: {
2465
+ registered: schedules.size
2466
+ },
2467
+ source: {
2468
+ changesReceived: sourceChangesReceived,
2469
+ connected: connectedSources,
2470
+ lastChangeAgeMs: sourceLastChangeAt === null ? null : now - sourceLastChangeAt,
2471
+ lastChangeAt: sourceLastChangeAt
2472
+ },
2473
+ subscriptions: {
2474
+ byCollection,
2475
+ byTenant: Object.fromEntries(subscriptionsByTenant),
2476
+ total: totalSubscriptions
2477
+ },
2478
+ uptimeMs: now - engineStartedAt,
2479
+ version
2480
+ };
2481
+ },
2482
+ onActivity: (listener) => {
2483
+ activityListeners.add(listener);
2484
+ return () => {
2485
+ activityListeners.delete(listener);
2486
+ };
2487
+ },
2488
+ streamChanges: ({
2489
+ since = 0,
2490
+ signal,
2491
+ maxBuffer = 1e4
2492
+ } = {}) => {
2493
+ const oldest = changeLog[0];
2494
+ if (since > 0 && oldest !== undefined && oldest.version > since + 1) {
2495
+ const err = new MissedChangesError(since, oldest.version);
2496
+ return {
2497
+ [Symbol.asyncIterator]() {
2498
+ return {
2499
+ next: () => Promise.reject(err)
2500
+ };
2501
+ }
2502
+ };
2503
+ }
2504
+ const buffer = [];
2505
+ let waiter = null;
2506
+ let overflow = false;
2507
+ const wake = () => {
2508
+ if (waiter !== null) {
2509
+ const resume = waiter;
2510
+ waiter = null;
2511
+ resume();
2512
+ }
2513
+ };
2514
+ const subscriber = (entry) => {
2515
+ if (buffer.length >= maxBuffer) {
2516
+ overflow = true;
2517
+ wake();
2518
+ return;
2519
+ }
2520
+ buffer.push(entry);
2521
+ wake();
2522
+ };
2523
+ streamSubscribers.add(subscriber);
2524
+ const onAbort = () => wake();
2525
+ signal?.addEventListener("abort", onAbort, { once: true });
2526
+ let lastDelivered = since;
2527
+ return {
2528
+ async* [Symbol.asyncIterator]() {
2529
+ try {
2530
+ const history = [...changeLog];
2531
+ const headVersion = history.length > 0 ? history[history.length - 1].version : since;
2532
+ for (const entry of history) {
2533
+ if (signal?.aborted)
2534
+ return;
2535
+ if (entry.version > since) {
2536
+ lastDelivered = entry.version;
2537
+ yield entry;
2538
+ }
2539
+ }
2540
+ while (!signal?.aborted) {
2541
+ while (buffer.length > 0) {
2542
+ const entry = buffer.shift();
2543
+ if (entry.version > headVersion) {
2544
+ lastDelivered = entry.version;
2545
+ yield entry;
2546
+ }
2547
+ }
2548
+ if (overflow) {
2549
+ throw new CdcConsumerSlowError(maxBuffer, lastDelivered);
2550
+ }
2551
+ if (signal?.aborted)
2552
+ return;
2553
+ await new Promise((resolve) => {
2554
+ waiter = resolve;
2555
+ });
2556
+ }
2557
+ } finally {
2558
+ streamSubscribers.delete(subscriber);
2559
+ signal?.removeEventListener("abort", onAbort);
2560
+ }
2561
+ }
2562
+ };
2563
+ }
2564
+ };
2565
+ return engine;
2566
+ };
2567
+
2568
+ // src/engine/socket.ts
2569
+ import { Elysia } from "elysia";
2570
+
2571
+ // src/serializer.ts
2572
+ var jsonSerializer = {
2573
+ decode: (raw) => {
2574
+ if (typeof raw === "string") {
2575
+ try {
2576
+ return JSON.parse(raw);
2577
+ } catch {
2578
+ return null;
2579
+ }
2580
+ }
2581
+ if (raw instanceof Uint8Array) {
2582
+ try {
2583
+ return JSON.parse(new TextDecoder().decode(raw));
2584
+ } catch {
2585
+ return null;
2586
+ }
2587
+ }
2588
+ if (raw instanceof ArrayBuffer) {
2589
+ try {
2590
+ return JSON.parse(new TextDecoder().decode(new Uint8Array(raw)));
2591
+ } catch {
2592
+ return null;
2593
+ }
2594
+ }
2595
+ return raw;
2596
+ },
2597
+ encodeClient: (frame) => JSON.stringify(frame),
2598
+ encodeServer: (frame) => JSON.stringify(frame)
2599
+ };
2600
+
2601
+ // src/engine/connection.ts
2602
+ var parseFrame = (raw, serializer) => {
2603
+ let value = raw;
2604
+ if (typeof value === "string" || value instanceof Uint8Array || value instanceof ArrayBuffer) {
2605
+ value = serializer.decode(raw);
2606
+ if (value === null)
2607
+ return;
2608
+ }
2609
+ if (typeof value !== "object" || value === null) {
2610
+ return;
2611
+ }
2612
+ const frame = value;
2613
+ if (frame.type === "subscribe") {
2614
+ const since = typeof frame.since === "number" || typeof frame.since === "string" ? frame.since : undefined;
2615
+ return typeof frame.id === "string" && typeof frame.collection === "string" ? {
2616
+ type: "subscribe",
2617
+ id: frame.id,
2618
+ collection: frame.collection,
2619
+ params: frame.params,
2620
+ since
2621
+ } : undefined;
2622
+ }
2623
+ if (frame.type === "unsubscribe") {
2624
+ return typeof frame.id === "string" ? { type: "unsubscribe", id: frame.id } : undefined;
2625
+ }
2626
+ if (frame.type === "mutate") {
2627
+ return typeof frame.mutationId === "number" && typeof frame.name === "string" ? {
2628
+ type: "mutate",
2629
+ mutationId: frame.mutationId,
2630
+ name: frame.name,
2631
+ args: frame.args
2632
+ } : undefined;
2633
+ }
2634
+ if (frame.type === "presence-join") {
2635
+ return typeof frame.room === "string" && typeof frame.memberId === "string" ? {
2636
+ type: "presence-join",
2637
+ room: frame.room,
2638
+ memberId: frame.memberId,
2639
+ state: frame.state
2640
+ } : undefined;
2641
+ }
2642
+ if (frame.type === "presence-set") {
2643
+ return typeof frame.room === "string" ? { type: "presence-set", room: frame.room, state: frame.state } : undefined;
2644
+ }
2645
+ if (frame.type === "presence-leave") {
2646
+ return typeof frame.room === "string" ? { type: "presence-leave", room: frame.room } : undefined;
2647
+ }
2648
+ return;
2649
+ };
2650
+ var createSyncConnection = ({
2651
+ engine,
2652
+ ctx,
2653
+ send: rawSend,
2654
+ presence,
2655
+ serializer = jsonSerializer
2656
+ }) => {
2657
+ const subscriptions = new Map;
2658
+ const presenceRooms = new Map;
2659
+ let framesSent = 0;
2660
+ let slowSendsRecent = 0;
2661
+ const send = (frame) => {
2662
+ const ret = rawSend(frame);
2663
+ if (ret === -1) {
2664
+ slowSendsRecent += 1;
2665
+ } else {
2666
+ framesSent += 1;
2667
+ slowSendsRecent = 0;
2668
+ }
2669
+ };
2670
+ let pending = [];
2671
+ let pendingVersion;
2672
+ let pendingCursor;
2673
+ let flushScheduled = false;
2674
+ const flush = () => {
2675
+ if (pending.length === 0) {
2676
+ return;
2677
+ }
2678
+ const diffs = pending;
2679
+ const version = pendingVersion;
2680
+ const cursor = pendingCursor;
2681
+ pending = [];
2682
+ pendingVersion = undefined;
2683
+ pendingCursor = undefined;
2684
+ if (diffs.length === 1) {
2685
+ const only = diffs[0];
2686
+ send({
2687
+ type: "diff",
2688
+ id: only.id,
2689
+ added: only.added,
2690
+ removed: only.removed,
2691
+ changed: only.changed,
2692
+ version,
2693
+ cursor
2694
+ });
2695
+ } else {
2696
+ send({ type: "frame", diffs, version, cursor });
2697
+ }
2698
+ };
2699
+ const scheduleFlush = () => {
2700
+ if (flushScheduled) {
2701
+ return;
2702
+ }
2703
+ flushScheduled = true;
2704
+ queueMicrotask(() => {
2705
+ flushScheduled = false;
2706
+ flush();
2707
+ });
2708
+ };
2709
+ const bufferDiff = (diff, diffVersion, cursor) => {
2710
+ if (pending.length > 0 && pendingVersion !== diffVersion) {
2711
+ flush();
2712
+ }
2713
+ pending.push(diff);
2714
+ pendingVersion = diffVersion;
2715
+ if (cursor !== undefined)
2716
+ pendingCursor = cursor;
2717
+ scheduleFlush();
2718
+ };
2719
+ const handle = async (raw) => {
2720
+ const frame = parseFrame(raw, serializer);
2721
+ if (frame === undefined) {
2722
+ send({ type: "error", message: "Malformed sync frame" });
2723
+ return;
2724
+ }
2725
+ if (frame.type === "mutate") {
2726
+ try {
2727
+ const result = await engine.runMutation(frame.name, frame.args, ctx);
2728
+ flush();
2729
+ send({ type: "ack", mutationId: frame.mutationId, result });
2730
+ } catch (error) {
2731
+ send({
2732
+ type: "reject",
2733
+ mutationId: frame.mutationId,
2734
+ message: error instanceof Error ? error.message : String(error)
2735
+ });
2736
+ }
2737
+ return;
2738
+ }
2739
+ if (frame.type === "unsubscribe") {
2740
+ subscriptions.get(frame.id)?.unsubscribe();
2741
+ subscriptions.delete(frame.id);
2742
+ return;
2743
+ }
2744
+ if (frame.type === "presence-join") {
2745
+ if (presence === undefined) {
2746
+ send({ type: "error", message: "Presence is not enabled" });
2747
+ return;
2748
+ }
2749
+ presenceRooms.get(frame.room)?.leave();
2750
+ const handle2 = presence.join(frame.room, frame.memberId, frame.state, (diff) => {
2751
+ send({
2752
+ type: "presence",
2753
+ room: frame.room,
2754
+ joined: diff.joined,
2755
+ updated: diff.updated,
2756
+ left: diff.left
2757
+ });
2758
+ });
2759
+ presenceRooms.set(frame.room, handle2);
2760
+ send({
2761
+ type: "presence",
2762
+ room: frame.room,
2763
+ joined: handle2.members,
2764
+ updated: [],
2765
+ left: []
2766
+ });
2767
+ return;
2768
+ }
2769
+ if (frame.type === "presence-set") {
2770
+ presenceRooms.get(frame.room)?.set(frame.state);
2771
+ return;
2772
+ }
2773
+ if (frame.type === "presence-leave") {
2774
+ presenceRooms.get(frame.room)?.leave();
2775
+ presenceRooms.delete(frame.room);
2776
+ return;
2777
+ }
2778
+ if (subscriptions.has(frame.id)) {
2779
+ send({
2780
+ type: "error",
2781
+ id: frame.id,
2782
+ message: `Subscription id "${frame.id}" already in use`
2783
+ });
2784
+ return;
2785
+ }
2786
+ try {
2787
+ const subscription = await engine.subscribe({
2788
+ collection: frame.collection,
2789
+ params: frame.params,
2790
+ ctx,
2791
+ since: frame.since,
2792
+ onDiff: (diff, diffVersion, cursor) => {
2793
+ bufferDiff({
2794
+ id: frame.id,
2795
+ added: diff.added,
2796
+ removed: diff.removed,
2797
+ changed: diff.changed
2798
+ }, diffVersion, cursor);
2799
+ }
2800
+ });
2801
+ subscriptions.set(frame.id, subscription);
2802
+ if (subscription.catchup !== undefined) {
2803
+ send({
2804
+ type: "diff",
2805
+ id: frame.id,
2806
+ added: subscription.catchup.added,
2807
+ removed: subscription.catchup.removed,
2808
+ changed: subscription.catchup.changed,
2809
+ version: subscription.version,
2810
+ cursor: subscription.cursor
2811
+ });
2812
+ } else {
2813
+ send({
2814
+ type: "snapshot",
2815
+ id: frame.id,
2816
+ rows: subscription.initial,
2817
+ version: subscription.version,
2818
+ cursor: subscription.cursor
2819
+ });
2820
+ }
2821
+ } catch (error) {
2822
+ send({
2823
+ type: "error",
2824
+ id: frame.id,
2825
+ message: error instanceof Error ? error.message : String(error)
2826
+ });
2827
+ }
2828
+ };
2829
+ const close = () => {
2830
+ for (const subscription of subscriptions.values()) {
2831
+ subscription.unsubscribe();
2832
+ }
2833
+ subscriptions.clear();
2834
+ for (const handle2 of presenceRooms.values()) {
2835
+ handle2.leave();
2836
+ }
2837
+ presenceRooms.clear();
2838
+ };
2839
+ const stats = () => ({
2840
+ framesSent,
2841
+ presenceRoomCount: presenceRooms.size,
2842
+ slowSendsRecent,
2843
+ subscriptionCount: subscriptions.size
2844
+ });
2845
+ return { close, handle, stats };
2846
+ };
2847
+
2848
+ // src/engine/socket.ts
2849
+ var syncSocket = ({
2850
+ engine,
2851
+ path = "/sync/ws",
2852
+ resolveContext,
2853
+ presence,
2854
+ maxBufferedBytes,
2855
+ onSlow,
2856
+ closeOnSlow = false,
2857
+ serializer = jsonSerializer
2858
+ }) => {
2859
+ const connections = new Map;
2860
+ const threshold = maxBufferedBytes ?? Infinity;
2861
+ const fireSlow = (event) => {
2862
+ if (!onSlow)
2863
+ return;
2864
+ try {
2865
+ const ret = onSlow(event);
2866
+ if (ret && typeof ret.then === "function") {
2867
+ ret.catch((error) => {
2868
+ console.error("[sync/socket] onSlow rejected:", error);
2869
+ });
2870
+ }
2871
+ } catch (error) {
2872
+ console.error("[sync/socket] onSlow threw:", error);
2873
+ }
2874
+ };
2875
+ return new Elysia({ name: "@absolutejs/sync/socket" }).ws(path, {
2876
+ async open(ws) {
2877
+ const bunWs = ws;
2878
+ const tracked = {
2879
+ connection: null,
2880
+ pending: [],
2881
+ slowSignaled: false
2882
+ };
2883
+ connections.set(bunWs.id, tracked);
2884
+ const ctx = resolveContext ? await resolveContext(ws.data) : {};
2885
+ const connection = createSyncConnection({
2886
+ engine,
2887
+ ctx,
2888
+ presence,
2889
+ serializer,
2890
+ send: (frame) => {
2891
+ const payload = serializer.encodeServer(frame);
2892
+ const ret = bunWs.send(typeof payload === "string" ? payload : payload);
2893
+ const buffered2 = bunWs.getBufferedAmount?.() ?? 0;
2894
+ const overBuffer = buffered2 > threshold;
2895
+ const backpressure = ret === -1;
2896
+ if ((overBuffer || backpressure) && !tracked.slowSignaled) {
2897
+ tracked.slowSignaled = true;
2898
+ fireSlow({
2899
+ bufferedAmount: buffered2,
2900
+ reason: backpressure ? "send-backpressure" : "buffer-threshold",
2901
+ stats: connection.stats(),
2902
+ wsId: bunWs.id
2903
+ });
2904
+ if (closeOnSlow)
2905
+ bunWs.close?.();
2906
+ }
2907
+ return ret;
2908
+ }
2909
+ });
2910
+ if (!connections.has(bunWs.id)) {
2911
+ connection.close();
2912
+ return;
2913
+ }
2914
+ tracked.connection = connection;
2915
+ const buffered = tracked.pending;
2916
+ tracked.pending = [];
2917
+ for (const frame of buffered) {
2918
+ await connection.handle(frame);
2919
+ }
2920
+ },
2921
+ async message(ws, message) {
2922
+ const tracked = connections.get(ws.id);
2923
+ if (!tracked)
2924
+ return;
2925
+ if (tracked.connection) {
2926
+ await tracked.connection.handle(message);
2927
+ } else {
2928
+ tracked.pending.push(message);
2929
+ }
2930
+ },
2931
+ drain(ws) {
2932
+ const tracked = connections.get(ws.id);
2933
+ if (tracked)
2934
+ tracked.slowSignaled = false;
2935
+ },
2936
+ close(ws) {
2937
+ const tracked = connections.get(ws.id);
2938
+ if (tracked) {
2939
+ tracked.connection?.close();
2940
+ connections.delete(ws.id);
2941
+ }
2942
+ }
2943
+ });
2944
+ };
2945
+
2946
+ // src/plugin.ts
2947
+ import { Elysia as Elysia2 } from "elysia";
2948
+
2949
+ // src/reactiveHub.ts
2950
+ var SYNC_OPEN_TOPIC = "@absolutejs/sync:open";
2951
+ var createReactiveHub = () => {
2952
+ const subscriptions = new Set;
2953
+ const matches = (subscription, topic) => {
2954
+ if (subscription.exact.has(topic)) {
2955
+ return true;
2956
+ }
2957
+ return subscription.prefixes.some((prefix) => topic.startsWith(prefix));
2958
+ };
2959
+ return {
2960
+ publish: (topic, payload) => {
2961
+ const event = { topic, at: Date.now(), payload };
2962
+ for (const subscription of subscriptions) {
2963
+ if (matches(subscription, topic)) {
2964
+ subscription.listener(event);
2965
+ }
2966
+ }
2967
+ },
2968
+ subscribe: (topics, listener) => {
2969
+ const exact = new Set;
2970
+ const prefixes = [];
2971
+ for (const topic of topics) {
2972
+ if (topic.endsWith("*")) {
2973
+ prefixes.push(topic.slice(0, -1));
2974
+ } else {
2975
+ exact.add(topic);
2976
+ }
2977
+ }
2978
+ const subscription = { exact, prefixes, listener };
2979
+ subscriptions.add(subscription);
2980
+ return () => {
2981
+ subscriptions.delete(subscription);
2982
+ };
2983
+ },
2984
+ subscriberCount: (topic) => {
2985
+ if (topic === undefined) {
2986
+ return subscriptions.size;
2987
+ }
2988
+ let count = 0;
2989
+ for (const subscription of subscriptions) {
2990
+ if (matches(subscription, topic)) {
2991
+ count += 1;
2992
+ }
2993
+ }
2994
+ return count;
2995
+ }
2996
+ };
2997
+ };
2998
+
2999
+ // src/plugin.ts
3000
+ var defaultResolveTopics = (context) => (context.query.topics ?? "").split(",").map((topic) => topic.trim()).filter(Boolean);
3001
+ var sync = ({
3002
+ hub,
3003
+ path = "/sync",
3004
+ resolveTopics = defaultResolveTopics,
3005
+ heartbeatMs = 25000
3006
+ }) => {
3007
+ const app = new Elysia2({ name: "@absolutejs/sync" }).get(path, (context) => {
3008
+ const topics = resolveTopics({
3009
+ query: context.query,
3010
+ request: context.request
3011
+ });
3012
+ const encoder = new TextEncoder;
3013
+ const stream = new ReadableStream({
3014
+ start(controller) {
3015
+ const write = (chunk) => {
3016
+ try {
3017
+ controller.enqueue(encoder.encode(chunk));
3018
+ } catch {}
3019
+ };
3020
+ const send = (event) => {
3021
+ write(`data: ${JSON.stringify(event)}
3022
+
3023
+ `);
3024
+ };
3025
+ send({
3026
+ topic: SYNC_OPEN_TOPIC,
3027
+ at: Date.now(),
3028
+ payload: { topics }
3029
+ });
3030
+ const unsubscribe = topics.length > 0 ? hub.subscribe(topics, send) : () => {};
3031
+ const heartbeat = setInterval(() => write(`: ping
3032
+
3033
+ `), heartbeatMs);
3034
+ context.request.signal.addEventListener("abort", () => {
3035
+ clearInterval(heartbeat);
3036
+ unsubscribe();
3037
+ try {
3038
+ controller.close();
3039
+ } catch {}
3040
+ }, { once: true });
3041
+ }
3042
+ });
3043
+ return new Response(stream, {
3044
+ headers: {
3045
+ "cache-control": "no-cache, no-transform",
3046
+ connection: "keep-alive",
3047
+ "content-type": "text/event-stream",
3048
+ "x-accel-buffering": "no"
3049
+ }
3050
+ });
3051
+ });
3052
+ return app;
3053
+ };
3054
+
3055
+ // src/platform.ts
3056
+ var PLATFORM_SYNC_ENVIRONMENT_KEY = "ABSOLUTE_SYNC_RUNTIME";
3057
+ var PLATFORM_SYNC_HEALTH_PATH = "/.well-known/absolute/sync";
3058
+ var PlatformSyncConfigurationSchema = Type.Object({
3059
+ changeLogRetainMs: Type.Integer({
3060
+ maximum: 604800000,
3061
+ minimum: 1000
3062
+ }),
3063
+ changeLogSize: Type.Integer({ maximum: 1e4, minimum: 1 }),
3064
+ closeOnSlow: Type.Boolean(),
3065
+ heartbeatMs: Type.Integer({ maximum: 120000, minimum: 1000 }),
3066
+ instanceId: Type.String({
3067
+ maxLength: 128,
3068
+ minLength: 1,
3069
+ pattern: "^[A-Za-z0-9_.:-]+$"
3070
+ }),
3071
+ maxBufferedBytes: Type.Integer({
3072
+ maximum: 16777216,
3073
+ minimum: 65536
3074
+ }),
3075
+ mutationConcurrency: Type.Integer({ maximum: 64, minimum: 1 }),
3076
+ mutationQueueLimit: Type.Integer({ maximum: 1e4, minimum: 0 }),
3077
+ pushPath: Type.String({
3078
+ maxLength: 128,
3079
+ minLength: 1,
3080
+ pattern: "^/[^?#]*$"
3081
+ }),
3082
+ socketPath: Type.String({
3083
+ maxLength: 128,
3084
+ minLength: 1,
3085
+ pattern: "^/[^?#]*$"
3086
+ }),
3087
+ tier: Type.Union([
3088
+ Type.Literal("push"),
3089
+ Type.Literal("engine"),
3090
+ Type.Literal("both")
3091
+ ]),
3092
+ version: Type.Literal(1)
3093
+ }, { additionalProperties: false });
3094
+
3095
+ class PlatformSyncConfigurationError extends Error {
3096
+ }
3097
+ var readPlatformSyncConfiguration = (environment = process.env) => {
3098
+ const serialized = environment[PLATFORM_SYNC_ENVIRONMENT_KEY]?.trim();
3099
+ if (!serialized)
3100
+ return null;
3101
+ let parsed;
3102
+ try {
3103
+ parsed = JSON.parse(serialized);
3104
+ } catch {
3105
+ throw new PlatformSyncConfigurationError("Platform Sync configuration is not valid JSON");
3106
+ }
3107
+ if (!Value.Check(PlatformSyncConfigurationSchema, parsed))
3108
+ throw new PlatformSyncConfigurationError("Platform Sync configuration is invalid");
3109
+ return parsed;
3110
+ };
3111
+ var createPlatformSyncRuntime = (options = {}) => {
3112
+ const configuration = options.configuration ?? readPlatformSyncConfiguration();
3113
+ if (!configuration)
3114
+ throw new PlatformSyncConfigurationError("Platform Sync is not configured");
3115
+ const pushEnabled = configuration.tier !== "engine";
3116
+ const engineEnabled = configuration.tier !== "push";
3117
+ const hub = pushEnabled ? options.hub ?? createReactiveHub() : undefined;
3118
+ const engine = engineEnabled ? createSyncEngine({
3119
+ ...options.engineOptions,
3120
+ changeLogRetainMs: configuration.changeLogRetainMs,
3121
+ changeLogSize: configuration.changeLogSize,
3122
+ instanceId: configuration.instanceId,
3123
+ mutationConcurrency: configuration.mutationConcurrency,
3124
+ mutationQueueLimit: configuration.mutationQueueLimit
3125
+ }) : undefined;
3126
+ const app = new Elysia3({ name: "@absolutejs/sync/platform" }).use(hub ? sync({
3127
+ heartbeatMs: configuration.heartbeatMs,
3128
+ hub,
3129
+ path: configuration.pushPath,
3130
+ ...options.resolveTopics ? { resolveTopics: options.resolveTopics } : {}
3131
+ }) : new Elysia3({ name: "@absolutejs/sync/platform/no-push" })).use(engine ? syncSocket({
3132
+ closeOnSlow: configuration.closeOnSlow,
3133
+ engine,
3134
+ maxBufferedBytes: configuration.maxBufferedBytes,
3135
+ path: configuration.socketPath,
3136
+ ...options.onSlow ? { onSlow: options.onSlow } : {},
3137
+ ...options.resolveContext ? { resolveContext: options.resolveContext } : {}
3138
+ }) : new Elysia3({ name: "@absolutejs/sync/platform/no-engine" })).get(PLATFORM_SYNC_HEALTH_PATH, () => ({
3139
+ contract: 1,
3140
+ instanceId: configuration.instanceId,
3141
+ ready: true,
3142
+ tier: configuration.tier
3143
+ }));
3144
+ return {
3145
+ app,
3146
+ configuration,
3147
+ engine,
3148
+ hub,
3149
+ metrics: () => engine?.metrics() ?? null
3150
+ };
3151
+ };
3152
+ export {
3153
+ readPlatformSyncConfiguration,
3154
+ createPlatformSyncRuntime,
3155
+ PlatformSyncConfigurationSchema,
3156
+ PlatformSyncConfigurationError,
3157
+ PLATFORM_SYNC_HEALTH_PATH,
3158
+ PLATFORM_SYNC_ENVIRONMENT_KEY
3159
+ };
3160
+
3161
+ //# debugId=49D1023B83CB5B7D64756E2164756E21
3162
+ //# sourceMappingURL=platform.js.map