@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.
@@ -1119,6 +1119,59 @@ class RetriesExhaustedError extends Error {
1119
1119
  this.cause = cause;
1120
1120
  }
1121
1121
  }
1122
+ // node_modules/@absolutejs/telemetry/dist/index.js
1123
+ var NOOP_SPAN_CONTEXT = {
1124
+ spanId: "0000000000000000",
1125
+ traceFlags: 0,
1126
+ traceId: "00000000000000000000000000000000"
1127
+ };
1128
+ var noopSpan = {
1129
+ addEvent: () => noopSpan,
1130
+ end: () => {},
1131
+ isRecording: () => false,
1132
+ recordException: () => {},
1133
+ setAttribute: () => noopSpan,
1134
+ setAttributes: () => noopSpan,
1135
+ setStatus: () => noopSpan,
1136
+ spanContext: () => NOOP_SPAN_CONTEXT,
1137
+ updateName: () => noopSpan
1138
+ };
1139
+ var startActiveSpanNoop = (_name, optionsOrFn, maybeFn) => {
1140
+ const fn = typeof optionsOrFn === "function" ? optionsOrFn : maybeFn;
1141
+ return fn(noopSpan);
1142
+ };
1143
+ var noopTracer = {
1144
+ startActiveSpan: startActiveSpanNoop,
1145
+ startSpan: () => noopSpan
1146
+ };
1147
+ var tracerOrNoop = (provider, name, version) => provider !== undefined ? provider.getTracer(name, version) : noopTracer;
1148
+ var ABS_ATTRS = {
1149
+ tenant: "abs.tenant",
1150
+ shardId: "abs.shard.id",
1151
+ engineId: "abs.engine.id",
1152
+ collection: "abs.collection",
1153
+ mutation: "abs.mutation",
1154
+ mutationAttempt: "abs.mutation.attempt",
1155
+ subscriptionId: "abs.subscription.id",
1156
+ batchSize: "abs.batch.size",
1157
+ clusterMessageOrigin: "abs.cluster.origin",
1158
+ jobId: "abs.job.id",
1159
+ jobKind: "abs.job.kind",
1160
+ jobAttempt: "abs.job.attempt",
1161
+ jobMaxAttempts: "abs.job.max_attempts",
1162
+ workerId: "abs.worker.id",
1163
+ runtimeKey: "abs.runtime.key",
1164
+ runtimePid: "abs.runtime.pid",
1165
+ runtimePort: "abs.runtime.port",
1166
+ runtimeExitReason: "abs.runtime.exit_reason",
1167
+ runtimeReadinessMs: "abs.runtime.readiness_ms",
1168
+ routeShard: "abs.route.shard",
1169
+ routeDecision: "abs.route.decision",
1170
+ secretName: "abs.secret.name",
1171
+ secretFingerprint: "abs.secret.fingerprint",
1172
+ auditKind: "abs.audit.kind"
1173
+ };
1174
+
1122
1175
  // src/engine/sandbox.ts
1123
1176
  var isolatedJscModule;
1124
1177
  var loadIsolatedJsc = async () => {
@@ -1648,6 +1701,7 @@ var createSyncEngine = (options = {}) => {
1648
1701
  const runInTransaction = options.transaction;
1649
1702
  const instanceId = options.instanceId ?? globalThis.crypto?.randomUUID?.() ?? `i${Math.random()}`;
1650
1703
  let clusterBus;
1704
+ const tracer = tracerOrNoop(options.tracerProvider, "@absolutejs/sync");
1651
1705
  const importChangeLog = (snapshot) => {
1652
1706
  if (version !== 0) {
1653
1707
  throw new Error(`[sync] importChangeLog: engine already has version ${version}; ` + `restore must happen before any local writes commit.`);
@@ -2439,107 +2493,124 @@ var createSyncEngine = (options = {}) => {
2439
2493
  registry.set(collection.name, collection);
2440
2494
  },
2441
2495
  subscribe: async ({ collection, params, ctx, onDiff, since, signal }) => {
2442
- checkAborted(signal);
2443
- const registered = registry.get(collection);
2444
- if (registered === undefined) {
2445
- throw new Error(`Unknown collection "${collection}"`);
2446
- }
2447
- const tenantSlot = acquireSubscriptionSlot(ctx, { collection });
2448
- let slotHandedOff = false;
2496
+ const subscribeSpan = tracer.startSpan("sync.subscribe", {
2497
+ attributes: {
2498
+ [ABS_ATTRS.engineId]: instanceId,
2499
+ [ABS_ATTRS.collection]: collection
2500
+ }
2501
+ });
2449
2502
  try {
2450
- const typedOnDiff = onDiff;
2451
- const subscribeSet = subsFor(collection);
2452
- const wrapReturn = (sub) => {
2453
- checkAborted(signal);
2454
- const innerUnsubscribe = sub.unsubscribe;
2455
- let released = false;
2456
- const wrappedUnsubscribe = () => {
2457
- if (released)
2458
- return;
2459
- released = true;
2460
- releaseSubscriptionSlot(tenantSlot);
2461
- innerUnsubscribe();
2503
+ checkAborted(signal);
2504
+ const registered = registry.get(collection);
2505
+ if (registered === undefined) {
2506
+ throw new Error(`Unknown collection "${collection}"`);
2507
+ }
2508
+ const tenantSlot = acquireSubscriptionSlot(ctx, { collection });
2509
+ let slotHandedOff = false;
2510
+ try {
2511
+ const typedOnDiff = onDiff;
2512
+ const subscribeSet = subsFor(collection);
2513
+ const wrapReturn = (sub) => {
2514
+ checkAborted(signal);
2515
+ const innerUnsubscribe = sub.unsubscribe;
2516
+ let released = false;
2517
+ const wrappedUnsubscribe = () => {
2518
+ if (released)
2519
+ return;
2520
+ released = true;
2521
+ releaseSubscriptionSlot(tenantSlot);
2522
+ innerUnsubscribe();
2523
+ };
2524
+ const wrapped = { ...sub, unsubscribe: wrappedUnsubscribe };
2525
+ linkAbortToUnsubscribe(signal, wrappedUnsubscribe);
2526
+ slotHandedOff = true;
2527
+ return wrapped;
2462
2528
  };
2463
- const wrapped = { ...sub, unsubscribe: wrappedUnsubscribe };
2464
- linkAbortToUnsubscribe(signal, wrappedUnsubscribe);
2465
- slotHandedOff = true;
2466
- return wrapped;
2467
- };
2468
- const registeredKind = registered.kind;
2469
- if (registeredKind === "join") {
2470
- const joined = await subscribeJoin(collection, registered, params, ctx, typedOnDiff, subscribeSet);
2471
- return wrapReturn(joined);
2472
- }
2473
- if (registeredKind === "graph") {
2474
- const graphed = await subscribeGraph(collection, registered, params, ctx, typedOnDiff, subscribeSet);
2475
- return wrapReturn(graphed);
2476
- }
2477
- if (registeredKind === "reactive") {
2478
- const reactived = await subscribeReactive(collection, registered, params, ctx, typedOnDiff, subscribeSet);
2479
- return wrapReturn(reactived);
2480
- }
2481
- if (registeredKind === "search") {
2482
- const searched = await subscribeSearch(collection, registered, params, ctx, typedOnDiff, subscribeSet);
2483
- return wrapReturn(searched);
2484
- }
2485
- const definition = registered;
2486
- if (definition.authorize !== undefined) {
2487
- const allowed = await definition.authorize(params, ctx);
2488
- if (!allowed) {
2489
- throw new UnauthorizedError(`subscribe to collection "${collection}"`);
2529
+ const registeredKind = registered.kind;
2530
+ if (registeredKind === "join") {
2531
+ const joined = await subscribeJoin(collection, registered, params, ctx, typedOnDiff, subscribeSet);
2532
+ return wrapReturn(joined);
2533
+ }
2534
+ if (registeredKind === "graph") {
2535
+ const graphed = await subscribeGraph(collection, registered, params, ctx, typedOnDiff, subscribeSet);
2536
+ return wrapReturn(graphed);
2537
+ }
2538
+ if (registeredKind === "reactive") {
2539
+ const reactived = await subscribeReactive(collection, registered, params, ctx, typedOnDiff, subscribeSet);
2540
+ return wrapReturn(reactived);
2541
+ }
2542
+ if (registeredKind === "search") {
2543
+ const searched = await subscribeSearch(collection, registered, params, ctx, typedOnDiff, subscribeSet);
2544
+ return wrapReturn(searched);
2545
+ }
2546
+ const definition = registered;
2547
+ if (definition.authorize !== undefined) {
2548
+ const allowed = await definition.authorize(params, ctx);
2549
+ if (!allowed) {
2550
+ throw new UnauthorizedError(`subscribe to collection "${collection}"`);
2551
+ }
2552
+ }
2553
+ const key = definition.key ?? defaultKey;
2554
+ const match = definition.match;
2555
+ const tables = definition.tables ?? [collection];
2556
+ const scopedTable = tables.length === 1 ? tables[0] : undefined;
2557
+ const readRule = scopedTable !== undefined ? readRuleFor(scopedTable) : undefined;
2558
+ const rehydrate = async () => {
2559
+ const raw = [...await definition.hydrate(params, ctx)];
2560
+ const rows = scopedTable !== undefined ? raw.map((row) => migrateRow(scopedTable, row)) : raw;
2561
+ return readRule ? rows.filter((row) => readRule(ctx, row)) : rows;
2562
+ };
2563
+ const incremental = match !== undefined && tables.length === 1;
2564
+ const boundMatch = incremental ? (row) => match(row, params, ctx) && (readRule ? readRule(ctx, row) : true) : () => true;
2565
+ const view = createMaterializedView({
2566
+ key,
2567
+ match: boundMatch
2568
+ });
2569
+ const resuming = since !== undefined && canResume(since, incremental);
2570
+ view.hydrate([...await rehydrate()]);
2571
+ const atVersion = version;
2572
+ const subscription = {
2573
+ kind: "view",
2574
+ collection,
2575
+ view,
2576
+ incremental,
2577
+ rehydrate,
2578
+ key,
2579
+ onDiff: typedOnDiff
2580
+ };
2581
+ subscribeSet.add(subscription);
2582
+ const unsubscribe = () => {
2583
+ subscribeSet.delete(subscription);
2584
+ };
2585
+ if (resuming) {
2586
+ return wrapReturn({
2587
+ initial: [],
2588
+ catchup: buildCatchup(since, tables, key, boundMatch),
2589
+ cursor: currentCursor(),
2590
+ version: atVersion,
2591
+ unsubscribe
2592
+ });
2490
2593
  }
2491
- }
2492
- const key = definition.key ?? defaultKey;
2493
- const match = definition.match;
2494
- const tables = definition.tables ?? [collection];
2495
- const scopedTable = tables.length === 1 ? tables[0] : undefined;
2496
- const readRule = scopedTable !== undefined ? readRuleFor(scopedTable) : undefined;
2497
- const rehydrate = async () => {
2498
- const raw = [...await definition.hydrate(params, ctx)];
2499
- const rows = scopedTable !== undefined ? raw.map((row) => migrateRow(scopedTable, row)) : raw;
2500
- return readRule ? rows.filter((row) => readRule(ctx, row)) : rows;
2501
- };
2502
- const incremental = match !== undefined && tables.length === 1;
2503
- const boundMatch = incremental ? (row) => match(row, params, ctx) && (readRule ? readRule(ctx, row) : true) : () => true;
2504
- const view = createMaterializedView({
2505
- key,
2506
- match: boundMatch
2507
- });
2508
- const resuming = since !== undefined && canResume(since, incremental);
2509
- view.hydrate([...await rehydrate()]);
2510
- const atVersion = version;
2511
- const subscription = {
2512
- kind: "view",
2513
- collection,
2514
- view,
2515
- incremental,
2516
- rehydrate,
2517
- key,
2518
- onDiff: typedOnDiff
2519
- };
2520
- subscribeSet.add(subscription);
2521
- const unsubscribe = () => {
2522
- subscribeSet.delete(subscription);
2523
- };
2524
- if (resuming) {
2525
2594
  return wrapReturn({
2526
- initial: [],
2527
- catchup: buildCatchup(since, tables, key, boundMatch),
2595
+ initial: view.rows(),
2528
2596
  cursor: currentCursor(),
2529
2597
  version: atVersion,
2530
2598
  unsubscribe
2531
2599
  });
2600
+ } catch (error) {
2601
+ if (!slotHandedOff)
2602
+ releaseSubscriptionSlot(tenantSlot);
2603
+ throw error;
2532
2604
  }
2533
- return wrapReturn({
2534
- initial: view.rows(),
2535
- cursor: currentCursor(),
2536
- version: atVersion,
2537
- unsubscribe
2605
+ } catch (spanError) {
2606
+ subscribeSpan.recordException(spanError);
2607
+ subscribeSpan.setStatus({
2608
+ code: 2,
2609
+ message: spanError instanceof Error ? spanError.message : String(spanError)
2538
2610
  });
2539
- } catch (error) {
2540
- if (!slotHandedOff)
2541
- releaseSubscriptionSlot(tenantSlot);
2542
- throw error;
2611
+ throw spanError;
2612
+ } finally {
2613
+ subscribeSpan.end();
2543
2614
  }
2544
2615
  },
2545
2616
  hydrate: async (collection, params, ctx, options2) => {
@@ -2643,85 +2714,102 @@ var createSyncEngine = (options = {}) => {
2643
2714
  },
2644
2715
  migrate: (table, row) => migrateRow(table, row),
2645
2716
  runMutation: async (name, args, ctx) => {
2646
- const mutation = mutations.get(name);
2647
- if (mutation === undefined) {
2648
- throw new Error(`Unknown mutation "${name}"`);
2649
- }
2650
- if (mutation.authorize !== undefined) {
2651
- const allowed = await mutation.authorize(args, ctx);
2652
- if (!allowed) {
2653
- throw new UnauthorizedError(`run mutation "${name}"`);
2717
+ const span = tracer.startSpan("sync.runMutation", {
2718
+ attributes: {
2719
+ [ABS_ATTRS.engineId]: instanceId,
2720
+ [ABS_ATTRS.mutation]: name
2654
2721
  }
2655
- }
2656
- await acquireMutationSlot();
2657
- const sandboxRunner = sandboxRunners.get(name);
2658
- const invokeHandler = sandboxRunner !== undefined ? sandboxRunner : (a, c, actions) => Promise.resolve(mutation.handler(a, c, actions));
2659
- const runHandler = async (tx) => {
2660
- const { actions, buffered } = makeActions(tx, ctx, true);
2661
- const result = await invokeHandler(args, ctx, actions);
2662
- return { buffered, result };
2663
- };
2664
- const retry = mutation.retry;
2665
- const maxAttempts = retry === undefined ? 1 : retry.maxAttempts ?? 5;
2666
- const isRetryable = retry?.isRetryable ?? isSerializationFailure;
2667
- const computeDelay = retry?.backoff ?? exponentialBackoff();
2668
- const maxElapsedMs = retry?.maxElapsedMs ?? 30000;
2669
- const startedAt = Date.now();
2670
- let lastError;
2671
- let attemptsMade = 0;
2722
+ });
2672
2723
  try {
2673
- for (let attempt = 1;attempt <= maxAttempts; attempt++) {
2674
- attemptsMade = attempt;
2675
- try {
2676
- const { buffered, result } = runInTransaction !== undefined ? await runInTransaction((tx) => runHandler(tx)) : await runHandler(undefined);
2677
- await applyChangeBatch(buffered);
2678
- mutationsCompleted += 1;
2679
- emitActivity({
2680
- type: "mutation",
2681
- at: Date.now(),
2682
- name,
2683
- status: "ok"
2684
- });
2685
- return result;
2686
- } catch (error) {
2687
- lastError = error;
2688
- const elapsedMs = Date.now() - startedAt;
2689
- const canRetry = attempt < maxAttempts && isRetryable(error) && elapsedMs < maxElapsedMs;
2690
- if (!canRetry)
2691
- break;
2692
- mutationsRetried += 1;
2693
- const rawDelay = computeDelay(attempt);
2694
- const remaining = maxElapsedMs - elapsedMs;
2695
- if (remaining <= 0)
2696
- break;
2697
- const delayMs = Math.max(0, Math.min(rawDelay, remaining));
2698
- emitActivity({
2699
- type: "mutationRetry",
2700
- at: Date.now(),
2701
- name,
2702
- attempt,
2703
- delayMs,
2704
- errorName: error instanceof Error ? error.name : "Error",
2705
- errorMessage: error instanceof Error ? error.message : String(error)
2706
- });
2707
- if (delayMs > 0) {
2708
- await new Promise((resolve) => setTimeout(resolve, delayMs));
2724
+ const mutation = mutations.get(name);
2725
+ if (mutation === undefined) {
2726
+ throw new Error(`Unknown mutation "${name}"`);
2727
+ }
2728
+ if (mutation.authorize !== undefined) {
2729
+ const allowed = await mutation.authorize(args, ctx);
2730
+ if (!allowed) {
2731
+ throw new UnauthorizedError(`run mutation "${name}"`);
2732
+ }
2733
+ }
2734
+ await acquireMutationSlot();
2735
+ const sandboxRunner = sandboxRunners.get(name);
2736
+ const invokeHandler = sandboxRunner !== undefined ? sandboxRunner : (a, c, actions) => Promise.resolve(mutation.handler(a, c, actions));
2737
+ const runHandler = async (tx) => {
2738
+ const { actions, buffered } = makeActions(tx, ctx, true);
2739
+ const result = await invokeHandler(args, ctx, actions);
2740
+ return { buffered, result };
2741
+ };
2742
+ const retry = mutation.retry;
2743
+ const maxAttempts = retry === undefined ? 1 : retry.maxAttempts ?? 5;
2744
+ const isRetryable = retry?.isRetryable ?? isSerializationFailure;
2745
+ const computeDelay = retry?.backoff ?? exponentialBackoff();
2746
+ const maxElapsedMs = retry?.maxElapsedMs ?? 30000;
2747
+ const startedAt = Date.now();
2748
+ let lastError;
2749
+ let attemptsMade = 0;
2750
+ try {
2751
+ for (let attempt = 1;attempt <= maxAttempts; attempt++) {
2752
+ attemptsMade = attempt;
2753
+ try {
2754
+ const { buffered, result } = runInTransaction !== undefined ? await runInTransaction((tx) => runHandler(tx)) : await runHandler(undefined);
2755
+ await applyChangeBatch(buffered);
2756
+ mutationsCompleted += 1;
2757
+ emitActivity({
2758
+ type: "mutation",
2759
+ at: Date.now(),
2760
+ name,
2761
+ status: "ok"
2762
+ });
2763
+ return result;
2764
+ } catch (error) {
2765
+ lastError = error;
2766
+ const elapsedMs = Date.now() - startedAt;
2767
+ const canRetry = attempt < maxAttempts && isRetryable(error) && elapsedMs < maxElapsedMs;
2768
+ if (!canRetry)
2769
+ break;
2770
+ mutationsRetried += 1;
2771
+ const rawDelay = computeDelay(attempt);
2772
+ const remaining = maxElapsedMs - elapsedMs;
2773
+ if (remaining <= 0)
2774
+ break;
2775
+ const delayMs = Math.max(0, Math.min(rawDelay, remaining));
2776
+ emitActivity({
2777
+ type: "mutationRetry",
2778
+ at: Date.now(),
2779
+ name,
2780
+ attempt,
2781
+ delayMs,
2782
+ errorName: error instanceof Error ? error.name : "Error",
2783
+ errorMessage: error instanceof Error ? error.message : String(error)
2784
+ });
2785
+ if (delayMs > 0) {
2786
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
2787
+ }
2709
2788
  }
2710
2789
  }
2790
+ mutationsFailed += 1;
2791
+ emitActivity({
2792
+ type: "mutation",
2793
+ at: Date.now(),
2794
+ name,
2795
+ status: "error"
2796
+ });
2797
+ if (attemptsMade > 1) {
2798
+ throw new RetriesExhaustedError(attemptsMade, Date.now() - startedAt, lastError);
2799
+ }
2800
+ throw lastError;
2801
+ } finally {
2802
+ releaseMutationSlot();
2711
2803
  }
2712
- mutationsFailed += 1;
2713
- emitActivity({
2714
- type: "mutation",
2715
- at: Date.now(),
2716
- name,
2717
- status: "error"
2804
+ } catch (spanError) {
2805
+ span.recordException(spanError);
2806
+ span.setStatus({
2807
+ code: 2,
2808
+ message: spanError instanceof Error ? spanError.message : String(spanError)
2718
2809
  });
2719
- if (attemptsMade > 1) {
2720
- throw new RetriesExhaustedError(attemptsMade, Date.now() - startedAt, lastError);
2721
- }
2722
- throw lastError;
2810
+ throw spanError;
2723
2811
  } finally {
2724
- releaseMutationSlot();
2812
+ span.end();
2725
2813
  }
2726
2814
  },
2727
2815
  runMutations: async (specs, ctx) => {
@@ -2971,6 +3059,43 @@ var createSyncEngine = (options = {}) => {
2971
3059
  version
2972
3060
  }),
2973
3061
  importChangeLog,
3062
+ replayTo: async ({ at, tables }) => {
3063
+ const filterTables = tables !== undefined ? new Set(tables) : undefined;
3064
+ const state = new Map;
3065
+ let asOfVersion = 0;
3066
+ let asOfAt = 0;
3067
+ const oldest = changeLog[0];
3068
+ const truncated = oldest !== undefined && oldest.version > 1 && oldest.at > at;
3069
+ for (const entry of changeLog) {
3070
+ if (entry.at > at)
3071
+ break;
3072
+ if (filterTables !== undefined && !filterTables.has(entry.table)) {
3073
+ continue;
3074
+ }
3075
+ let tableState = state.get(entry.table);
3076
+ if (tableState === undefined) {
3077
+ tableState = new Map;
3078
+ state.set(entry.table, tableState);
3079
+ }
3080
+ const reader = readers.get(entry.table);
3081
+ const key = reader?.key?.(entry.change.row) ?? entry.change.row?.id;
3082
+ if (key === undefined) {
3083
+ continue;
3084
+ }
3085
+ if (entry.change.op === "delete") {
3086
+ tableState.delete(key);
3087
+ } else {
3088
+ tableState.set(key, entry.change.row);
3089
+ }
3090
+ asOfVersion = entry.version;
3091
+ asOfAt = entry.at;
3092
+ }
3093
+ const rows = {};
3094
+ for (const [table, map] of state) {
3095
+ rows[table] = [...map.values()];
3096
+ }
3097
+ return { asOfAt, asOfVersion, rows, truncated };
3098
+ },
2974
3099
  metrics: () => {
2975
3100
  const now = Date.now();
2976
3101
  const byCollection = {};
@@ -3545,5 +3670,5 @@ export {
3545
3670
  CdcConsumerSlowError
3546
3671
  };
3547
3672
 
3548
- //# debugId=251EC576E67A0CAF64756E2164756E21
3673
+ //# debugId=04E0C6606054D14564756E2164756E21
3549
3674
  //# sourceMappingURL=index.js.map