@absolutejs/sync 1.20.1 → 1.21.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.
@@ -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';
@@ -633,6 +634,27 @@ export type SyncEngineOptions = {
633
634
  collection: string;
634
635
  }) => string | undefined;
635
636
  };
637
+ /**
638
+ * Optional OpenTelemetry tracer provider. When set, the engine
639
+ * wraps `subscribe`, `runMutation`, `runMutations`, and cluster
640
+ * fan-out in spans named `sync.<op>` with `ABS_ATTRS` semantic
641
+ * conventions (`abs.engine.id`, `abs.collection`, `abs.mutation`,
642
+ * etc.). When absent, all tracing is a zero-allocation noop —
643
+ * existing call sites pay nothing. Added in 1.21.0.
644
+ *
645
+ * Pass any `@opentelemetry/api`-compatible `TracerProvider`. See
646
+ * `@absolutejs/telemetry` for the type shape — sync re-uses its
647
+ * helpers but doesn't peer-dep `@opentelemetry/api` directly.
648
+ *
649
+ * @example
650
+ * ```ts
651
+ * import { NodeTracerProvider } from '@opentelemetry/sdk-node';
652
+ * const tp = new NodeTracerProvider({ ... });
653
+ * tp.register();
654
+ * const engine = createSyncEngine({ tracerProvider: tp });
655
+ * ```
656
+ */
657
+ tracerProvider?: TelemetryTracerProvider;
636
658
  };
637
659
  /**
638
660
  * 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
- checkAborted(signal);
2185
- const registered = registry.get(collection);
2186
- if (registered === undefined) {
2187
- throw new Error(`Unknown collection "${collection}"`);
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);
2214
- }
2215
- if (registeredKind === "graph") {
2216
- const graphed = await subscribeGraph(collection, registered, params, ctx, typedOnDiff, subscribeSet);
2217
- return wrapReturn(graphed);
2238
+ const subscribeSpan = tracer.startSpan("sync.subscribe", {
2239
+ attributes: {
2240
+ [ABS_ATTRS.engineId]: instanceId,
2241
+ [ABS_ATTRS.collection]: collection
2218
2242
  }
2219
- if (registeredKind === "reactive") {
2220
- const reactived = await subscribeReactive(collection, registered, params, ctx, typedOnDiff, subscribeSet);
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 definition = registered;
2228
- if (definition.authorize !== undefined) {
2229
- const allowed = await definition.authorize(params, ctx);
2230
- if (!allowed) {
2231
- throw new UnauthorizedError(`subscribe to collection "${collection}"`);
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
- return wrapReturn({
2276
- initial: view.rows(),
2277
- cursor: currentCursor(),
2278
- version: atVersion,
2279
- unsubscribe
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
- } catch (error) {
2282
- if (!slotHandedOff)
2283
- releaseSubscriptionSlot(tenantSlot);
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 mutation = mutations.get(name);
2389
- if (mutation === undefined) {
2390
- throw new Error(`Unknown mutation "${name}"`);
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
- for (let attempt = 1;attempt <= maxAttempts; attempt++) {
2416
- attemptsMade = attempt;
2417
- try {
2418
- const { buffered, result } = runInTransaction !== undefined ? await runInTransaction((tx) => runHandler(tx)) : await runHandler(undefined);
2419
- await applyChangeBatch(buffered);
2420
- mutationsCompleted += 1;
2421
- emitActivity({
2422
- type: "mutation",
2423
- at: Date.now(),
2424
- name,
2425
- status: "ok"
2426
- });
2427
- return result;
2428
- } catch (error) {
2429
- lastError = error;
2430
- const elapsedMs = Date.now() - startedAt;
2431
- const canRetry = attempt < maxAttempts && isRetryable(error) && elapsedMs < maxElapsedMs;
2432
- if (!canRetry)
2433
- break;
2434
- mutationsRetried += 1;
2435
- const rawDelay = computeDelay(attempt);
2436
- const remaining = maxElapsedMs - elapsedMs;
2437
- if (remaining <= 0)
2438
- break;
2439
- const delayMs = Math.max(0, Math.min(rawDelay, remaining));
2440
- emitActivity({
2441
- type: "mutationRetry",
2442
- at: Date.now(),
2443
- name,
2444
- attempt,
2445
- delayMs,
2446
- errorName: error instanceof Error ? error.name : "Error",
2447
- errorMessage: error instanceof Error ? error.message : String(error)
2448
- });
2449
- if (delayMs > 0) {
2450
- await new Promise((resolve) => setTimeout(resolve, delayMs));
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
- mutationsFailed += 1;
2455
- emitActivity({
2456
- type: "mutation",
2457
- at: Date.now(),
2458
- name,
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
- if (attemptsMade > 1) {
2462
- throw new RetriesExhaustedError(attemptsMade, Date.now() - startedAt, lastError);
2463
- }
2464
- throw lastError;
2552
+ throw spanError;
2465
2553
  } finally {
2466
- releaseMutationSlot();
2554
+ span.end();
2467
2555
  }
2468
2556
  },
2469
2557
  runMutations: async (specs, ctx) => {
@@ -3130,5 +3218,5 @@ export {
3130
3218
  createPresenceHub
3131
3219
  };
3132
3220
 
3133
- //# debugId=27A5DE0DF43D569D64756E2164756E21
3221
+ //# debugId=06ECBF0C02EA0B8464756E2164756E21
3134
3222
  //# sourceMappingURL=index.js.map