@absolutejs/sync 1.20.1 → 1.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/engine/index.js +289 -164
- package/dist/engine/index.js.map +5 -4
- package/dist/engine/syncEngine.d.ts +91 -0
- package/dist/index.js +289 -164
- package/dist/index.js.map +5 -4
- package/dist/testing.js +289 -164
- package/dist/testing.js.map +5 -4
- package/package.json +5 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type TracerProvider as TelemetryTracerProvider } from '@absolutejs/telemetry';
|
|
1
2
|
import type { CollectionContext, CollectionDefinition, JoinCollectionDefinition } from './collection';
|
|
2
3
|
import type { GraphCollectionDefinition } from './graph';
|
|
3
4
|
import type { MutationDefinition, TableWriter, TransactionRunner } from './mutation';
|
|
@@ -296,6 +297,33 @@ export type SyncEngine = {
|
|
|
296
297
|
* Added in 1.19.0.
|
|
297
298
|
*/
|
|
298
299
|
importChangeLog: (snapshot: ChangeLogSnapshot) => number;
|
|
300
|
+
/**
|
|
301
|
+
* Reconstruct the state of registered tables as of a target
|
|
302
|
+
* timestamp by walking the change log forward and folding each op
|
|
303
|
+
* into a per-table view. Useful for forensic incident response
|
|
304
|
+
* ("what did the tenant see at 14:32?") and the "I deleted prod
|
|
305
|
+
* — restore us to 2h ago" recovery story.
|
|
306
|
+
*
|
|
307
|
+
* The reconstruction is exact when the log spans `targetAt` (i.e.
|
|
308
|
+
* the log's oldest entry is at version 1). When the log has been
|
|
309
|
+
* trimmed (`changeLogSize` / `changeLogRetainMs` evicted older
|
|
310
|
+
* entries) AND `targetAt` falls in the gap, the result is
|
|
311
|
+
* best-effort: state walked forward from the OLDEST retained
|
|
312
|
+
* entry, with `truncated: true` so the caller knows.
|
|
313
|
+
*
|
|
314
|
+
* Added in 1.22.0.
|
|
315
|
+
*
|
|
316
|
+
* @example
|
|
317
|
+
* ```ts
|
|
318
|
+
* const twoHoursAgo = Date.now() - 2 * 60 * 60 * 1000;
|
|
319
|
+
* const result = await engine.replayTo({ at: twoHoursAgo, tables: ['orders'] });
|
|
320
|
+
* if (result.truncated) {
|
|
321
|
+
* console.warn('Replay truncated — log retention window too short.');
|
|
322
|
+
* }
|
|
323
|
+
* console.log(result.rows.orders); // orders as of two hours ago
|
|
324
|
+
* ```
|
|
325
|
+
*/
|
|
326
|
+
replayTo: (options: ReplayOptions) => Promise<ReplayResult>;
|
|
299
327
|
/**
|
|
300
328
|
* Subscribe to the live engine activity stream (changes, mutation outcomes,
|
|
301
329
|
* subscribe/unsubscribe). Returns an unsubscribe. Powers the devtools feed.
|
|
@@ -466,6 +494,48 @@ export type ChangeLogSnapshot = {
|
|
|
466
494
|
*/
|
|
467
495
|
exportedAt?: number;
|
|
468
496
|
};
|
|
497
|
+
/**
|
|
498
|
+
* Options for {@link SyncEngine.replayTo}. Added in 1.22.0.
|
|
499
|
+
*/
|
|
500
|
+
export type ReplayOptions = {
|
|
501
|
+
/**
|
|
502
|
+
* Target timestamp (`Date.now()`-shaped). The engine walks the
|
|
503
|
+
* change log forward, applying entries with `at <= targetAt`. The
|
|
504
|
+
* result is the state as-of `targetAt` (or as close as the log
|
|
505
|
+
* permits — see `truncated`).
|
|
506
|
+
*/
|
|
507
|
+
at: number;
|
|
508
|
+
/**
|
|
509
|
+
* Optional table filter. When set, only entries whose `table` is
|
|
510
|
+
* in this list are folded into the result; entries for other
|
|
511
|
+
* tables are skipped. Useful for "show me what `tasks` looked
|
|
512
|
+
* like at T" without paying to reconstruct every table.
|
|
513
|
+
*/
|
|
514
|
+
tables?: ReadonlyArray<string>;
|
|
515
|
+
};
|
|
516
|
+
/**
|
|
517
|
+
* Returned by {@link SyncEngine.replayTo}. Added in 1.22.0.
|
|
518
|
+
*
|
|
519
|
+
* - `rows` — per-table arrays of rows that existed as of `asOfAt`.
|
|
520
|
+
* Keys are table names; values are the row objects (in last-write
|
|
521
|
+
* order — last write wins for duplicate-keyed inserts).
|
|
522
|
+
* - `asOfVersion` / `asOfAt` — the version + wall-clock of the LAST
|
|
523
|
+
* entry folded into the result. May be earlier than `targetAt` if
|
|
524
|
+
* no entries existed between the last-included entry and the
|
|
525
|
+
* target.
|
|
526
|
+
* - `truncated` — `true` when the log has been trimmed past the
|
|
527
|
+
* target window (`changeLog[0].version > 1 && changeLog[0].at >
|
|
528
|
+
* targetAt`). In this case, `rows` represents the state walked
|
|
529
|
+
* forward from the OLDEST retained entry — NOT the actual state
|
|
530
|
+
* at `targetAt`. The caller should treat the result as
|
|
531
|
+
* "best-effort given retention window" and warn the operator.
|
|
532
|
+
*/
|
|
533
|
+
export type ReplayResult = {
|
|
534
|
+
asOfVersion: number;
|
|
535
|
+
asOfAt: number;
|
|
536
|
+
rows: Record<string, ReadonlyArray<unknown>>;
|
|
537
|
+
truncated: boolean;
|
|
538
|
+
};
|
|
469
539
|
export type SyncEngineOptions = {
|
|
470
540
|
/**
|
|
471
541
|
* Stable identifier for this engine instance. Defaults to a per-process
|
|
@@ -633,6 +703,27 @@ export type SyncEngineOptions = {
|
|
|
633
703
|
collection: string;
|
|
634
704
|
}) => string | undefined;
|
|
635
705
|
};
|
|
706
|
+
/**
|
|
707
|
+
* Optional OpenTelemetry tracer provider. When set, the engine
|
|
708
|
+
* wraps `subscribe`, `runMutation`, `runMutations`, and cluster
|
|
709
|
+
* fan-out in spans named `sync.<op>` with `ABS_ATTRS` semantic
|
|
710
|
+
* conventions (`abs.engine.id`, `abs.collection`, `abs.mutation`,
|
|
711
|
+
* etc.). When absent, all tracing is a zero-allocation noop —
|
|
712
|
+
* existing call sites pay nothing. Added in 1.21.0.
|
|
713
|
+
*
|
|
714
|
+
* Pass any `@opentelemetry/api`-compatible `TracerProvider`. See
|
|
715
|
+
* `@absolutejs/telemetry` for the type shape — sync re-uses its
|
|
716
|
+
* helpers but doesn't peer-dep `@opentelemetry/api` directly.
|
|
717
|
+
*
|
|
718
|
+
* @example
|
|
719
|
+
* ```ts
|
|
720
|
+
* import { NodeTracerProvider } from '@opentelemetry/sdk-node';
|
|
721
|
+
* const tp = new NodeTracerProvider({ ... });
|
|
722
|
+
* tp.register();
|
|
723
|
+
* const engine = createSyncEngine({ tracerProvider: tp });
|
|
724
|
+
* ```
|
|
725
|
+
*/
|
|
726
|
+
tracerProvider?: TelemetryTracerProvider;
|
|
636
727
|
};
|
|
637
728
|
/**
|
|
638
729
|
* The Tier 3 sync engine: a registry of collections plus the view syncer. It is
|
package/dist/index.js
CHANGED
|
@@ -542,6 +542,59 @@ var syncSocket = ({
|
|
|
542
542
|
}
|
|
543
543
|
});
|
|
544
544
|
};
|
|
545
|
+
// node_modules/@absolutejs/telemetry/dist/index.js
|
|
546
|
+
var NOOP_SPAN_CONTEXT = {
|
|
547
|
+
spanId: "0000000000000000",
|
|
548
|
+
traceFlags: 0,
|
|
549
|
+
traceId: "00000000000000000000000000000000"
|
|
550
|
+
};
|
|
551
|
+
var noopSpan = {
|
|
552
|
+
addEvent: () => noopSpan,
|
|
553
|
+
end: () => {},
|
|
554
|
+
isRecording: () => false,
|
|
555
|
+
recordException: () => {},
|
|
556
|
+
setAttribute: () => noopSpan,
|
|
557
|
+
setAttributes: () => noopSpan,
|
|
558
|
+
setStatus: () => noopSpan,
|
|
559
|
+
spanContext: () => NOOP_SPAN_CONTEXT,
|
|
560
|
+
updateName: () => noopSpan
|
|
561
|
+
};
|
|
562
|
+
var startActiveSpanNoop = (_name, optionsOrFn, maybeFn) => {
|
|
563
|
+
const fn = typeof optionsOrFn === "function" ? optionsOrFn : maybeFn;
|
|
564
|
+
return fn(noopSpan);
|
|
565
|
+
};
|
|
566
|
+
var noopTracer = {
|
|
567
|
+
startActiveSpan: startActiveSpanNoop,
|
|
568
|
+
startSpan: () => noopSpan
|
|
569
|
+
};
|
|
570
|
+
var tracerOrNoop = (provider, name, version) => provider !== undefined ? provider.getTracer(name, version) : noopTracer;
|
|
571
|
+
var ABS_ATTRS = {
|
|
572
|
+
tenant: "abs.tenant",
|
|
573
|
+
shardId: "abs.shard.id",
|
|
574
|
+
engineId: "abs.engine.id",
|
|
575
|
+
collection: "abs.collection",
|
|
576
|
+
mutation: "abs.mutation",
|
|
577
|
+
mutationAttempt: "abs.mutation.attempt",
|
|
578
|
+
subscriptionId: "abs.subscription.id",
|
|
579
|
+
batchSize: "abs.batch.size",
|
|
580
|
+
clusterMessageOrigin: "abs.cluster.origin",
|
|
581
|
+
jobId: "abs.job.id",
|
|
582
|
+
jobKind: "abs.job.kind",
|
|
583
|
+
jobAttempt: "abs.job.attempt",
|
|
584
|
+
jobMaxAttempts: "abs.job.max_attempts",
|
|
585
|
+
workerId: "abs.worker.id",
|
|
586
|
+
runtimeKey: "abs.runtime.key",
|
|
587
|
+
runtimePid: "abs.runtime.pid",
|
|
588
|
+
runtimePort: "abs.runtime.port",
|
|
589
|
+
runtimeExitReason: "abs.runtime.exit_reason",
|
|
590
|
+
runtimeReadinessMs: "abs.runtime.readiness_ms",
|
|
591
|
+
routeShard: "abs.route.shard",
|
|
592
|
+
routeDecision: "abs.route.decision",
|
|
593
|
+
secretName: "abs.secret.name",
|
|
594
|
+
secretFingerprint: "abs.secret.fingerprint",
|
|
595
|
+
auditKind: "abs.audit.kind"
|
|
596
|
+
};
|
|
597
|
+
|
|
545
598
|
// src/engine/equiJoin.ts
|
|
546
599
|
var shallowEqual = (a, b) => {
|
|
547
600
|
if (a === b) {
|
|
@@ -1390,6 +1443,7 @@ var createSyncEngine = (options = {}) => {
|
|
|
1390
1443
|
const runInTransaction = options.transaction;
|
|
1391
1444
|
const instanceId = options.instanceId ?? globalThis.crypto?.randomUUID?.() ?? `i${Math.random()}`;
|
|
1392
1445
|
let clusterBus;
|
|
1446
|
+
const tracer = tracerOrNoop(options.tracerProvider, "@absolutejs/sync");
|
|
1393
1447
|
const importChangeLog = (snapshot) => {
|
|
1394
1448
|
if (version !== 0) {
|
|
1395
1449
|
throw new Error(`[sync] importChangeLog: engine already has version ${version}; ` + `restore must happen before any local writes commit.`);
|
|
@@ -2181,107 +2235,124 @@ var createSyncEngine = (options = {}) => {
|
|
|
2181
2235
|
registry.set(collection.name, collection);
|
|
2182
2236
|
},
|
|
2183
2237
|
subscribe: async ({ collection, params, ctx, onDiff, since, signal }) => {
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
}
|
|
2189
|
-
const tenantSlot = acquireSubscriptionSlot(ctx, { collection });
|
|
2190
|
-
let slotHandedOff = false;
|
|
2191
|
-
try {
|
|
2192
|
-
const typedOnDiff = onDiff;
|
|
2193
|
-
const subscribeSet = subsFor(collection);
|
|
2194
|
-
const wrapReturn = (sub) => {
|
|
2195
|
-
checkAborted(signal);
|
|
2196
|
-
const innerUnsubscribe = sub.unsubscribe;
|
|
2197
|
-
let released = false;
|
|
2198
|
-
const wrappedUnsubscribe = () => {
|
|
2199
|
-
if (released)
|
|
2200
|
-
return;
|
|
2201
|
-
released = true;
|
|
2202
|
-
releaseSubscriptionSlot(tenantSlot);
|
|
2203
|
-
innerUnsubscribe();
|
|
2204
|
-
};
|
|
2205
|
-
const wrapped = { ...sub, unsubscribe: wrappedUnsubscribe };
|
|
2206
|
-
linkAbortToUnsubscribe(signal, wrappedUnsubscribe);
|
|
2207
|
-
slotHandedOff = true;
|
|
2208
|
-
return wrapped;
|
|
2209
|
-
};
|
|
2210
|
-
const registeredKind = registered.kind;
|
|
2211
|
-
if (registeredKind === "join") {
|
|
2212
|
-
const joined = await subscribeJoin(collection, registered, params, ctx, typedOnDiff, subscribeSet);
|
|
2213
|
-
return wrapReturn(joined);
|
|
2238
|
+
const subscribeSpan = tracer.startSpan("sync.subscribe", {
|
|
2239
|
+
attributes: {
|
|
2240
|
+
[ABS_ATTRS.engineId]: instanceId,
|
|
2241
|
+
[ABS_ATTRS.collection]: collection
|
|
2214
2242
|
}
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
if (
|
|
2220
|
-
|
|
2221
|
-
return wrapReturn(reactived);
|
|
2222
|
-
}
|
|
2223
|
-
if (registeredKind === "search") {
|
|
2224
|
-
const searched = await subscribeSearch(collection, registered, params, ctx, typedOnDiff, subscribeSet);
|
|
2225
|
-
return wrapReturn(searched);
|
|
2243
|
+
});
|
|
2244
|
+
try {
|
|
2245
|
+
checkAborted(signal);
|
|
2246
|
+
const registered = registry.get(collection);
|
|
2247
|
+
if (registered === undefined) {
|
|
2248
|
+
throw new Error(`Unknown collection "${collection}"`);
|
|
2226
2249
|
}
|
|
2227
|
-
const
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2250
|
+
const tenantSlot = acquireSubscriptionSlot(ctx, { collection });
|
|
2251
|
+
let slotHandedOff = false;
|
|
2252
|
+
try {
|
|
2253
|
+
const typedOnDiff = onDiff;
|
|
2254
|
+
const subscribeSet = subsFor(collection);
|
|
2255
|
+
const wrapReturn = (sub) => {
|
|
2256
|
+
checkAborted(signal);
|
|
2257
|
+
const innerUnsubscribe = sub.unsubscribe;
|
|
2258
|
+
let released = false;
|
|
2259
|
+
const wrappedUnsubscribe = () => {
|
|
2260
|
+
if (released)
|
|
2261
|
+
return;
|
|
2262
|
+
released = true;
|
|
2263
|
+
releaseSubscriptionSlot(tenantSlot);
|
|
2264
|
+
innerUnsubscribe();
|
|
2265
|
+
};
|
|
2266
|
+
const wrapped = { ...sub, unsubscribe: wrappedUnsubscribe };
|
|
2267
|
+
linkAbortToUnsubscribe(signal, wrappedUnsubscribe);
|
|
2268
|
+
slotHandedOff = true;
|
|
2269
|
+
return wrapped;
|
|
2270
|
+
};
|
|
2271
|
+
const registeredKind = registered.kind;
|
|
2272
|
+
if (registeredKind === "join") {
|
|
2273
|
+
const joined = await subscribeJoin(collection, registered, params, ctx, typedOnDiff, subscribeSet);
|
|
2274
|
+
return wrapReturn(joined);
|
|
2275
|
+
}
|
|
2276
|
+
if (registeredKind === "graph") {
|
|
2277
|
+
const graphed = await subscribeGraph(collection, registered, params, ctx, typedOnDiff, subscribeSet);
|
|
2278
|
+
return wrapReturn(graphed);
|
|
2279
|
+
}
|
|
2280
|
+
if (registeredKind === "reactive") {
|
|
2281
|
+
const reactived = await subscribeReactive(collection, registered, params, ctx, typedOnDiff, subscribeSet);
|
|
2282
|
+
return wrapReturn(reactived);
|
|
2283
|
+
}
|
|
2284
|
+
if (registeredKind === "search") {
|
|
2285
|
+
const searched = await subscribeSearch(collection, registered, params, ctx, typedOnDiff, subscribeSet);
|
|
2286
|
+
return wrapReturn(searched);
|
|
2287
|
+
}
|
|
2288
|
+
const definition = registered;
|
|
2289
|
+
if (definition.authorize !== undefined) {
|
|
2290
|
+
const allowed = await definition.authorize(params, ctx);
|
|
2291
|
+
if (!allowed) {
|
|
2292
|
+
throw new UnauthorizedError(`subscribe to collection "${collection}"`);
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
const key = definition.key ?? defaultKey;
|
|
2296
|
+
const match = definition.match;
|
|
2297
|
+
const tables = definition.tables ?? [collection];
|
|
2298
|
+
const scopedTable = tables.length === 1 ? tables[0] : undefined;
|
|
2299
|
+
const readRule = scopedTable !== undefined ? readRuleFor(scopedTable) : undefined;
|
|
2300
|
+
const rehydrate = async () => {
|
|
2301
|
+
const raw = [...await definition.hydrate(params, ctx)];
|
|
2302
|
+
const rows = scopedTable !== undefined ? raw.map((row) => migrateRow(scopedTable, row)) : raw;
|
|
2303
|
+
return readRule ? rows.filter((row) => readRule(ctx, row)) : rows;
|
|
2304
|
+
};
|
|
2305
|
+
const incremental = match !== undefined && tables.length === 1;
|
|
2306
|
+
const boundMatch = incremental ? (row) => match(row, params, ctx) && (readRule ? readRule(ctx, row) : true) : () => true;
|
|
2307
|
+
const view = createMaterializedView({
|
|
2308
|
+
key,
|
|
2309
|
+
match: boundMatch
|
|
2310
|
+
});
|
|
2311
|
+
const resuming = since !== undefined && canResume(since, incremental);
|
|
2312
|
+
view.hydrate([...await rehydrate()]);
|
|
2313
|
+
const atVersion = version;
|
|
2314
|
+
const subscription = {
|
|
2315
|
+
kind: "view",
|
|
2316
|
+
collection,
|
|
2317
|
+
view,
|
|
2318
|
+
incremental,
|
|
2319
|
+
rehydrate,
|
|
2320
|
+
key,
|
|
2321
|
+
onDiff: typedOnDiff
|
|
2322
|
+
};
|
|
2323
|
+
subscribeSet.add(subscription);
|
|
2324
|
+
const unsubscribe = () => {
|
|
2325
|
+
subscribeSet.delete(subscription);
|
|
2326
|
+
};
|
|
2327
|
+
if (resuming) {
|
|
2328
|
+
return wrapReturn({
|
|
2329
|
+
initial: [],
|
|
2330
|
+
catchup: buildCatchup(since, tables, key, boundMatch),
|
|
2331
|
+
cursor: currentCursor(),
|
|
2332
|
+
version: atVersion,
|
|
2333
|
+
unsubscribe
|
|
2334
|
+
});
|
|
2232
2335
|
}
|
|
2233
|
-
}
|
|
2234
|
-
const key = definition.key ?? defaultKey;
|
|
2235
|
-
const match = definition.match;
|
|
2236
|
-
const tables = definition.tables ?? [collection];
|
|
2237
|
-
const scopedTable = tables.length === 1 ? tables[0] : undefined;
|
|
2238
|
-
const readRule = scopedTable !== undefined ? readRuleFor(scopedTable) : undefined;
|
|
2239
|
-
const rehydrate = async () => {
|
|
2240
|
-
const raw = [...await definition.hydrate(params, ctx)];
|
|
2241
|
-
const rows = scopedTable !== undefined ? raw.map((row) => migrateRow(scopedTable, row)) : raw;
|
|
2242
|
-
return readRule ? rows.filter((row) => readRule(ctx, row)) : rows;
|
|
2243
|
-
};
|
|
2244
|
-
const incremental = match !== undefined && tables.length === 1;
|
|
2245
|
-
const boundMatch = incremental ? (row) => match(row, params, ctx) && (readRule ? readRule(ctx, row) : true) : () => true;
|
|
2246
|
-
const view = createMaterializedView({
|
|
2247
|
-
key,
|
|
2248
|
-
match: boundMatch
|
|
2249
|
-
});
|
|
2250
|
-
const resuming = since !== undefined && canResume(since, incremental);
|
|
2251
|
-
view.hydrate([...await rehydrate()]);
|
|
2252
|
-
const atVersion = version;
|
|
2253
|
-
const subscription = {
|
|
2254
|
-
kind: "view",
|
|
2255
|
-
collection,
|
|
2256
|
-
view,
|
|
2257
|
-
incremental,
|
|
2258
|
-
rehydrate,
|
|
2259
|
-
key,
|
|
2260
|
-
onDiff: typedOnDiff
|
|
2261
|
-
};
|
|
2262
|
-
subscribeSet.add(subscription);
|
|
2263
|
-
const unsubscribe = () => {
|
|
2264
|
-
subscribeSet.delete(subscription);
|
|
2265
|
-
};
|
|
2266
|
-
if (resuming) {
|
|
2267
2336
|
return wrapReturn({
|
|
2268
|
-
initial:
|
|
2269
|
-
catchup: buildCatchup(since, tables, key, boundMatch),
|
|
2337
|
+
initial: view.rows(),
|
|
2270
2338
|
cursor: currentCursor(),
|
|
2271
2339
|
version: atVersion,
|
|
2272
2340
|
unsubscribe
|
|
2273
2341
|
});
|
|
2342
|
+
} catch (error) {
|
|
2343
|
+
if (!slotHandedOff)
|
|
2344
|
+
releaseSubscriptionSlot(tenantSlot);
|
|
2345
|
+
throw error;
|
|
2274
2346
|
}
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2347
|
+
} catch (spanError) {
|
|
2348
|
+
subscribeSpan.recordException(spanError);
|
|
2349
|
+
subscribeSpan.setStatus({
|
|
2350
|
+
code: 2,
|
|
2351
|
+
message: spanError instanceof Error ? spanError.message : String(spanError)
|
|
2280
2352
|
});
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
throw error;
|
|
2353
|
+
throw spanError;
|
|
2354
|
+
} finally {
|
|
2355
|
+
subscribeSpan.end();
|
|
2285
2356
|
}
|
|
2286
2357
|
},
|
|
2287
2358
|
hydrate: async (collection, params, ctx, options2) => {
|
|
@@ -2385,85 +2456,102 @@ var createSyncEngine = (options = {}) => {
|
|
|
2385
2456
|
},
|
|
2386
2457
|
migrate: (table, row) => migrateRow(table, row),
|
|
2387
2458
|
runMutation: async (name, args, ctx) => {
|
|
2388
|
-
const
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
if (mutation.authorize !== undefined) {
|
|
2393
|
-
const allowed = await mutation.authorize(args, ctx);
|
|
2394
|
-
if (!allowed) {
|
|
2395
|
-
throw new UnauthorizedError(`run mutation "${name}"`);
|
|
2459
|
+
const span = tracer.startSpan("sync.runMutation", {
|
|
2460
|
+
attributes: {
|
|
2461
|
+
[ABS_ATTRS.engineId]: instanceId,
|
|
2462
|
+
[ABS_ATTRS.mutation]: name
|
|
2396
2463
|
}
|
|
2397
|
-
}
|
|
2398
|
-
await acquireMutationSlot();
|
|
2399
|
-
const sandboxRunner = sandboxRunners.get(name);
|
|
2400
|
-
const invokeHandler = sandboxRunner !== undefined ? sandboxRunner : (a, c, actions) => Promise.resolve(mutation.handler(a, c, actions));
|
|
2401
|
-
const runHandler = async (tx) => {
|
|
2402
|
-
const { actions, buffered } = makeActions(tx, ctx, true);
|
|
2403
|
-
const result = await invokeHandler(args, ctx, actions);
|
|
2404
|
-
return { buffered, result };
|
|
2405
|
-
};
|
|
2406
|
-
const retry = mutation.retry;
|
|
2407
|
-
const maxAttempts = retry === undefined ? 1 : retry.maxAttempts ?? 5;
|
|
2408
|
-
const isRetryable = retry?.isRetryable ?? isSerializationFailure;
|
|
2409
|
-
const computeDelay = retry?.backoff ?? exponentialBackoff();
|
|
2410
|
-
const maxElapsedMs = retry?.maxElapsedMs ?? 30000;
|
|
2411
|
-
const startedAt = Date.now();
|
|
2412
|
-
let lastError;
|
|
2413
|
-
let attemptsMade = 0;
|
|
2464
|
+
});
|
|
2414
2465
|
try {
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2466
|
+
const mutation = mutations.get(name);
|
|
2467
|
+
if (mutation === undefined) {
|
|
2468
|
+
throw new Error(`Unknown mutation "${name}"`);
|
|
2469
|
+
}
|
|
2470
|
+
if (mutation.authorize !== undefined) {
|
|
2471
|
+
const allowed = await mutation.authorize(args, ctx);
|
|
2472
|
+
if (!allowed) {
|
|
2473
|
+
throw new UnauthorizedError(`run mutation "${name}"`);
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
await acquireMutationSlot();
|
|
2477
|
+
const sandboxRunner = sandboxRunners.get(name);
|
|
2478
|
+
const invokeHandler = sandboxRunner !== undefined ? sandboxRunner : (a, c, actions) => Promise.resolve(mutation.handler(a, c, actions));
|
|
2479
|
+
const runHandler = async (tx) => {
|
|
2480
|
+
const { actions, buffered } = makeActions(tx, ctx, true);
|
|
2481
|
+
const result = await invokeHandler(args, ctx, actions);
|
|
2482
|
+
return { buffered, result };
|
|
2483
|
+
};
|
|
2484
|
+
const retry = mutation.retry;
|
|
2485
|
+
const maxAttempts = retry === undefined ? 1 : retry.maxAttempts ?? 5;
|
|
2486
|
+
const isRetryable = retry?.isRetryable ?? isSerializationFailure;
|
|
2487
|
+
const computeDelay = retry?.backoff ?? exponentialBackoff();
|
|
2488
|
+
const maxElapsedMs = retry?.maxElapsedMs ?? 30000;
|
|
2489
|
+
const startedAt = Date.now();
|
|
2490
|
+
let lastError;
|
|
2491
|
+
let attemptsMade = 0;
|
|
2492
|
+
try {
|
|
2493
|
+
for (let attempt = 1;attempt <= maxAttempts; attempt++) {
|
|
2494
|
+
attemptsMade = attempt;
|
|
2495
|
+
try {
|
|
2496
|
+
const { buffered, result } = runInTransaction !== undefined ? await runInTransaction((tx) => runHandler(tx)) : await runHandler(undefined);
|
|
2497
|
+
await applyChangeBatch(buffered);
|
|
2498
|
+
mutationsCompleted += 1;
|
|
2499
|
+
emitActivity({
|
|
2500
|
+
type: "mutation",
|
|
2501
|
+
at: Date.now(),
|
|
2502
|
+
name,
|
|
2503
|
+
status: "ok"
|
|
2504
|
+
});
|
|
2505
|
+
return result;
|
|
2506
|
+
} catch (error) {
|
|
2507
|
+
lastError = error;
|
|
2508
|
+
const elapsedMs = Date.now() - startedAt;
|
|
2509
|
+
const canRetry = attempt < maxAttempts && isRetryable(error) && elapsedMs < maxElapsedMs;
|
|
2510
|
+
if (!canRetry)
|
|
2511
|
+
break;
|
|
2512
|
+
mutationsRetried += 1;
|
|
2513
|
+
const rawDelay = computeDelay(attempt);
|
|
2514
|
+
const remaining = maxElapsedMs - elapsedMs;
|
|
2515
|
+
if (remaining <= 0)
|
|
2516
|
+
break;
|
|
2517
|
+
const delayMs = Math.max(0, Math.min(rawDelay, remaining));
|
|
2518
|
+
emitActivity({
|
|
2519
|
+
type: "mutationRetry",
|
|
2520
|
+
at: Date.now(),
|
|
2521
|
+
name,
|
|
2522
|
+
attempt,
|
|
2523
|
+
delayMs,
|
|
2524
|
+
errorName: error instanceof Error ? error.name : "Error",
|
|
2525
|
+
errorMessage: error instanceof Error ? error.message : String(error)
|
|
2526
|
+
});
|
|
2527
|
+
if (delayMs > 0) {
|
|
2528
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
2529
|
+
}
|
|
2451
2530
|
}
|
|
2452
2531
|
}
|
|
2532
|
+
mutationsFailed += 1;
|
|
2533
|
+
emitActivity({
|
|
2534
|
+
type: "mutation",
|
|
2535
|
+
at: Date.now(),
|
|
2536
|
+
name,
|
|
2537
|
+
status: "error"
|
|
2538
|
+
});
|
|
2539
|
+
if (attemptsMade > 1) {
|
|
2540
|
+
throw new RetriesExhaustedError(attemptsMade, Date.now() - startedAt, lastError);
|
|
2541
|
+
}
|
|
2542
|
+
throw lastError;
|
|
2543
|
+
} finally {
|
|
2544
|
+
releaseMutationSlot();
|
|
2453
2545
|
}
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
status: "error"
|
|
2546
|
+
} catch (spanError) {
|
|
2547
|
+
span.recordException(spanError);
|
|
2548
|
+
span.setStatus({
|
|
2549
|
+
code: 2,
|
|
2550
|
+
message: spanError instanceof Error ? spanError.message : String(spanError)
|
|
2460
2551
|
});
|
|
2461
|
-
|
|
2462
|
-
throw new RetriesExhaustedError(attemptsMade, Date.now() - startedAt, lastError);
|
|
2463
|
-
}
|
|
2464
|
-
throw lastError;
|
|
2552
|
+
throw spanError;
|
|
2465
2553
|
} finally {
|
|
2466
|
-
|
|
2554
|
+
span.end();
|
|
2467
2555
|
}
|
|
2468
2556
|
},
|
|
2469
2557
|
runMutations: async (specs, ctx) => {
|
|
@@ -2713,6 +2801,43 @@ var createSyncEngine = (options = {}) => {
|
|
|
2713
2801
|
version
|
|
2714
2802
|
}),
|
|
2715
2803
|
importChangeLog,
|
|
2804
|
+
replayTo: async ({ at, tables }) => {
|
|
2805
|
+
const filterTables = tables !== undefined ? new Set(tables) : undefined;
|
|
2806
|
+
const state = new Map;
|
|
2807
|
+
let asOfVersion = 0;
|
|
2808
|
+
let asOfAt = 0;
|
|
2809
|
+
const oldest = changeLog[0];
|
|
2810
|
+
const truncated = oldest !== undefined && oldest.version > 1 && oldest.at > at;
|
|
2811
|
+
for (const entry of changeLog) {
|
|
2812
|
+
if (entry.at > at)
|
|
2813
|
+
break;
|
|
2814
|
+
if (filterTables !== undefined && !filterTables.has(entry.table)) {
|
|
2815
|
+
continue;
|
|
2816
|
+
}
|
|
2817
|
+
let tableState = state.get(entry.table);
|
|
2818
|
+
if (tableState === undefined) {
|
|
2819
|
+
tableState = new Map;
|
|
2820
|
+
state.set(entry.table, tableState);
|
|
2821
|
+
}
|
|
2822
|
+
const reader = readers.get(entry.table);
|
|
2823
|
+
const key = reader?.key?.(entry.change.row) ?? entry.change.row?.id;
|
|
2824
|
+
if (key === undefined) {
|
|
2825
|
+
continue;
|
|
2826
|
+
}
|
|
2827
|
+
if (entry.change.op === "delete") {
|
|
2828
|
+
tableState.delete(key);
|
|
2829
|
+
} else {
|
|
2830
|
+
tableState.set(key, entry.change.row);
|
|
2831
|
+
}
|
|
2832
|
+
asOfVersion = entry.version;
|
|
2833
|
+
asOfAt = entry.at;
|
|
2834
|
+
}
|
|
2835
|
+
const rows = {};
|
|
2836
|
+
for (const [table, map] of state) {
|
|
2837
|
+
rows[table] = [...map.values()];
|
|
2838
|
+
}
|
|
2839
|
+
return { asOfAt, asOfVersion, rows, truncated };
|
|
2840
|
+
},
|
|
2716
2841
|
metrics: () => {
|
|
2717
2842
|
const now = Date.now();
|
|
2718
2843
|
const byCollection = {};
|
|
@@ -3130,5 +3255,5 @@ export {
|
|
|
3130
3255
|
createPresenceHub
|
|
3131
3256
|
};
|
|
3132
3257
|
|
|
3133
|
-
//# debugId=
|
|
3258
|
+
//# debugId=6DBA546C7A7B1DC564756E2164756E21
|
|
3134
3259
|
//# sourceMappingURL=index.js.map
|