@absolutejs/sync 1.8.1 → 1.9.1

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.
@@ -20,7 +20,7 @@
20
20
  * `error` SSE event so the client knows to resubscribe (vs silently
21
21
  * dropping commits).
22
22
  */
23
- import { Elysia } from 'elysia';
23
+ import type { Elysia as ElysiaType } from 'elysia';
24
24
  import { type SyncEngine } from './syncEngine';
25
25
  export type SyncCdcOptions = {
26
26
  /** The engine whose change log this route streams. */
@@ -32,7 +32,7 @@ export type SyncCdcOptions = {
32
32
  /** Per-stream in-flight buffer cap. Passed to {@link SyncEngine.streamChanges}. Default 10000. */
33
33
  maxBuffer?: number;
34
34
  };
35
- export declare const syncCdc: ({ engine, path, heartbeatMs, maxBuffer }: SyncCdcOptions) => Elysia<"", {
35
+ export declare const syncCdc: ({ engine, path, heartbeatMs, maxBuffer }: SyncCdcOptions) => ElysiaType<"", {
36
36
  decorator: {};
37
37
  store: {};
38
38
  derive: {};
@@ -35,6 +35,18 @@ export type EngineInspection = {
35
35
  table: string;
36
36
  op: RowOp;
37
37
  }[];
38
+ /**
39
+ * Registered sync packs (see {@link SyncEngine.registerPack}). Each
40
+ * entry reports the pack's name, version, the tables it owns, and the
41
+ * tables it reads but does not own. Surfaced for devtools and for
42
+ * conflict diagnostics.
43
+ */
44
+ packs: {
45
+ name: string;
46
+ version: string;
47
+ ownsTables: string[];
48
+ readsTables: string[];
49
+ }[];
38
50
  };
39
51
  /**
40
52
  * A live engine event (see {@link SyncEngine.onActivity}): a committed change or
@@ -63,4 +75,19 @@ export type EngineActivity = {
63
75
  delayMs: number;
64
76
  errorName: string;
65
77
  errorMessage: string;
78
+ } | {
79
+ type: 'schedule';
80
+ at: number;
81
+ name: string;
82
+ status: 'ok' | 'error';
83
+ } | {
84
+ /** Emitted between attempts of a retried schedule. Mirrors
85
+ * {@link mutationRetry}. */
86
+ type: 'scheduleRetry';
87
+ at: number;
88
+ name: string;
89
+ attempt: number;
90
+ delayMs: number;
91
+ errorName: string;
92
+ errorMessage: string;
66
93
  };
@@ -40,6 +40,8 @@ export { createVectorIndex } from './vectorIndex';
40
40
  export type { VectorIndexOptions, VectorMetric } from './vectorIndex';
41
41
  export { defineSchedule } from './schedule';
42
42
  export type { ScheduleContext, ScheduleDefinition } from './schedule';
43
+ export { defineSyncPack, PackMissingDependencyError, PackTableConflictError } from './pack';
44
+ export type { CrdtFieldsMap, RegisteredPack, SyncPack } from './pack';
43
45
  export { defineMutation } from './mutation';
44
46
  export type { MutationActions, MutationDefinition, MutationHandler, TableWriter, TransactionRunner } from './mutation';
45
47
  export type { BridgeFetchConfig, BridgeFetchEndpoint, BridgeFetchResponse, HandlerMetricsHook, HandlerMetricsRecord, SandboxConfig } from './sandbox';
@@ -1056,6 +1056,31 @@ var createVectorIndex = (options) => {
1056
1056
  };
1057
1057
  // src/engine/schedule.ts
1058
1058
  var defineSchedule = (definition) => definition;
1059
+ // src/engine/pack.ts
1060
+ class PackTableConflictError extends Error {
1061
+ table;
1062
+ existingPack;
1063
+ newPack;
1064
+ constructor(table, existingPack, newPack) {
1065
+ super(`Pack "${newPack}" claims table "${table}", but "${existingPack}" already owns it. Use a tablePrefix on one of them.`);
1066
+ this.name = "PackTableConflictError";
1067
+ this.table = table;
1068
+ this.existingPack = existingPack;
1069
+ this.newPack = newPack;
1070
+ }
1071
+ }
1072
+
1073
+ class PackMissingDependencyError extends Error {
1074
+ pack;
1075
+ missingTable;
1076
+ constructor(pack, missingTable) {
1077
+ super(`Pack "${pack}" requires a reader for table "${missingTable}" but none is registered. Call engine.registerReader("${missingTable}", ...) before engine.registerPack.`);
1078
+ this.name = "PackMissingDependencyError";
1079
+ this.pack = pack;
1080
+ this.missingTable = missingTable;
1081
+ }
1082
+ }
1083
+ var defineSyncPack = (pack) => pack;
1059
1084
  // src/engine/mutation.ts
1060
1085
  var defineMutation = (definition) => definition;
1061
1086
  // src/engine/retry.ts
@@ -1405,6 +1430,8 @@ var createSyncEngine = (options = {}) => {
1405
1430
  const writers = new Map;
1406
1431
  const readers = new Map;
1407
1432
  const schedules = new Map;
1433
+ const packTableOwners = new Map;
1434
+ const registeredPacks = [];
1408
1435
  const permissions = new Map;
1409
1436
  for (const [table, rules] of Object.entries(options.permissions ?? {})) {
1410
1437
  permissions.set(table, rules);
@@ -2136,7 +2163,7 @@ var createSyncEngine = (options = {}) => {
2136
2163
  }
2137
2164
  };
2138
2165
  };
2139
- return {
2166
+ const engine = {
2140
2167
  register: (collection) => {
2141
2168
  registry.set(collection.name, collection);
2142
2169
  for (const table of collection.tables ?? [collection.name]) {
@@ -2411,13 +2438,136 @@ var createSyncEngine = (options = {}) => {
2411
2438
  throw new Error(`Unknown schedule "${name}"`);
2412
2439
  }
2413
2440
  const runHandler = async (tx) => {
2414
- const { actions, buffered: buffered2 } = makeActions(tx, {}, false);
2441
+ const { actions, buffered } = makeActions(tx, {}, false);
2415
2442
  const db = makeReadHandle({}, new Set, new Set, [], false);
2416
2443
  await schedule.run({ actions, db });
2417
- return buffered2;
2444
+ return buffered;
2418
2445
  };
2419
- const buffered = runInTransaction !== undefined ? await runInTransaction((tx) => runHandler(tx)) : await runHandler(undefined);
2420
- await applyChangeBatch(buffered);
2446
+ const retry = schedule.retry;
2447
+ const maxAttempts = retry === undefined ? 1 : retry.maxAttempts ?? 5;
2448
+ const isRetryable = retry?.isRetryable ?? isSerializationFailure;
2449
+ const computeDelay = retry?.backoff ?? exponentialBackoff();
2450
+ const maxElapsedMs = retry?.maxElapsedMs ?? 30000;
2451
+ const startedAt = Date.now();
2452
+ let lastError;
2453
+ let attemptsMade = 0;
2454
+ for (let attempt = 1;attempt <= maxAttempts; attempt++) {
2455
+ attemptsMade = attempt;
2456
+ try {
2457
+ const buffered = runInTransaction !== undefined ? await runInTransaction((tx) => runHandler(tx)) : await runHandler(undefined);
2458
+ await applyChangeBatch(buffered);
2459
+ emitActivity({
2460
+ type: "schedule",
2461
+ at: Date.now(),
2462
+ name,
2463
+ status: "ok"
2464
+ });
2465
+ return;
2466
+ } catch (error) {
2467
+ lastError = error;
2468
+ const elapsedMs = Date.now() - startedAt;
2469
+ const canRetry = attempt < maxAttempts && isRetryable(error) && elapsedMs < maxElapsedMs;
2470
+ if (!canRetry)
2471
+ break;
2472
+ const rawDelay = computeDelay(attempt);
2473
+ const remaining = maxElapsedMs - elapsedMs;
2474
+ if (remaining <= 0)
2475
+ break;
2476
+ const delayMs = Math.max(0, Math.min(rawDelay, remaining));
2477
+ emitActivity({
2478
+ type: "scheduleRetry",
2479
+ at: Date.now(),
2480
+ name,
2481
+ attempt,
2482
+ delayMs,
2483
+ errorName: error instanceof Error ? error.name : "Error",
2484
+ errorMessage: error instanceof Error ? error.message : String(error)
2485
+ });
2486
+ if (delayMs > 0) {
2487
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
2488
+ }
2489
+ }
2490
+ }
2491
+ emitActivity({
2492
+ type: "schedule",
2493
+ at: Date.now(),
2494
+ name,
2495
+ status: "error"
2496
+ });
2497
+ if (attemptsMade > 1) {
2498
+ throw new RetriesExhaustedError(attemptsMade, Date.now() - startedAt, lastError);
2499
+ }
2500
+ throw lastError;
2501
+ },
2502
+ registerPack: (pack) => {
2503
+ for (const table of pack.ownsTables) {
2504
+ const existing = packTableOwners.get(table);
2505
+ if (existing !== undefined) {
2506
+ throw new PackTableConflictError(table, existing, pack.name);
2507
+ }
2508
+ }
2509
+ if (pack.requireDependencies === true) {
2510
+ for (const table of pack.readsTables ?? []) {
2511
+ if (!readers.has(table)) {
2512
+ throw new PackMissingDependencyError(pack.name, table);
2513
+ }
2514
+ }
2515
+ }
2516
+ if (pack.schemas !== undefined) {
2517
+ for (const [table, schema] of Object.entries(pack.schemas)) {
2518
+ engine.registerSchema(table, schema);
2519
+ }
2520
+ }
2521
+ if (pack.permissions !== undefined) {
2522
+ for (const [table, rules] of Object.entries(pack.permissions)) {
2523
+ engine.registerPermissions(table, rules);
2524
+ }
2525
+ }
2526
+ if (pack.readers !== undefined) {
2527
+ for (const [table, reader] of Object.entries(pack.readers)) {
2528
+ engine.registerReader(table, reader);
2529
+ }
2530
+ }
2531
+ if (pack.writers !== undefined) {
2532
+ for (const [table, writer] of Object.entries(pack.writers)) {
2533
+ engine.registerWriter(table, writer);
2534
+ }
2535
+ }
2536
+ if (pack.crdt !== undefined) {
2537
+ for (const [table, fields] of Object.entries(pack.crdt)) {
2538
+ engine.registerCrdt(table, fields);
2539
+ }
2540
+ }
2541
+ for (const collection of pack.collections ?? []) {
2542
+ engine.register(collection);
2543
+ }
2544
+ for (const collection of pack.joinCollections ?? []) {
2545
+ engine.registerJoin(collection);
2546
+ }
2547
+ for (const collection of pack.graphCollections ?? []) {
2548
+ engine.registerGraph(collection);
2549
+ }
2550
+ for (const collection of pack.searchCollections ?? []) {
2551
+ engine.registerSearch(collection);
2552
+ }
2553
+ for (const query2 of pack.reactiveQueries ?? []) {
2554
+ engine.registerReactive(query2);
2555
+ }
2556
+ for (const mutation of pack.mutations ?? []) {
2557
+ engine.registerMutation(mutation);
2558
+ }
2559
+ for (const schedule of pack.schedules ?? []) {
2560
+ engine.registerSchedule(schedule);
2561
+ }
2562
+ for (const table of pack.ownsTables) {
2563
+ packTableOwners.set(table, pack.name);
2564
+ }
2565
+ registeredPacks.push({
2566
+ name: pack.name,
2567
+ version: pack.version,
2568
+ ownsTables: [...pack.ownsTables],
2569
+ readsTables: [...pack.readsTables ?? []]
2570
+ });
2421
2571
  },
2422
2572
  inspect: () => {
2423
2573
  const collections = [...registry.entries()].map(([name, def]) => {
@@ -2457,6 +2607,12 @@ var createSyncEngine = (options = {}) => {
2457
2607
  version: entry.version,
2458
2608
  table: entry.table,
2459
2609
  op: entry.change.op
2610
+ })),
2611
+ packs: registeredPacks.map((pack) => ({
2612
+ name: pack.name,
2613
+ version: pack.version,
2614
+ ownsTables: [...pack.ownsTables],
2615
+ readsTables: [...pack.readsTables]
2460
2616
  }))
2461
2617
  };
2462
2618
  },
@@ -2543,9 +2699,17 @@ var createSyncEngine = (options = {}) => {
2543
2699
  };
2544
2700
  }
2545
2701
  };
2702
+ return engine;
2546
2703
  };
2547
2704
  // src/engine/cdc.ts
2548
- import { Elysia } from "elysia";
2705
+ var cachedElysia;
2706
+ var loadElysia = () => {
2707
+ if (cachedElysia !== undefined)
2708
+ return cachedElysia;
2709
+ const mod = __require("elysia");
2710
+ cachedElysia = mod.Elysia;
2711
+ return cachedElysia;
2712
+ };
2549
2713
  var parseSince = (query2, lastEventId) => {
2550
2714
  const raw = query2.since ?? lastEventId ?? "0";
2551
2715
  const parsed = Number(raw);
@@ -2567,68 +2731,71 @@ var syncCdc = ({
2567
2731
  path = "/sync/cdc",
2568
2732
  heartbeatMs = 25000,
2569
2733
  maxBuffer = 1e4
2570
- }) => new Elysia({ name: "@absolutejs/sync/cdc" }).get(path, (context) => {
2571
- const lastEventId = context.request.headers.get("last-event-id");
2572
- const since = parseSince(context.query, lastEventId);
2573
- const encoder = new TextEncoder;
2574
- const stream = new ReadableStream({
2575
- async start(controller) {
2576
- const write = (chunk) => {
2577
- try {
2578
- controller.enqueue(encoder.encode(chunk));
2579
- } catch {}
2580
- };
2581
- write(encodeEvent("open", null, {
2582
- since,
2583
- at: Date.now()
2584
- }));
2585
- const heartbeat = setInterval(() => write(`: ping
2734
+ }) => {
2735
+ const Elysia = loadElysia();
2736
+ return new Elysia({ name: "@absolutejs/sync/cdc" }).get(path, (context) => {
2737
+ const lastEventId = context.request.headers.get("last-event-id");
2738
+ const since = parseSince(context.query, lastEventId);
2739
+ const encoder = new TextEncoder;
2740
+ const stream = new ReadableStream({
2741
+ async start(controller) {
2742
+ const write = (chunk) => {
2743
+ try {
2744
+ controller.enqueue(encoder.encode(chunk));
2745
+ } catch {}
2746
+ };
2747
+ write(encodeEvent("open", null, {
2748
+ since,
2749
+ at: Date.now()
2750
+ }));
2751
+ const heartbeat = setInterval(() => write(`: ping
2586
2752
 
2587
2753
  `), heartbeatMs);
2588
- try {
2589
- for await (const entry of engine.streamChanges({
2590
- since,
2591
- signal: context.request.signal,
2592
- maxBuffer
2593
- })) {
2594
- write(encodeEvent("change", entry.version, entry));
2595
- }
2596
- } catch (error) {
2597
- if (error instanceof MissedChangesError) {
2598
- write(encodeEvent("error", null, {
2599
- name: "MissedChangesError",
2600
- message: error.message,
2601
- requestedSince: error.requestedSince,
2602
- availableSince: error.availableSince
2603
- }));
2604
- } else if (error instanceof CdcConsumerSlowError) {
2605
- write(encodeEvent("error", null, {
2606
- name: "CdcConsumerSlowError",
2607
- message: error.message,
2608
- lastDeliveredVersion: error.lastDeliveredVersion
2609
- }));
2610
- } else {
2611
- write(encodeEvent("error", null, {
2612
- name: error instanceof Error ? error.name : "Error",
2613
- message: error instanceof Error ? error.message : String(error)
2614
- }));
2615
- }
2616
- } finally {
2617
- clearInterval(heartbeat);
2618
2754
  try {
2619
- controller.close();
2620
- } catch {}
2755
+ for await (const entry of engine.streamChanges({
2756
+ since,
2757
+ signal: context.request.signal,
2758
+ maxBuffer
2759
+ })) {
2760
+ write(encodeEvent("change", entry.version, entry));
2761
+ }
2762
+ } catch (error) {
2763
+ if (error instanceof MissedChangesError) {
2764
+ write(encodeEvent("error", null, {
2765
+ name: "MissedChangesError",
2766
+ message: error.message,
2767
+ requestedSince: error.requestedSince,
2768
+ availableSince: error.availableSince
2769
+ }));
2770
+ } else if (error instanceof CdcConsumerSlowError) {
2771
+ write(encodeEvent("error", null, {
2772
+ name: "CdcConsumerSlowError",
2773
+ message: error.message,
2774
+ lastDeliveredVersion: error.lastDeliveredVersion
2775
+ }));
2776
+ } else {
2777
+ write(encodeEvent("error", null, {
2778
+ name: error instanceof Error ? error.name : "Error",
2779
+ message: error instanceof Error ? error.message : String(error)
2780
+ }));
2781
+ }
2782
+ } finally {
2783
+ clearInterval(heartbeat);
2784
+ try {
2785
+ controller.close();
2786
+ } catch {}
2787
+ }
2621
2788
  }
2622
- }
2623
- });
2624
- return new Response(stream, {
2625
- headers: {
2626
- "cache-control": "no-cache, no-transform",
2627
- connection: "keep-alive",
2628
- "content-type": "text/event-stream"
2629
- }
2789
+ });
2790
+ return new Response(stream, {
2791
+ headers: {
2792
+ "cache-control": "no-cache, no-transform",
2793
+ connection: "keep-alive",
2794
+ "content-type": "text/event-stream"
2795
+ }
2796
+ });
2630
2797
  });
2631
- });
2798
+ };
2632
2799
  // src/engine/schema.ts
2633
2800
  var defineSchema = (schemas) => schemas;
2634
2801
  var isFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value);
@@ -2892,6 +3059,7 @@ export {
2892
3059
  filterOp,
2893
3060
  field,
2894
3061
  exponentialBackoff,
3062
+ defineSyncPack,
2895
3063
  defineSearchCollection,
2896
3064
  defineSchema,
2897
3065
  defineSchedule,
@@ -2917,9 +3085,11 @@ export {
2917
3085
  SchemaError,
2918
3086
  SEARCH_SCORE_FIELD,
2919
3087
  RetriesExhaustedError,
3088
+ PackTableConflictError,
3089
+ PackMissingDependencyError,
2920
3090
  MissedChangesError,
2921
3091
  CdcConsumerSlowError
2922
3092
  };
2923
3093
 
2924
- //# debugId=2984A33F7845F42C64756E2164756E21
3094
+ //# debugId=5962FD669C5BC3D964756E2164756E21
2925
3095
  //# sourceMappingURL=index.js.map