@zveltio/sdk 3.0.0-beta.4 → 3.0.0-beta.68

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.
Files changed (101) hide show
  1. package/dist/client/Auth.d.ts.map +1 -1
  2. package/dist/client/Auth.js +3 -0
  3. package/dist/client/QueryBuilder.d.ts.map +1 -1
  4. package/dist/client/QueryBuilder.js +4 -0
  5. package/dist/client/ZveltioClient.d.ts.map +1 -1
  6. package/dist/client/ZveltioClient.js +5 -0
  7. package/dist/client.d.ts +4 -1
  8. package/dist/client.d.ts.map +1 -1
  9. package/dist/client.js +30 -13
  10. package/dist/core.d.ts.map +1 -1
  11. package/dist/core.js +21 -3
  12. package/dist/crdt.d.ts.map +1 -1
  13. package/dist/crdt.js +11 -2
  14. package/dist/ddl/index.d.ts.map +1 -1
  15. package/dist/errors.d.ts +58 -0
  16. package/dist/errors.d.ts.map +1 -0
  17. package/dist/errors.js +124 -0
  18. package/dist/extension/index.d.ts +419 -19
  19. package/dist/extension/index.d.ts.map +1 -1
  20. package/dist/extension/index.js +3 -0
  21. package/dist/extension/jsonb.d.ts +62 -0
  22. package/dist/extension/jsonb.d.ts.map +1 -0
  23. package/dist/extension/jsonb.js +64 -0
  24. package/dist/extension/multipart.d.ts +58 -0
  25. package/dist/extension/multipart.d.ts.map +1 -0
  26. package/dist/extension/multipart.js +61 -0
  27. package/dist/extension/numeric.d.ts +94 -0
  28. package/dist/extension/numeric.d.ts.map +1 -0
  29. package/dist/extension/numeric.js +162 -0
  30. package/dist/extension/permission-gate.d.ts +15 -0
  31. package/dist/extension/permission-gate.d.ts.map +1 -1
  32. package/dist/extension/permission-gate.js +33 -1
  33. package/dist/index.d.ts +2 -0
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +1 -0
  36. package/dist/local-store.d.ts +5 -0
  37. package/dist/local-store.d.ts.map +1 -1
  38. package/dist/local-store.js +62 -10
  39. package/dist/offline/index.d.ts +3 -3
  40. package/dist/offline/index.js +51 -15
  41. package/dist/publish/index.d.ts.map +1 -1
  42. package/dist/publish/index.js +6 -1
  43. package/dist/realtime.d.ts +9 -1
  44. package/dist/realtime.d.ts.map +1 -1
  45. package/dist/realtime.js +15 -2
  46. package/dist/rpc/index.d.ts.map +1 -1
  47. package/dist/rpc/index.js +2 -0
  48. package/dist/schema-codegen.d.ts +3 -1
  49. package/dist/schema-codegen.d.ts.map +1 -1
  50. package/dist/schema-codegen.js +41 -7
  51. package/dist/schema-watcher.d.ts +1 -1
  52. package/dist/schema-watcher.js +1 -1
  53. package/dist/studio/index.d.ts +1 -0
  54. package/dist/studio/index.d.ts.map +1 -1
  55. package/dist/studio/index.js +3 -0
  56. package/dist/studio/slot-owner-registry.d.ts +13 -0
  57. package/dist/studio/slot-owner-registry.d.ts.map +1 -0
  58. package/dist/studio/slot-owner-registry.js +21 -0
  59. package/dist/svelte.d.ts +1 -1
  60. package/dist/svelte.d.ts.map +1 -1
  61. package/dist/svelte.js +4 -2
  62. package/dist/sync-manager.d.ts +30 -4
  63. package/dist/sync-manager.d.ts.map +1 -1
  64. package/dist/sync-manager.js +101 -15
  65. package/dist/testing/index.d.ts.map +1 -1
  66. package/dist/testing/index.js +23 -1
  67. package/dist/testing/with-test-db.d.ts.map +1 -1
  68. package/dist/testing/with-test-db.js +21 -14
  69. package/dist/tests/client.test.d.ts +2 -0
  70. package/dist/tests/client.test.d.ts.map +1 -0
  71. package/dist/tests/client.test.js +55 -0
  72. package/dist/tests/crdt-orset.test.d.ts +2 -0
  73. package/dist/tests/crdt-orset.test.d.ts.map +1 -0
  74. package/dist/tests/crdt-orset.test.js +23 -0
  75. package/dist/tests/local-store.test.js +44 -0
  76. package/dist/tests/multipart.test.d.ts +2 -0
  77. package/dist/tests/multipart.test.d.ts.map +1 -0
  78. package/dist/tests/multipart.test.js +37 -0
  79. package/dist/tests/offline-provider-crdt.test.d.ts +2 -0
  80. package/dist/tests/offline-provider-crdt.test.d.ts.map +1 -0
  81. package/dist/tests/offline-provider-crdt.test.js +119 -0
  82. package/dist/tests/realtime.test.d.ts +2 -0
  83. package/dist/tests/realtime.test.d.ts.map +1 -0
  84. package/dist/tests/realtime.test.js +39 -0
  85. package/dist/tests/sync-manager.test.d.ts +2 -0
  86. package/dist/tests/sync-manager.test.d.ts.map +1 -0
  87. package/dist/tests/sync-manager.test.js +65 -0
  88. package/dist/tests/with-test-db.test.d.ts +2 -0
  89. package/dist/tests/with-test-db.test.d.ts.map +1 -0
  90. package/dist/tests/with-test-db.test.js +57 -0
  91. package/dist/types/index.d.ts.map +1 -1
  92. package/dist/validate/index.d.ts +49 -0
  93. package/dist/validate/index.d.ts.map +1 -1
  94. package/dist/validate/index.js +494 -7
  95. package/dist/validate/shared-message-keys.d.ts +16 -0
  96. package/dist/validate/shared-message-keys.d.ts.map +1 -0
  97. package/dist/validate/shared-message-keys.js +227 -0
  98. package/package.json +7 -7
  99. package/dist/generate-types.d.ts +0 -37
  100. package/dist/generate-types.d.ts.map +0 -1
  101. package/dist/generate-types.js +0 -157
@@ -20,6 +20,10 @@ export declare class SyncManager {
20
20
  private syncTimer;
21
21
  private isOnline;
22
22
  private isSyncing;
23
+ /** Set by `stop()`; `start()` checks it after every await it makes. */
24
+ private stopped;
25
+ private readonly onOnline;
26
+ private readonly onOffline;
23
27
  private listeners;
24
28
  constructor(client: ZveltioClient, config?: SyncManagerConfig);
25
29
  start(realtimeUrl?: string): Promise<void>;
@@ -32,22 +36,22 @@ export declare class SyncManager {
32
36
  collection(name: string): {
33
37
  /** List records — reads LOCAL instantly */
34
38
  list: () => Promise<{
35
- _syncStatus: "synced" | "pending" | "conflict";
39
+ _syncStatus: "conflict" | "synced" | "pending";
36
40
  id: string;
37
41
  }[]>;
38
42
  /** Get one record — reads LOCAL instantly */
39
43
  get: (id: string) => Promise<{
40
- _syncStatus: "synced" | "pending" | "conflict";
44
+ _syncStatus: "conflict" | "synced" | "pending";
41
45
  id: string;
42
46
  } | null>;
43
47
  /** Create — writes LOCAL + queues sync */
44
48
  create: (data: Record<string, any>) => Promise<{
45
- _syncStatus: "synced" | "pending" | "conflict";
49
+ _syncStatus: "conflict" | "synced" | "pending";
46
50
  id: string;
47
51
  }>;
48
52
  /** Update — writes LOCAL + queues sync */
49
53
  update: (id: string, data: Record<string, any>) => Promise<{
50
- _syncStatus: "synced" | "pending" | "conflict";
54
+ _syncStatus: "conflict" | "synced" | "pending";
51
55
  id: string;
52
56
  }>;
53
57
  /** Delete — soft delete LOCAL + queues sync */
@@ -62,6 +66,28 @@ export declare class SyncManager {
62
66
  /** Force sync now (non-blocking) */
63
67
  syncNow(): Promise<void>;
64
68
  private notifyListeners;
69
+ /**
70
+ * Open the local store without starting background sync.
71
+ *
72
+ * `start()` opens the store, attaches online/offline listeners, connects
73
+ * realtime and installs a periodic timer. A caller driving `pull()` and
74
+ * `push()` explicitly — the `OfflineProvider` contract in `offline/` — wants
75
+ * the store and none of the rest, and a timer firing `syncNow()` underneath it
76
+ * makes "how many operations did that push send" unanswerable.
77
+ */
78
+ open(): Promise<void>;
79
+ /**
80
+ * Pull rows for the named collections from the server into the local store.
81
+ *
82
+ * There was no way to do this. Records reached the local store only through
83
+ * realtime events, one at a time, which means a client that was offline while
84
+ * a row was created never learns it exists: it missed the event and nothing
85
+ * ever asks the server what it has. `syncNow()` is push-only despite the name.
86
+ *
87
+ * Pages through `list()` rather than asking for everything at once, and stops
88
+ * on the first page that comes back empty or short.
89
+ */
90
+ pull(collections: string[], pageSize?: number): Promise<number>;
65
91
  /** Status: pending operations count, conflicts count */
66
92
  getStatus(): Promise<{
67
93
  pending: number;
@@ -1 +1 @@
1
- {"version":3,"file":"sync-manager.d.ts","sourceRoot":"","sources":["../src/sync-manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAMjD,MAAM,MAAM,sBAAsB,GAAG,KAAK,GAAG,QAAQ,CAAC;AAEtD,MAAM,WAAW,iBAAiB;IAChC,0CAA0C;IAC1C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,oDAAoD;IACpD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qDAAqD;IACrD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,oDAAoD;IACpD,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,KAAK,GAAG,CAAC;IAC9C,mHAAmH;IACnH,kBAAkB,CAAC,EAAE,sBAAsB,CAAC;CAC7C;AAED,qBAAa,WAAW;IACtB,OAAO,CAAC,KAAK,CAAa;IAC1B,OAAO,CAAC,MAAM,CAAgB;IAC9B,OAAO,CAAC,QAAQ,CAAgC;IAChD,OAAO,CAAC,MAAM,CAA8B;IAC5C,OAAO,CAAC,SAAS,CAA+C;IAChE,OAAO,CAAC,QAAQ,CAAiB;IACjC,OAAO,CAAC,SAAS,CAAkB;IACnC,OAAO,CAAC,SAAS,CAAyD;gBAE9D,MAAM,EAAE,aAAa,EAAE,MAAM,GAAE,iBAAsB;IAY3D,KAAK,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA8BhD;;;;;OAKG;IACH,UAAU,CAAC,IAAI,EAAE,MAAM;QAEnB,2CAA2C;;;;;QAU3C,6CAA6C;kBAC7B,MAAM;;;;QAUtB,0CAA0C;uBACrB,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;;;;QAYxC,0CAA0C;qBACvB,MAAM,QAAQ,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;;;;QAapD,+CAA+C;qBAC5B,MAAM;QAMzB,qDAAqD;8BAC/B,CAAC,OAAO,EAAE,GAAG,EAAE,KAAK,IAAI;QA0C9C,8CAA8C;;QAK9C,kCAAkC;8BACN,MAAM,gBAAgB,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;;IAQzE,oCAAoC;IAC9B,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;YA8FhB,eAAe;IAmB7B,wDAAwD;IAClD,SAAS,IAAI,OAAO,CAAC;QACzB,OAAO,EAAE,MAAM,CAAC;QAChB,SAAS,EAAE,MAAM,CAAC;QAClB,QAAQ,EAAE,OAAO,CAAC;KACnB,CAAC;IAUI,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAK5B"}
1
+ {"version":3,"file":"sync-manager.d.ts","sourceRoot":"","sources":["../src/sync-manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAMjD,MAAM,MAAM,sBAAsB,GAAG,KAAK,GAAG,QAAQ,CAAC;AAEtD,MAAM,WAAW,iBAAiB;IAChC,0CAA0C;IAC1C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,oDAAoD;IACpD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qDAAqD;IACrD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,oDAAoD;IAEpD,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,KAAK,GAAG,CAAC;IAC9C,mHAAmH;IACnH,kBAAkB,CAAC,EAAE,sBAAsB,CAAC;CAC7C;AAED,qBAAa,WAAW;IACtB,OAAO,CAAC,KAAK,CAAa;IAC1B,OAAO,CAAC,MAAM,CAAgB;IAC9B,OAAO,CAAC,QAAQ,CAAgC;IAChD,OAAO,CAAC,MAAM,CAA8B;IAC5C,OAAO,CAAC,SAAS,CAA+C;IAChE,OAAO,CAAC,QAAQ,CAAiB;IACjC,OAAO,CAAC,SAAS,CAAkB;IACnC,uEAAuE;IACvE,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAGvB;IACF,OAAO,CAAC,QAAQ,CAAC,SAAS,CAExB;IAEF,OAAO,CAAC,SAAS,CAAyD;gBAE9D,MAAM,EAAE,aAAa,EAAE,MAAM,GAAE,iBAAsB;IAY3D,KAAK,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA+BhD;;;;;OAKG;IACH,UAAU,CAAC,IAAI,EAAE,MAAM;QAEnB,2CAA2C;;;;;QAU3C,6CAA6C;kBAC7B,MAAM;;;;QAUtB,0CAA0C;uBAErB,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;;;;QAYxC,0CAA0C;qBAEvB,MAAM,QAAQ,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;;;;QAapD,+CAA+C;qBAC5B,MAAM;QAMzB,qDAAqD;8BAE/B,CAAC,OAAO,EAAE,GAAG,EAAE,KAAK,IAAI;QA8C9C,8CAA8C;;QAK9C,kCAAkC;8BAEN,MAAM,gBAAgB,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;;IAQzE,oCAAoC;IAC9B,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;YAwGhB,eAAe;IAmB7B;;;;;;;;OAQG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAI3B;;;;;;;;;;OAUG;IACG,IAAI,CAAC,WAAW,EAAE,MAAM,EAAE,EAAE,QAAQ,SAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAuBlE,wDAAwD;IAClD,SAAS,IAAI,OAAO,CAAC;QACzB,OAAO,EAAE,MAAM,CAAC;QAChB,SAAS,EAAE,MAAM,CAAC;QAClB,QAAQ,EAAE,OAAO,CAAC;KACnB,CAAC;IAUI,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAY5B"}
@@ -9,6 +9,16 @@ export class SyncManager {
9
9
  syncTimer = null;
10
10
  isOnline = true;
11
11
  isSyncing = false;
12
+ /** Set by `stop()`; `start()` checks it after every await it makes. */
13
+ stopped = false;
14
+ onOnline = () => {
15
+ this.isOnline = true;
16
+ this.syncNow(); // Sync immediately on reconnect
17
+ };
18
+ onOffline = () => {
19
+ this.isOnline = false;
20
+ };
21
+ // biome-ignore lint/suspicious/noExplicitAny: legacy any; tracked in hardening plan item H-01
12
22
  listeners = new Map();
13
23
  constructor(client, config = {}) {
14
24
  this.store = new LocalStore();
@@ -22,21 +32,24 @@ export class SyncManager {
22
32
  };
23
33
  }
24
34
  async start(realtimeUrl) {
35
+ this.stopped = false;
25
36
  await this.store.open();
37
+ // A caller that unmounts before `start()` settles calls `stop()` first — a
38
+ // React effect cleanup, and StrictMode runs one on every mount in
39
+ // development. Arming the timer and socket after that leaked both forever.
40
+ if (this.stopped)
41
+ return this.store.close();
26
42
  // Online/offline detection
27
43
  if (typeof window !== 'undefined') {
28
- window.addEventListener('online', () => {
29
- this.isOnline = true;
30
- this.syncNow(); // Sync immediately on reconnect
31
- });
32
- window.addEventListener('offline', () => {
33
- this.isOnline = false;
34
- });
44
+ window.addEventListener('online', this.onOnline);
45
+ window.addEventListener('offline', this.onOffline);
35
46
  this.isOnline = navigator.onLine;
36
47
  }
37
48
  // Realtime: receives push updates from server
38
49
  if (realtimeUrl) {
39
50
  const { ZveltioRealtime } = await import('./realtime.js');
51
+ if (this.stopped)
52
+ return;
40
53
  this.realtime = new ZveltioRealtime(realtimeUrl);
41
54
  this.realtime.connect();
42
55
  // Subscriptions are per-collection via collection()
@@ -75,6 +88,7 @@ export class SyncManager {
75
88
  };
76
89
  },
77
90
  /** Create — writes LOCAL + queues sync */
91
+ // biome-ignore lint/suspicious/noExplicitAny: legacy any; tracked in hardening plan item H-01
78
92
  create: async (data) => {
79
93
  const id = data.id || generateUUID();
80
94
  const record = await this.store.put(name, id, data);
@@ -87,6 +101,7 @@ export class SyncManager {
87
101
  };
88
102
  },
89
103
  /** Update — writes LOCAL + queues sync */
104
+ // biome-ignore lint/suspicious/noExplicitAny: legacy any; tracked in hardening plan item H-01
90
105
  update: async (id, data) => {
91
106
  const existing = await this.store.get(name, id);
92
107
  const merged = { ...(existing?.data || {}), ...data };
@@ -106,6 +121,7 @@ export class SyncManager {
106
121
  this.syncNow();
107
122
  },
108
123
  /** Subscribe la changes (realtime + local writes) */
124
+ // biome-ignore lint/suspicious/noExplicitAny: legacy any; tracked in hardening plan item H-01
109
125
  subscribe: (callback) => {
110
126
  if (!this.listeners.has(name))
111
127
  this.listeners.set(name, new Set());
@@ -114,19 +130,24 @@ export class SyncManager {
114
130
  let unsubRealtime;
115
131
  if (this.realtime) {
116
132
  unsubRealtime = this.realtime.subscribe(name, async (event) => {
117
- // Apply update from server to local store
118
- if (event.event === 'record.created' || event.event === 'record.updated') {
133
+ // Apply update from server to local store. The engine's socket
134
+ // sends `{ type: 'event', event: 'insert'|'update'|'delete', data }`
135
+ // (routes/ws.ts `broadcastEvent`); the id lives on `data`.
136
+ const id = event.data?.id;
137
+ if (event.type !== 'event' || !id)
138
+ return;
139
+ if (event.event === 'insert' || event.event === 'update') {
119
140
  try {
120
- const serverRecord = await this.client.collection(name).get(event.record_id);
121
- await this.store.applyServerUpdate(name, event.record_id, serverRecord, Date.now());
141
+ const serverRecord = await this.client.collection(name).get(id);
142
+ await this.store.applyServerUpdate(name, id, serverRecord, Date.now());
122
143
  this.notifyListeners(name);
123
144
  }
124
145
  catch {
125
146
  /* offline or error — ignore, periodic sync will resolve */
126
147
  }
127
148
  }
128
- else if (event.event === 'record.deleted') {
129
- await this.store.delete(name, event.record_id);
149
+ else if (event.event === 'delete') {
150
+ await this.store.applyServerDelete(name, id);
130
151
  this.notifyListeners(name);
131
152
  }
132
153
  });
@@ -150,6 +171,7 @@ export class SyncManager {
150
171
  return this.store.getConflicts(name);
151
172
  },
152
173
  /** Resolve a conflict manually */
174
+ // biome-ignore lint/suspicious/noExplicitAny: legacy any; tracked in hardening plan item H-01
153
175
  resolveConflict: async (id, resolvedData) => {
154
176
  await this.store.resolveConflict(name, id, resolvedData);
155
177
  this.notifyListeners(name);
@@ -170,6 +192,7 @@ export class SyncManager {
170
192
  const file = new File([blobItem.blob], `offline_${blobItem.id}`, {
171
193
  type: blobItem.blob.type,
172
194
  });
195
+ // biome-ignore lint/suspicious/noExplicitAny: legacy any; tracked in hardening plan item H-01
173
196
  const result = (await this.client.storage.upload(file));
174
197
  const url = result?.url || result?.publicUrl || result?.path || '';
175
198
  if (!url)
@@ -214,6 +237,7 @@ export class SyncManager {
214
237
  }
215
238
  await this.store.markSynced(op.id, op.collection, op.recordId, serverVersion);
216
239
  this.notifyListeners(op.collection);
240
+ // biome-ignore lint/suspicious/noExplicitAny: legacy any; tracked in hardening plan item H-01
217
241
  }
218
242
  catch (err) {
219
243
  // Conflict from server (409) — apply conflict resolution
@@ -224,15 +248,23 @@ export class SyncManager {
224
248
  // CRDT LWW merge if both sides have CRDT docs and strategy is 'lww'
225
249
  if (this.config.conflictResolution === 'lww' &&
226
250
  localRecord?._crdtDoc &&
251
+ // biome-ignore lint/suspicious/noExplicitAny: legacy any; tracked in hardening plan item H-01
227
252
  serverRecord.__crdt) {
253
+ // biome-ignore lint/suspicious/noExplicitAny: legacy any; tracked in hardening plan item H-01
228
254
  const merged = mergeLWW(localRecord._crdtDoc, serverRecord.__crdt);
229
- const resolvedData = fromDocument(merged);
230
- await this.store.resolveConflict(op.collection, op.recordId, resolvedData);
255
+ await this.store.resolveConflict(op.collection, op.recordId, fromDocument(merged));
231
256
  }
232
257
  else {
233
258
  const resolved = this.config.onConflict(localRecord?.data, serverRecord);
234
259
  await this.store.resolveConflict(op.collection, op.recordId, resolved);
235
260
  }
261
+ // The operation the server just rejected stayed in the queue with
262
+ // its original payload and its attempt count untouched, so the next
263
+ // tick sent the same payload, took the same 409 and resolved the
264
+ // same conflict again — every `syncInterval`, with no end. Count
265
+ // the attempt so `maxRetries` retires it; the resolved row is
266
+ // queued by `resolveConflict` as its own operation.
267
+ await this.store.markFailed(op.id, 'rejected with 409 — resolved locally');
236
268
  }
237
269
  catch {
238
270
  /* fallback: server wins — ignore error */
@@ -268,6 +300,53 @@ export class SyncManager {
268
300
  }
269
301
  });
270
302
  }
303
+ /**
304
+ * Open the local store without starting background sync.
305
+ *
306
+ * `start()` opens the store, attaches online/offline listeners, connects
307
+ * realtime and installs a periodic timer. A caller driving `pull()` and
308
+ * `push()` explicitly — the `OfflineProvider` contract in `offline/` — wants
309
+ * the store and none of the rest, and a timer firing `syncNow()` underneath it
310
+ * makes "how many operations did that push send" unanswerable.
311
+ */
312
+ async open() {
313
+ await this.store.open();
314
+ }
315
+ /**
316
+ * Pull rows for the named collections from the server into the local store.
317
+ *
318
+ * There was no way to do this. Records reached the local store only through
319
+ * realtime events, one at a time, which means a client that was offline while
320
+ * a row was created never learns it exists: it missed the event and nothing
321
+ * ever asks the server what it has. `syncNow()` is push-only despite the name.
322
+ *
323
+ * Pages through `list()` rather than asking for everything at once, and stops
324
+ * on the first page that comes back empty or short.
325
+ */
326
+ async pull(collections, pageSize = 200) {
327
+ let applied = 0;
328
+ for (const name of collections) {
329
+ for (let page = 1;; page++) {
330
+ const result = await this.client.collection(name).list({ page, limit: pageSize });
331
+ const rows = result?.data ?? [];
332
+ for (const row of rows) {
333
+ const id = row.id;
334
+ if (!id)
335
+ continue;
336
+ // `Date.now()` as the server version: the collections API does not
337
+ // return a version column, and the local store only compares versions
338
+ // to decide whether an incoming row is newer than one it already has.
339
+ // A server row arriving now IS newer than anything already local.
340
+ await this.store.applyServerUpdate(name, id, row, Date.now());
341
+ applied++;
342
+ }
343
+ if (rows.length < pageSize)
344
+ break;
345
+ }
346
+ await this.notifyListeners(name);
347
+ }
348
+ return applied;
349
+ }
271
350
  /** Status: pending operations count, conflicts count */
272
351
  async getStatus() {
273
352
  const pending = await this.store.getPendingOps();
@@ -279,9 +358,16 @@ export class SyncManager {
279
358
  };
280
359
  }
281
360
  async stop() {
361
+ this.stopped = true;
282
362
  if (this.syncTimer)
283
363
  clearInterval(this.syncTimer);
364
+ this.syncTimer = null;
365
+ if (typeof window !== 'undefined') {
366
+ window.removeEventListener('online', this.onOnline);
367
+ window.removeEventListener('offline', this.onOffline);
368
+ }
284
369
  this.realtime?.disconnect();
370
+ this.realtime = null;
285
371
  await this.store.close();
286
372
  }
287
373
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"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"}
@@ -24,10 +24,13 @@ import { Hono } from 'hono';
24
24
  * `.executeTakeFirstOrThrow()` returns a preset if matched, otherwise
25
25
  * an empty list (for execute) or undefined.
26
26
  */
27
+ // biome-ignore lint/suspicious/noExplicitAny: legacy any; tracked in hardening plan item H-01
27
28
  export function mockDb(presets = {}) {
28
29
  const calls = [];
29
30
  const presetMap = new Map(Object.entries(presets));
31
+ // biome-ignore lint/suspicious/noExplicitAny: legacy any; tracked in hardening plan item H-01
30
32
  function buildChain(prefix) {
33
+ // biome-ignore lint/suspicious/noExplicitAny: legacy any; tracked in hardening plan item H-01
31
34
  const proxy = new Proxy(() => { }, {
32
35
  get(_, prop) {
33
36
  if (typeof prop === 'symbol')
@@ -78,6 +81,7 @@ export function mockDb(presets = {}) {
78
81
  });
79
82
  return proxy;
80
83
  }
84
+ // biome-ignore lint/suspicious/noExplicitAny: legacy any; tracked in hardening plan item H-01
81
85
  return buildChain([]);
82
86
  }
83
87
  function matchPreset(chain, presets) {
@@ -93,7 +97,9 @@ function matchPreset(chain, presets) {
93
97
  }
94
98
  export function mockEventBus() {
95
99
  const emitted = [];
100
+ // biome-ignore lint/suspicious/noExplicitAny: legacy any; tracked in hardening plan item H-01
96
101
  const listeners = new Map();
102
+ // biome-ignore lint/suspicious/noExplicitAny: legacy any; tracked in hardening plan item H-01
97
103
  const beforeHandlers = new Map();
98
104
  return {
99
105
  emit(event, payload) {
@@ -128,12 +134,14 @@ export function mockEventBus() {
128
134
  };
129
135
  },
130
136
  async runBefore(event, seed) {
137
+ // biome-ignore lint/suspicious/noExplicitAny: legacy any; tracked in hardening plan item H-01
131
138
  const payload = { ...seed };
132
139
  let aborted = false;
133
140
  let abortReason = '';
134
141
  payload.abort = (reason) => {
135
142
  aborted = true;
136
143
  abortReason = reason;
144
+ // biome-ignore lint/suspicious/noExplicitAny: legacy any; tracked in hardening plan item H-01
137
145
  const err = new Error(`Aborted: ${reason}`);
138
146
  err.name = 'AbortHookError';
139
147
  err.reason = reason;
@@ -187,8 +195,10 @@ export function mockServiceRegistry() {
187
195
  }
188
196
  // ─── Mock queryAlter / entityAccess scoped registries ──────────────────────
189
197
  function mockQueryAlterScope() {
198
+ // biome-ignore lint/suspicious/noExplicitAny: legacy any; tracked in hardening plan item H-01
190
199
  const entries = [];
191
200
  return {
201
+ // biome-ignore lint/suspicious/noExplicitAny: legacy any; tracked in hardening plan item H-01
192
202
  register(def) {
193
203
  entries.push(def);
194
204
  },
@@ -201,8 +211,10 @@ function mockQueryAlterScope() {
201
211
  };
202
212
  }
203
213
  function mockEntityAccessScope() {
214
+ // biome-ignore lint/suspicious/noExplicitAny: legacy any; tracked in hardening plan item H-01
204
215
  const entries = [];
205
216
  return {
217
+ // biome-ignore lint/suspicious/noExplicitAny: legacy any; tracked in hardening plan item H-01
206
218
  register(def) {
207
219
  entries.push(def);
208
220
  },
@@ -214,6 +226,7 @@ function mockEntityAccessScope() {
214
226
  },
215
227
  };
216
228
  }
229
+ // biome-ignore lint/suspicious/noExplicitAny: legacy any; tracked in hardening plan item H-01
217
230
  export function mockAuth(opts = {}) {
218
231
  const user = opts.user === undefined ? { id: 'test-user', email: 'test@example.com', roles: [] } : opts.user;
219
232
  return {
@@ -224,6 +237,7 @@ export function mockAuth(opts = {}) {
224
237
  },
225
238
  };
226
239
  }
240
+ // biome-ignore lint/suspicious/noExplicitAny: legacy any; tracked in hardening plan item H-01
227
241
  export function createTestContext(opts = {}) {
228
242
  const db = opts.db ?? mockDb();
229
243
  const events = opts.events ?? mockEventBus();
@@ -239,14 +253,20 @@ export function createTestContext(opts = {}) {
239
253
  deserialize: (_t, v) => v,
240
254
  serialize: (_t, v) => v,
241
255
  },
256
+ // biome-ignore lint/suspicious/noExplicitAny: legacy any; tracked in hardening plan item H-01
242
257
  events: events,
243
258
  checkPermission: async () => true,
244
259
  getUserRoles: async () => ['admin'],
260
+ // biome-ignore lint/suspicious/noExplicitAny: legacy any; tracked in hardening plan item H-01
245
261
  DDLManager: {},
262
+ // biome-ignore lint/suspicious/noExplicitAny: legacy any; tracked in hardening plan item H-01
246
263
  services: mockServiceRegistry(),
264
+ // biome-ignore lint/suspicious/noExplicitAny: legacy any; tracked in hardening plan item H-01
247
265
  queryAlter: mockQueryAlterScope(),
266
+ // biome-ignore lint/suspicious/noExplicitAny: legacy any; tracked in hardening plan item H-01
248
267
  entityAccess: mockEntityAccessScope(),
249
268
  registerPublicRoute: () => { },
269
+ // biome-ignore lint/suspicious/noExplicitAny: legacy any; tracked in hardening plan item H-01
250
270
  internals: {},
251
271
  ...(opts.extra ?? {}),
252
272
  };
@@ -259,7 +279,9 @@ export function createTestContext(opts = {}) {
259
279
  * mounts the sub-app at `/ext/<extension.name>` by default — same shape
260
280
  * the engine produces in production.
261
281
  */
262
- export async function createTestApp(extension, opts = {}) {
282
+ export async function createTestApp(
283
+ // biome-ignore lint/suspicious/noExplicitAny: legacy any; tracked in hardening plan item H-01
284
+ extension, opts = {}) {
263
285
  const ctx = opts.ctx ?? createTestContext();
264
286
  const strategy = extension.mountStrategy ?? 'global';
265
287
  if (strategy === 'subapp') {
@@ -1 +1 @@
1
- {"version":3,"file":"with-test-db.d.ts","sourceRoot":"","sources":["../../src/testing/with-test-db.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAErC,MAAM,WAAW,iBAAiB;IAChC,qDAAqD;IACrD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kDAAkD;IAClD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gEAAgE;IAChE,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;OAGG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,MAAM;IACrB,8DAA8D;IAC9D,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC;IAChB,+CAA+C;IAC/C,gBAAgB,EAAE,MAAM,CAAC;IACzB,iEAAiE;IACjE,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AA6BD;;;;;;;GAOG;AACH,wBAAsB,WAAW,CAAC,IAAI,GAAE,iBAAsB,GAAG,OAAO,CAAC,MAAM,CAAC,CA4E/E;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,UAAU,CAAC,CAAC,EAChC,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,iBAAiB,EAC/D,OAAO,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,GACxC,OAAO,CAAC,CAAC,CAAC,CAYZ;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,qBAAqB,CAAC,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAUhG;AAED;;;GAGG;AACH,wBAAsB,mBAAmB,CAAC,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAIzF;AAED;;;;GAIG;AACH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAiBtD;AAgBD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAuErD;AAGD,eAAO,MAAM,iBAAiB;;CAAsB,CAAC"}
1
+ {"version":3,"file":"with-test-db.d.ts","sourceRoot":"","sources":["../../src/testing/with-test-db.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAGrC,MAAM,WAAW,iBAAiB;IAChC,qDAAqD;IACrD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kDAAkD;IAClD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gEAAgE;IAChE,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;OAGG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,MAAM;IACrB,8DAA8D;IAE9D,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC;IAChB,+CAA+C;IAC/C,gBAAgB,EAAE,MAAM,CAAC;IACzB,iEAAiE;IACjE,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AA4BD;;;;;;;GAOG;AACH,wBAAsB,WAAW,CAAC,IAAI,GAAE,iBAAsB,GAAG,OAAO,CAAC,MAAM,CAAC,CA8E/E;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,UAAU,CAAC,CAAC,EAEhC,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,iBAAiB,EAE/D,OAAO,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,GACxC,OAAO,CAAC,CAAC,CAAC,CAYZ;AAED;;;;;;;;;;;GAWG;AAGH,wBAAsB,qBAAqB,CAAC,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAgBhG;AAED;;;GAGG;AAGH,wBAAsB,mBAAmB,CAAC,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAIzF;AAED;;;;GAIG;AACH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAStD;AAkBD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAuErD;AAGD,eAAO,MAAM,iBAAiB;;CAAsB,CAAC"}
@@ -24,15 +24,15 @@
24
24
  * weight just to use `mockDb`. By importing lazily, the cost is
25
25
  * opt-in at the call site.
26
26
  */
27
+ import { parseMigrationSql } from '../validate/migration-parse.js';
27
28
  // ── Module-level reuse cache ────────────────────────────────────────────────
28
29
  //
29
30
  // When `reuse: true`, we keep a single container alive for the process
30
31
  // lifetime. The first call to `withTestDb` boots it; subsequent calls open
31
32
  // fresh DBs *inside* the same container (CREATE DATABASE per-call), so
32
33
  // tests stay isolated without paying the container-start cost again.
34
+ // biome-ignore lint/suspicious/noExplicitAny: legacy any; tracked in hardening plan item H-01
33
35
  let _sharedContainer = null;
34
- let _sharedClient = null;
35
- let _sharedDbCounter = 0;
36
36
  async function loadPgContainer() {
37
37
  try {
38
38
  return await import('@testcontainers/postgresql');
@@ -64,6 +64,7 @@ export async function startTestDb(opts = {}) {
64
64
  let port;
65
65
  let username;
66
66
  let password;
67
+ // biome-ignore lint/suspicious/noExplicitAny: legacy any; tracked in hardening plan item H-01
67
68
  let createdContainer = null;
68
69
  if (opts.reuse && _sharedContainer) {
69
70
  host = _sharedContainer.getHost();
@@ -87,14 +88,15 @@ export async function startTestDb(opts = {}) {
87
88
  const adminPool = await openPgPool(adminConnStr);
88
89
  try {
89
90
  await adminPool.query(`CREATE DATABASE "${database}"`);
90
- _sharedDbCounter++;
91
91
  }
92
92
  finally {
93
93
  await adminPool.end();
94
94
  }
95
95
  const connectionString = `postgres://${username}:${password}@${host}:${port}/${database}`;
96
96
  const pool = await openPgPool(connectionString);
97
+ // biome-ignore lint/suspicious/noExplicitAny: legacy any; tracked in hardening plan item H-01
97
98
  const db = new Kysely({
99
+ // biome-ignore lint/suspicious/noExplicitAny: legacy any; tracked in hardening plan item H-01
98
100
  dialect: new PostgresDialect({ pool: pool }),
99
101
  });
100
102
  if (opts.migrations && opts.migrations.length > 0) {
@@ -147,7 +149,11 @@ export async function startTestDb(opts = {}) {
147
149
  * });
148
150
  * });
149
151
  */
150
- export async function withTestDb(fnOrOpts, maybeFn) {
152
+ export async function withTestDb(
153
+ // biome-ignore lint/suspicious/noExplicitAny: legacy any; tracked in hardening plan item H-01
154
+ fnOrOpts,
155
+ // biome-ignore lint/suspicious/noExplicitAny: legacy any; tracked in hardening plan item H-01
156
+ maybeFn) {
151
157
  const opts = typeof fnOrOpts === 'function' ? {} : fnOrOpts;
152
158
  const fn = typeof fnOrOpts === 'function' ? fnOrOpts : maybeFn;
153
159
  if (typeof fn !== 'function') {
@@ -173,10 +179,17 @@ export async function withTestDb(fnOrOpts, maybeFn) {
173
179
  * const files = glob('engine/migrations/*.sql').map((p) => readFileSync(p, 'utf8'));
174
180
  * await applyMigrationStrings(db, files);
175
181
  */
182
+ // biome-ignore lint/suspicious/noExplicitAny: legacy any; tracked in hardening plan item H-01
176
183
  export async function applyMigrationStrings(db, sqlStrings) {
177
184
  const { sql } = await loadKysely();
178
185
  for (const raw of sqlStrings) {
179
- const statements = splitStatements(raw);
186
+ // Only the UP half. A Zveltio migration keeps its rollback in the same file
187
+ // behind a `-- DOWN` marker, and the runner that applies it in production
188
+ // splits on that marker — this helper did not, so the documented example
189
+ // (glob `engine/migrations/*.sql`, hand them all to this function) created
190
+ // the schema and then dropped it again, in the same call. Every assertion
191
+ // after it failed on a table that had existed a statement earlier.
192
+ const statements = splitStatements(parseMigrationSql(raw).up);
180
193
  for (const stmt of statements) {
181
194
  const trimmed = stmt.trim();
182
195
  if (!trimmed)
@@ -189,6 +202,7 @@ export async function applyMigrationStrings(db, sqlStrings) {
189
202
  * Same as `applyMigrationStrings` but takes file paths. Reads each file
190
203
  * synchronously, applies in order. Bun-native — uses `Bun.file()`.
191
204
  */
205
+ // biome-ignore lint/suspicious/noExplicitAny: legacy any; tracked in hardening plan item H-01
192
206
  export async function applyMigrationFiles(db, paths) {
193
207
  // Bun.file().text() returns a Promise<string>.
194
208
  const contents = await Promise.all(paths.map((p) => Bun.file(p).text()));
@@ -200,15 +214,6 @@ export async function applyMigrationFiles(db, paths) {
200
214
  * reuse container is active.
201
215
  */
202
216
  export async function stopReusedTestDb() {
203
- if (_sharedClient) {
204
- try {
205
- await _sharedClient.end();
206
- }
207
- catch {
208
- /* */
209
- }
210
- _sharedClient = null;
211
- }
212
217
  if (_sharedContainer) {
213
218
  try {
214
219
  await _sharedContainer.stop();
@@ -221,12 +226,14 @@ export async function stopReusedTestDb() {
221
226
  }
222
227
  // ── Internals ───────────────────────────────────────────────────────────────
223
228
  /** Open a pg connection pool. Uses `pg` (peer dep with testcontainers). */
229
+ // biome-ignore lint/suspicious/noExplicitAny: legacy any; tracked in hardening plan item H-01
224
230
  async function openPgPool(connectionString) {
225
231
  const pg = await import('pg').catch(() => {
226
232
  throw new Error(`withTestDb requires the 'pg' npm package. Install it alongside testcontainers:\n` +
227
233
  ` bun add -d testcontainers pg @types/pg`);
228
234
  });
229
235
  // pg's Pool — the Kysely PostgresDialect accepts this directly.
236
+ // biome-ignore lint/suspicious/noExplicitAny: legacy any; tracked in hardening plan item H-01
230
237
  return new pg.Pool({ connectionString });
231
238
  }
232
239
  /**
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=client.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.test.d.ts","sourceRoot":"","sources":["../../src/tests/client.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,55 @@
1
+ import { describe, it, expect, afterEach } from 'bun:test';
2
+ import { createZveltioClient } from '../client.js';
3
+ const realFetch = globalThis.fetch;
4
+ afterEach(() => {
5
+ globalThis.fetch = realFetch;
6
+ });
7
+ /** Records the URL each call is given and answers `{}` without a network hop. */
8
+ function captureUrls() {
9
+ const urls = [];
10
+ globalThis.fetch = (async (input) => {
11
+ urls.push(String(input));
12
+ return new Response('{}', { status: 200, headers: { 'content-type': 'application/json' } });
13
+ });
14
+ return urls;
15
+ }
16
+ describe('ZveltioClient URL building', () => {
17
+ it('encodes a record id into a single path segment', async () => {
18
+ const urls = captureUrls();
19
+ const client = createZveltioClient({ baseUrl: 'https://engine.test' });
20
+ const products = client.collection('products');
21
+ await products.get('a/b');
22
+ await products.update('a/b', { name: 'x' });
23
+ await products.delete('a/b');
24
+ expect(urls).toEqual([
25
+ 'https://engine.test/api/data/products/a%2Fb',
26
+ 'https://engine.test/api/data/products/a%2Fb',
27
+ 'https://engine.test/api/data/products/a%2Fb',
28
+ ]);
29
+ });
30
+ it('encodes the storage folder into the query string', async () => {
31
+ const urls = captureUrls();
32
+ const client = createZveltioClient({ baseUrl: 'https://engine.test' });
33
+ await client.storage.list('invoices&limit=9999');
34
+ expect(urls).toEqual(['https://engine.test/api/storage?folder_id=invoices%26limit%3D9999']);
35
+ });
36
+ it('sends the upload folder under the field name the engine reads', async () => {
37
+ let body;
38
+ globalThis.fetch = (async (_input, init) => {
39
+ body = init?.body;
40
+ return new Response('{}', { status: 200, headers: { 'content-type': 'application/json' } });
41
+ });
42
+ const client = createZveltioClient({ baseUrl: 'https://engine.test' });
43
+ await client.storage.upload(new File(['x'], 'a.txt'), 'folder-uuid');
44
+ expect(body?.get('folder_id')).toBe('folder-uuid');
45
+ expect(body?.has('folder')).toBe(false);
46
+ });
47
+ it('leaves an ordinary id and collection name untouched', async () => {
48
+ const urls = captureUrls();
49
+ const client = createZveltioClient({ baseUrl: 'https://engine.test/' });
50
+ await client.collection('order_items').get('018f2a1e-0000-7000-8000-000000000000');
51
+ expect(urls).toEqual([
52
+ 'https://engine.test/api/data/order_items/018f2a1e-0000-7000-8000-000000000000',
53
+ ]);
54
+ });
55
+ });
@@ -0,0 +1,2 @@
1
+ import './setup';
2
+ //# sourceMappingURL=crdt-orset.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crdt-orset.test.d.ts","sourceRoot":"","sources":["../../src/tests/crdt-orset.test.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,CAAC"}
@@ -0,0 +1,23 @@
1
+ import './setup';
2
+ import { describe, it, expect } from 'bun:test';
3
+ import { orSetAdd, orSetRemove, orSetMerge, orSetValues } from '../crdt.js';
4
+ describe('OR-Set — removal survives a merge with a stale replica', () => {
5
+ it('does not resurrect an element that one replica removed', () => {
6
+ const added = orSetAdd([], 'x');
7
+ const uid = added[0].uid;
8
+ const stale = [...added]; // a replica that never saw the removal
9
+ const removed = orSetRemove(added, uid);
10
+ const first = orSetMerge(removed, stale);
11
+ expect(orSetValues(first)).toEqual([]);
12
+ // The stale replica syncs again. Without its tombstone the merged set has
13
+ // no record of the removal, so the element reads as a fresh addition.
14
+ const second = orSetMerge(first, stale);
15
+ expect(orSetValues(second), 'the removed element came back').toEqual([]);
16
+ });
17
+ it('an element added after a removal is kept', () => {
18
+ const first = orSetAdd([], 'x');
19
+ const removed = orSetRemove(first, first[0].uid);
20
+ const readded = orSetAdd(removed, 'x');
21
+ expect(orSetValues(orSetMerge(readded, removed))).toEqual(['x']);
22
+ });
23
+ });