@anfenn/dync 1.0.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.
Files changed (108) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +212 -0
  3. package/dist/capacitor.cjs +228 -0
  4. package/dist/capacitor.cjs.map +1 -0
  5. package/dist/capacitor.d.cts +62 -0
  6. package/dist/capacitor.d.ts +62 -0
  7. package/dist/capacitor.js +9 -0
  8. package/dist/capacitor.js.map +1 -0
  9. package/dist/chunk-LGHOZECP.js +3884 -0
  10. package/dist/chunk-LGHOZECP.js.map +1 -0
  11. package/dist/chunk-SQB6E7V2.js +191 -0
  12. package/dist/chunk-SQB6E7V2.js.map +1 -0
  13. package/dist/dexie-Bv-fV10P.d.cts +444 -0
  14. package/dist/dexie-DJFApKsM.d.ts +444 -0
  15. package/dist/dexie.cjs +381 -0
  16. package/dist/dexie.cjs.map +1 -0
  17. package/dist/dexie.d.cts +3 -0
  18. package/dist/dexie.d.ts +3 -0
  19. package/dist/dexie.js +343 -0
  20. package/dist/dexie.js.map +1 -0
  21. package/dist/expoSqlite.cjs +98 -0
  22. package/dist/expoSqlite.cjs.map +1 -0
  23. package/dist/expoSqlite.d.cts +17 -0
  24. package/dist/expoSqlite.d.ts +17 -0
  25. package/dist/expoSqlite.js +61 -0
  26. package/dist/expoSqlite.js.map +1 -0
  27. package/dist/index.cjs +3916 -0
  28. package/dist/index.cjs.map +1 -0
  29. package/dist/index.d.cts +8 -0
  30. package/dist/index.d.ts +8 -0
  31. package/dist/index.js +20 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/index.shared-CPIge2ZM.d.ts +234 -0
  34. package/dist/index.shared-YSn6c01d.d.cts +234 -0
  35. package/dist/node.cjs +126 -0
  36. package/dist/node.cjs.map +1 -0
  37. package/dist/node.d.cts +80 -0
  38. package/dist/node.d.ts +80 -0
  39. package/dist/node.js +89 -0
  40. package/dist/node.js.map +1 -0
  41. package/dist/react/index.cjs +1754 -0
  42. package/dist/react/index.cjs.map +1 -0
  43. package/dist/react/index.d.cts +40 -0
  44. package/dist/react/index.d.ts +40 -0
  45. package/dist/react/index.js +78 -0
  46. package/dist/react/index.js.map +1 -0
  47. package/dist/types-CSbIAfu2.d.cts +46 -0
  48. package/dist/types-CSbIAfu2.d.ts +46 -0
  49. package/dist/wa-sqlite.cjs +318 -0
  50. package/dist/wa-sqlite.cjs.map +1 -0
  51. package/dist/wa-sqlite.d.cts +175 -0
  52. package/dist/wa-sqlite.d.ts +175 -0
  53. package/dist/wa-sqlite.js +281 -0
  54. package/dist/wa-sqlite.js.map +1 -0
  55. package/package.json +171 -0
  56. package/src/addVisibilityChangeListener.native.ts +33 -0
  57. package/src/addVisibilityChangeListener.ts +24 -0
  58. package/src/capacitor.ts +4 -0
  59. package/src/core/StateManager.ts +272 -0
  60. package/src/core/firstLoad.ts +332 -0
  61. package/src/core/pullOperations.ts +212 -0
  62. package/src/core/pushOperations.ts +290 -0
  63. package/src/core/tableEnhancers.ts +457 -0
  64. package/src/core/types.ts +3 -0
  65. package/src/createLocalId.native.ts +8 -0
  66. package/src/createLocalId.ts +6 -0
  67. package/src/dexie.ts +2 -0
  68. package/src/expoSqlite.ts +2 -0
  69. package/src/helpers.ts +87 -0
  70. package/src/index.native.ts +28 -0
  71. package/src/index.shared.ts +613 -0
  72. package/src/index.ts +28 -0
  73. package/src/logger.ts +26 -0
  74. package/src/node.ts +4 -0
  75. package/src/react/index.ts +2 -0
  76. package/src/react/useDync.ts +156 -0
  77. package/src/storage/dexie/DexieAdapter.ts +72 -0
  78. package/src/storage/dexie/DexieQueryContext.ts +14 -0
  79. package/src/storage/dexie/DexieStorageCollection.ts +124 -0
  80. package/src/storage/dexie/DexieStorageTable.ts +123 -0
  81. package/src/storage/dexie/DexieStorageWhereClause.ts +103 -0
  82. package/src/storage/dexie/helpers.ts +1 -0
  83. package/src/storage/dexie/index.ts +7 -0
  84. package/src/storage/memory/MemoryAdapter.ts +55 -0
  85. package/src/storage/memory/MemoryCollection.ts +215 -0
  86. package/src/storage/memory/MemoryQueryContext.ts +14 -0
  87. package/src/storage/memory/MemoryTable.ts +336 -0
  88. package/src/storage/memory/MemoryWhereClause.ts +134 -0
  89. package/src/storage/memory/index.ts +7 -0
  90. package/src/storage/memory/types.ts +24 -0
  91. package/src/storage/sqlite/SQLiteAdapter.ts +564 -0
  92. package/src/storage/sqlite/SQLiteCollection.ts +294 -0
  93. package/src/storage/sqlite/SQLiteTable.ts +604 -0
  94. package/src/storage/sqlite/SQLiteWhereClause.ts +341 -0
  95. package/src/storage/sqlite/SqliteQueryContext.ts +30 -0
  96. package/src/storage/sqlite/drivers/BetterSqlite3Driver.ts +156 -0
  97. package/src/storage/sqlite/drivers/CapacitorFastSqlDriver.ts +114 -0
  98. package/src/storage/sqlite/drivers/CapacitorSQLiteDriver.ts +137 -0
  99. package/src/storage/sqlite/drivers/ExpoSQLiteDriver.native.ts +67 -0
  100. package/src/storage/sqlite/drivers/WaSqliteDriver.ts +537 -0
  101. package/src/storage/sqlite/drivers/wa-sqlite-vfs.d.ts +46 -0
  102. package/src/storage/sqlite/helpers.ts +144 -0
  103. package/src/storage/sqlite/index.ts +11 -0
  104. package/src/storage/sqlite/schema.ts +44 -0
  105. package/src/storage/sqlite/types.ts +164 -0
  106. package/src/storage/types.ts +112 -0
  107. package/src/types.ts +186 -0
  108. package/src/wa-sqlite.ts +4 -0
@@ -0,0 +1,1754 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/react/index.ts
21
+ var react_exports = {};
22
+ __export(react_exports, {
23
+ makeDync: () => makeDync
24
+ });
25
+ module.exports = __toCommonJS(react_exports);
26
+
27
+ // src/react/useDync.ts
28
+ var import_react = require("react");
29
+
30
+ // src/logger.ts
31
+ function newLogger(base, min) {
32
+ const order = {
33
+ debug: 10,
34
+ info: 20,
35
+ warn: 30,
36
+ error: 40,
37
+ none: 100
38
+ };
39
+ const threshold = order[min];
40
+ const enabled = (lvl) => order[lvl] >= threshold;
41
+ return {
42
+ debug: (...a) => enabled("debug") && base.debug?.(...a),
43
+ info: (...a) => enabled("info") && base.info?.(...a),
44
+ warn: (...a) => enabled("warn") && base.warn?.(...a),
45
+ error: (...a) => enabled("error") && base.error?.(...a)
46
+ };
47
+ }
48
+
49
+ // src/types.ts
50
+ var SERVER_PK = "id";
51
+ var LOCAL_PK = "_localId";
52
+ var UPDATED_AT = "updated_at";
53
+
54
+ // src/createLocalId.ts
55
+ function createLocalId() {
56
+ if (typeof globalThis.crypto?.randomUUID === "function") {
57
+ return globalThis.crypto.randomUUID();
58
+ }
59
+ throw new Error("createLocalId(): crypto.randomUUID is not available");
60
+ }
61
+
62
+ // src/helpers.ts
63
+ function sleep(ms, signal) {
64
+ return new Promise((resolve) => {
65
+ if (signal?.aborted) {
66
+ resolve();
67
+ return;
68
+ }
69
+ const timer = setTimeout(resolve, ms);
70
+ signal?.addEventListener("abort", () => {
71
+ clearTimeout(timer);
72
+ resolve();
73
+ });
74
+ });
75
+ }
76
+ function orderFor(a) {
77
+ switch (a) {
78
+ case "create" /* Create */:
79
+ return 1;
80
+ case "update" /* Update */:
81
+ return 2;
82
+ case "remove" /* Remove */:
83
+ return 3;
84
+ }
85
+ }
86
+ function omitFields(item, fields) {
87
+ const result = { ...item };
88
+ for (const k of fields) delete result[k];
89
+ return result;
90
+ }
91
+ function deleteKeyIfEmptyObject(obj, key) {
92
+ if (obj[key] && Object.keys(obj[key]).length === 0) {
93
+ delete obj[key];
94
+ }
95
+ }
96
+
97
+ // src/addVisibilityChangeListener.ts
98
+ function addVisibilityChangeListener(add, currentSubscription, onVisibilityChange) {
99
+ if (add && !currentSubscription) {
100
+ const handler = () => {
101
+ onVisibilityChange(document.visibilityState === "visible");
102
+ };
103
+ document.addEventListener("visibilitychange", handler);
104
+ return {
105
+ remove: () => {
106
+ document.removeEventListener("visibilitychange", handler);
107
+ }
108
+ };
109
+ } else if (!add && currentSubscription) {
110
+ currentSubscription.remove();
111
+ return void 0;
112
+ }
113
+ return currentSubscription;
114
+ }
115
+
116
+ // src/core/StateManager.ts
117
+ var LOCAL_ONLY_SYNC_FIELDS = [LOCAL_PK, UPDATED_AT];
118
+ var DYNC_STATE_TABLE = "_dync_state";
119
+ var SYNC_STATE_KEY = "sync_state";
120
+ var DEFAULT_STATE = {
121
+ firstLoadDone: false,
122
+ pendingChanges: [],
123
+ lastPulled: {}
124
+ };
125
+ var StateManager = class {
126
+ persistedState;
127
+ syncStatus;
128
+ listeners = /* @__PURE__ */ new Set();
129
+ storageAdapter;
130
+ hydrated = false;
131
+ constructor(ctx) {
132
+ this.storageAdapter = ctx.storageAdapter;
133
+ this.persistedState = DEFAULT_STATE;
134
+ this.syncStatus = ctx.initialStatus ?? "disabled";
135
+ }
136
+ /**
137
+ * Load state from the database. Called after stores() defines the schema.
138
+ */
139
+ async hydrate() {
140
+ if (this.hydrated) return;
141
+ if (!this.storageAdapter) {
142
+ throw new Error("Cannot hydrate state without a storage adapter");
143
+ }
144
+ const table = this.storageAdapter.table(DYNC_STATE_TABLE);
145
+ const row = await table.get(SYNC_STATE_KEY);
146
+ if (row?.value) {
147
+ this.persistedState = parseStoredState(row.value);
148
+ }
149
+ this.hydrated = true;
150
+ this.emit();
151
+ }
152
+ emit() {
153
+ this.listeners.forEach((fn) => fn(this.getSyncState()));
154
+ }
155
+ async persist() {
156
+ if (!this.hydrated || !this.storageAdapter) return;
157
+ this.emit();
158
+ const table = this.storageAdapter.table(DYNC_STATE_TABLE);
159
+ await table.put({ [LOCAL_PK]: SYNC_STATE_KEY, value: JSON.stringify(this.persistedState) });
160
+ }
161
+ getState() {
162
+ return clonePersistedState(this.persistedState);
163
+ }
164
+ setState(setterOrState) {
165
+ this.persistedState = resolveNextState(this.persistedState, setterOrState);
166
+ return this.persist();
167
+ }
168
+ /**
169
+ * Set error in memory only without persisting to database.
170
+ * Used when the database itself failed to open.
171
+ */
172
+ setErrorInMemory(error) {
173
+ this.persistedState = { ...this.persistedState, error };
174
+ this.emit();
175
+ }
176
+ addPendingChange(change) {
177
+ const next = clonePersistedState(this.persistedState);
178
+ const queueItem = next.pendingChanges.find((p) => p.localId === change.localId && p.stateKey === change.stateKey);
179
+ const omittedChanges = omitFields(change.changes, LOCAL_ONLY_SYNC_FIELDS);
180
+ const omittedBefore = omitFields(change.before, LOCAL_ONLY_SYNC_FIELDS);
181
+ const omittedAfter = omitFields(change.after, LOCAL_ONLY_SYNC_FIELDS);
182
+ const hasChanges = Object.keys(omittedChanges || {}).length > 0;
183
+ const action = change.action;
184
+ if (queueItem) {
185
+ if (queueItem.action === "remove" /* Remove */) {
186
+ return Promise.resolve();
187
+ }
188
+ queueItem.version += 1;
189
+ if (action === "remove" /* Remove */) {
190
+ queueItem.action = "remove" /* Remove */;
191
+ } else if (hasChanges) {
192
+ queueItem.changes = { ...queueItem.changes, ...omittedChanges };
193
+ queueItem.after = { ...queueItem.after, ...omittedAfter };
194
+ }
195
+ } else if (action === "remove" /* Remove */ || hasChanges) {
196
+ next.pendingChanges = [...next.pendingChanges];
197
+ next.pendingChanges.push({
198
+ action,
199
+ stateKey: change.stateKey,
200
+ localId: change.localId,
201
+ id: change.id,
202
+ version: 1,
203
+ changes: omittedChanges,
204
+ before: omittedBefore,
205
+ after: omittedAfter
206
+ });
207
+ }
208
+ this.persistedState = next;
209
+ return this.persist();
210
+ }
211
+ samePendingVersion(stateKey, localId, version) {
212
+ return this.persistedState.pendingChanges.find((p) => p.localId === localId && p.stateKey === stateKey)?.version === version;
213
+ }
214
+ removePendingChange(localId, stateKey) {
215
+ const next = clonePersistedState(this.persistedState);
216
+ next.pendingChanges = next.pendingChanges.filter((p) => !(p.localId === localId && p.stateKey === stateKey));
217
+ this.persistedState = next;
218
+ return this.persist();
219
+ }
220
+ updatePendingChange(stateKey, localId, action, id) {
221
+ const next = clonePersistedState(this.persistedState);
222
+ const changeItem = next.pendingChanges.find((p) => p.stateKey === stateKey && p.localId === localId);
223
+ if (changeItem) {
224
+ changeItem.action = action;
225
+ if (id) changeItem.id = id;
226
+ this.persistedState = next;
227
+ return this.persist();
228
+ }
229
+ return Promise.resolve();
230
+ }
231
+ setPendingChangeBefore(stateKey, localId, before) {
232
+ const next = clonePersistedState(this.persistedState);
233
+ const changeItem = next.pendingChanges.find((p) => p.stateKey === stateKey && p.localId === localId);
234
+ if (changeItem) {
235
+ changeItem.before = { ...changeItem.before ?? {}, ...before };
236
+ this.persistedState = next;
237
+ return this.persist();
238
+ }
239
+ return Promise.resolve();
240
+ }
241
+ hasConflicts(localId) {
242
+ return Boolean(this.persistedState.conflicts?.[localId]);
243
+ }
244
+ getSyncStatus() {
245
+ return this.syncStatus;
246
+ }
247
+ setSyncStatus(status) {
248
+ if (this.syncStatus === status) return;
249
+ this.syncStatus = status;
250
+ this.emit();
251
+ }
252
+ getSyncState() {
253
+ return buildSyncState(this.persistedState, this.syncStatus, this.hydrated);
254
+ }
255
+ subscribe(listener) {
256
+ this.listeners.add(listener);
257
+ return () => this.listeners.delete(listener);
258
+ }
259
+ };
260
+ function parseStoredState(stored) {
261
+ const parsed = JSON.parse(stored);
262
+ if (parsed.pendingChanges) {
263
+ parsed.pendingChanges = parsed.pendingChanges.map((change) => ({
264
+ ...change,
265
+ timestamp: change.timestamp ? new Date(change.timestamp) : change.timestamp
266
+ }));
267
+ }
268
+ return parsed;
269
+ }
270
+ function resolveNextState(current, setterOrState) {
271
+ if (typeof setterOrState === "function") {
272
+ return { ...current, ...setterOrState(clonePersistedState(current)) };
273
+ }
274
+ return { ...current, ...setterOrState };
275
+ }
276
+ function buildSyncState(state, status, hydrated) {
277
+ const persisted = clonePersistedState(state);
278
+ const syncState = {
279
+ ...persisted,
280
+ status,
281
+ hydrated
282
+ };
283
+ deleteKeyIfEmptyObject(syncState, "conflicts");
284
+ return syncState;
285
+ }
286
+ function clonePersistedState(state) {
287
+ return {
288
+ ...state,
289
+ pendingChanges: state.pendingChanges.map((change) => ({
290
+ ...change,
291
+ changes: cloneRecord(change.changes),
292
+ before: cloneRecord(change.before),
293
+ after: cloneRecord(change.after)
294
+ })),
295
+ lastPulled: { ...state.lastPulled },
296
+ conflicts: cloneConflicts(state.conflicts)
297
+ };
298
+ }
299
+ function cloneConflicts(conflicts) {
300
+ if (!conflicts) return void 0;
301
+ const next = {};
302
+ for (const [key, value] of Object.entries(conflicts)) {
303
+ next[key] = {
304
+ stateKey: value.stateKey,
305
+ fields: value.fields.map((field) => ({ ...field }))
306
+ };
307
+ }
308
+ return next;
309
+ }
310
+ function cloneRecord(record) {
311
+ if (!record) return record;
312
+ return { ...record };
313
+ }
314
+
315
+ // src/core/tableEnhancers.ts
316
+ function wrapWithMutationEmitter(table, tableName, emitMutation) {
317
+ const rawAdd = table.raw.add;
318
+ const rawPut = table.raw.put;
319
+ const rawUpdate = table.raw.update;
320
+ const rawDelete = table.raw.delete;
321
+ const rawBulkAdd = table.raw.bulkAdd;
322
+ const rawBulkPut = table.raw.bulkPut;
323
+ const rawBulkUpdate = table.raw.bulkUpdate;
324
+ const rawBulkDelete = table.raw.bulkDelete;
325
+ const rawClear = table.raw.clear;
326
+ table.add = async (item) => {
327
+ const result = await rawAdd(item);
328
+ emitMutation({ type: "add", tableName, keys: [result] });
329
+ return result;
330
+ };
331
+ table.put = async (item) => {
332
+ const result = await rawPut(item);
333
+ emitMutation({ type: "update", tableName, keys: [result] });
334
+ return result;
335
+ };
336
+ table.update = async (key, changes) => {
337
+ const result = await rawUpdate(key, changes);
338
+ if (result > 0) {
339
+ emitMutation({ type: "update", tableName, keys: [key] });
340
+ }
341
+ return result;
342
+ };
343
+ table.delete = async (key) => {
344
+ await rawDelete(key);
345
+ emitMutation({ type: "delete", tableName, keys: [key] });
346
+ };
347
+ table.bulkAdd = async (items) => {
348
+ const result = await rawBulkAdd(items);
349
+ if (items.length > 0) {
350
+ emitMutation({ type: "add", tableName });
351
+ }
352
+ return result;
353
+ };
354
+ table.bulkPut = async (items) => {
355
+ const result = await rawBulkPut(items);
356
+ if (items.length > 0) {
357
+ emitMutation({ type: "update", tableName });
358
+ }
359
+ return result;
360
+ };
361
+ table.bulkUpdate = async (keysAndChanges) => {
362
+ const result = await rawBulkUpdate(keysAndChanges);
363
+ if (result > 0) {
364
+ emitMutation({ type: "update", tableName, keys: keysAndChanges.map((kc) => kc.key) });
365
+ }
366
+ return result;
367
+ };
368
+ table.bulkDelete = async (keys) => {
369
+ await rawBulkDelete(keys);
370
+ if (keys.length > 0) {
371
+ emitMutation({ type: "delete", tableName });
372
+ }
373
+ };
374
+ table.clear = async () => {
375
+ await rawClear();
376
+ emitMutation({ type: "delete", tableName });
377
+ };
378
+ }
379
+ function setupEnhancedTables({ owner, tableCache, enhancedTables, getTable }, tableNames) {
380
+ for (const tableName of tableNames) {
381
+ tableCache.delete(tableName);
382
+ enhancedTables.delete(tableName);
383
+ if (!Object.prototype.hasOwnProperty.call(owner, tableName)) {
384
+ Object.defineProperty(owner, tableName, {
385
+ get: () => getTable(tableName),
386
+ enumerable: true,
387
+ configurable: true
388
+ });
389
+ }
390
+ }
391
+ }
392
+ function enhanceSyncTable({ table, tableName, withTransaction, state, enhancedTables, emitMutation }) {
393
+ const rawAdd = table.raw.add;
394
+ const rawPut = table.raw.put;
395
+ const rawUpdate = table.raw.update;
396
+ const rawDelete = table.raw.delete;
397
+ const rawBulkAdd = table.raw.bulkAdd;
398
+ const rawBulkPut = table.raw.bulkPut;
399
+ const rawBulkUpdate = table.raw.bulkUpdate;
400
+ const rawBulkDelete = table.raw.bulkDelete;
401
+ const rawClear = table.raw.clear;
402
+ const wrappedAdd = async (item) => {
403
+ let localId = item._localId;
404
+ if (!localId) localId = createLocalId();
405
+ const syncedItem = {
406
+ ...item,
407
+ _localId: localId,
408
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
409
+ };
410
+ let result;
411
+ await withTransaction("rw", [tableName, DYNC_STATE_TABLE], async () => {
412
+ result = await rawAdd(syncedItem);
413
+ await state.addPendingChange({
414
+ action: "create" /* Create */,
415
+ stateKey: tableName,
416
+ localId,
417
+ changes: syncedItem,
418
+ before: null,
419
+ after: syncedItem
420
+ });
421
+ });
422
+ emitMutation({ type: "add", tableName, keys: [localId] });
423
+ return result;
424
+ };
425
+ const wrappedPut = async (item) => {
426
+ let localId = item._localId;
427
+ if (!localId) localId = createLocalId();
428
+ const syncedItem = {
429
+ ...item,
430
+ _localId: localId,
431
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
432
+ };
433
+ let result;
434
+ let isUpdate = false;
435
+ let existingRecord;
436
+ await withTransaction("rw", [tableName, DYNC_STATE_TABLE], async (tables) => {
437
+ const txTable = tables[tableName];
438
+ existingRecord = await txTable.get(localId);
439
+ isUpdate = !!existingRecord;
440
+ result = await rawPut(syncedItem);
441
+ await state.addPendingChange({
442
+ action: isUpdate ? "update" /* Update */ : "create" /* Create */,
443
+ stateKey: tableName,
444
+ localId,
445
+ id: existingRecord?.id,
446
+ changes: syncedItem,
447
+ before: existingRecord ?? null,
448
+ after: syncedItem
449
+ });
450
+ });
451
+ emitMutation({ type: isUpdate ? "update" : "add", tableName, keys: [localId] });
452
+ return result;
453
+ };
454
+ const wrappedUpdate = async (key, changes) => {
455
+ const updatedChanges = {
456
+ ...changes,
457
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
458
+ };
459
+ let result = 0;
460
+ await withTransaction("rw", [tableName, DYNC_STATE_TABLE], async (tables) => {
461
+ const txTable = tables[tableName];
462
+ const record = await txTable.get(key);
463
+ if (!record) {
464
+ throw new Error(`Record with key=${key} not found`);
465
+ }
466
+ result = await rawUpdate(key, updatedChanges) ?? 0;
467
+ if (result > 0) {
468
+ await state.addPendingChange({
469
+ action: "update" /* Update */,
470
+ stateKey: tableName,
471
+ localId: key,
472
+ id: record.id,
473
+ changes: updatedChanges,
474
+ before: record,
475
+ after: { ...record, ...updatedChanges }
476
+ });
477
+ }
478
+ });
479
+ if (result > 0) {
480
+ emitMutation({ type: "update", tableName, keys: [key] });
481
+ }
482
+ return result;
483
+ };
484
+ const wrappedDelete = async (key) => {
485
+ let deletedLocalId;
486
+ await withTransaction("rw", [tableName, DYNC_STATE_TABLE], async (tables) => {
487
+ const txTable = tables[tableName];
488
+ const record = await txTable.get(key);
489
+ await rawDelete(key);
490
+ if (record) {
491
+ deletedLocalId = record._localId;
492
+ await state.addPendingChange({
493
+ action: "remove" /* Remove */,
494
+ stateKey: tableName,
495
+ localId: record._localId,
496
+ id: record.id,
497
+ changes: null,
498
+ before: record
499
+ });
500
+ }
501
+ });
502
+ if (deletedLocalId) {
503
+ emitMutation({ type: "delete", tableName, keys: [deletedLocalId] });
504
+ }
505
+ };
506
+ const wrappedBulkAdd = async (items) => {
507
+ if (items.length === 0) return;
508
+ const now = (/* @__PURE__ */ new Date()).toISOString();
509
+ const syncedItems = items.map((item) => {
510
+ const localId = item._localId || createLocalId();
511
+ return {
512
+ ...item,
513
+ _localId: localId,
514
+ updated_at: now
515
+ };
516
+ });
517
+ let result;
518
+ await withTransaction("rw", [tableName, DYNC_STATE_TABLE], async () => {
519
+ result = await rawBulkAdd(syncedItems);
520
+ for (const syncedItem of syncedItems) {
521
+ await state.addPendingChange({
522
+ action: "create" /* Create */,
523
+ stateKey: tableName,
524
+ localId: syncedItem._localId,
525
+ changes: syncedItem,
526
+ before: null,
527
+ after: syncedItem
528
+ });
529
+ }
530
+ });
531
+ emitMutation({ type: "add", tableName, keys: syncedItems.map((i) => i._localId) });
532
+ return result;
533
+ };
534
+ const wrappedBulkPut = async (items) => {
535
+ if (items.length === 0) return;
536
+ const now = (/* @__PURE__ */ new Date()).toISOString();
537
+ const syncedItems = items.map((item) => {
538
+ const localId = item._localId || createLocalId();
539
+ return {
540
+ ...item,
541
+ _localId: localId,
542
+ updated_at: now
543
+ };
544
+ });
545
+ const localIds = syncedItems.map((i) => i._localId);
546
+ let result;
547
+ await withTransaction("rw", [tableName, DYNC_STATE_TABLE], async (tables) => {
548
+ const txTable = tables[tableName];
549
+ const existingRecords = await txTable.bulkGet(localIds);
550
+ const existingMap = /* @__PURE__ */ new Map();
551
+ for (let i = 0; i < localIds.length; i++) {
552
+ if (existingRecords[i]) {
553
+ existingMap.set(localIds[i], existingRecords[i]);
554
+ }
555
+ }
556
+ result = await rawBulkPut(syncedItems);
557
+ for (const syncedItem of syncedItems) {
558
+ const existing = existingMap.get(syncedItem._localId);
559
+ await state.addPendingChange({
560
+ action: existing ? "update" /* Update */ : "create" /* Create */,
561
+ stateKey: tableName,
562
+ localId: syncedItem._localId,
563
+ id: existing?.id,
564
+ changes: syncedItem,
565
+ before: existing ?? null,
566
+ after: syncedItem
567
+ });
568
+ }
569
+ });
570
+ emitMutation({ type: "update", tableName, keys: localIds });
571
+ return result;
572
+ };
573
+ const wrappedBulkUpdate = async (keysAndChanges) => {
574
+ if (keysAndChanges.length === 0) return 0;
575
+ const now = (/* @__PURE__ */ new Date()).toISOString();
576
+ const updatedKeysAndChanges = keysAndChanges.map(({ key, changes }) => ({
577
+ key,
578
+ changes: {
579
+ ...changes,
580
+ updated_at: now
581
+ }
582
+ }));
583
+ let result = 0;
584
+ const updatedKeys = [];
585
+ await withTransaction("rw", [tableName, DYNC_STATE_TABLE], async (tables) => {
586
+ const txTable = tables[tableName];
587
+ const keys = updatedKeysAndChanges.map((kc) => kc.key);
588
+ const records = await txTable.bulkGet(keys);
589
+ const recordMap = /* @__PURE__ */ new Map();
590
+ for (let i = 0; i < keys.length; i++) {
591
+ if (records[i]) {
592
+ recordMap.set(String(keys[i]), records[i]);
593
+ }
594
+ }
595
+ result = await rawBulkUpdate(updatedKeysAndChanges);
596
+ for (const { key, changes } of updatedKeysAndChanges) {
597
+ const record = recordMap.get(String(key));
598
+ if (record) {
599
+ updatedKeys.push(record._localId);
600
+ await state.addPendingChange({
601
+ action: "update" /* Update */,
602
+ stateKey: tableName,
603
+ localId: record._localId,
604
+ id: record.id,
605
+ changes,
606
+ before: record,
607
+ after: { ...record, ...changes }
608
+ });
609
+ }
610
+ }
611
+ });
612
+ if (updatedKeys.length > 0) {
613
+ emitMutation({ type: "update", tableName, keys: updatedKeys });
614
+ }
615
+ return result;
616
+ };
617
+ const wrappedBulkDelete = async (keys) => {
618
+ if (keys.length === 0) return;
619
+ const deletedLocalIds = [];
620
+ await withTransaction("rw", [tableName, DYNC_STATE_TABLE], async (tables) => {
621
+ const txTable = tables[tableName];
622
+ const records = await txTable.bulkGet(keys);
623
+ await rawBulkDelete(keys);
624
+ for (const record of records) {
625
+ if (record) {
626
+ deletedLocalIds.push(record._localId);
627
+ await state.addPendingChange({
628
+ action: "remove" /* Remove */,
629
+ stateKey: tableName,
630
+ localId: record._localId,
631
+ id: record.id,
632
+ changes: null,
633
+ before: record
634
+ });
635
+ }
636
+ }
637
+ });
638
+ if (deletedLocalIds.length > 0) {
639
+ emitMutation({ type: "delete", tableName, keys: deletedLocalIds });
640
+ }
641
+ };
642
+ const wrappedClear = async () => {
643
+ const deletedLocalIds = [];
644
+ await withTransaction("rw", [tableName, DYNC_STATE_TABLE], async (tables) => {
645
+ const txTable = tables[tableName];
646
+ const allRecords = await txTable.toArray();
647
+ await rawClear();
648
+ for (const record of allRecords) {
649
+ if (record._localId) {
650
+ deletedLocalIds.push(record._localId);
651
+ await state.addPendingChange({
652
+ action: "remove" /* Remove */,
653
+ stateKey: tableName,
654
+ localId: record._localId,
655
+ id: record.id,
656
+ changes: null,
657
+ before: record
658
+ });
659
+ }
660
+ }
661
+ });
662
+ if (deletedLocalIds.length > 0) {
663
+ emitMutation({ type: "delete", tableName, keys: deletedLocalIds });
664
+ }
665
+ };
666
+ table.add = wrappedAdd;
667
+ table.put = wrappedPut;
668
+ table.update = wrappedUpdate;
669
+ table.delete = wrappedDelete;
670
+ table.bulkAdd = wrappedBulkAdd;
671
+ table.bulkPut = wrappedBulkPut;
672
+ table.bulkUpdate = wrappedBulkUpdate;
673
+ table.bulkDelete = wrappedBulkDelete;
674
+ table.clear = wrappedClear;
675
+ enhancedTables.add(tableName);
676
+ }
677
+
678
+ // src/core/pullOperations.ts
679
+ async function pullAll(ctx) {
680
+ let firstSyncError;
681
+ const changedTables = [];
682
+ for (const [stateKey, api] of Object.entries(ctx.syncApis)) {
683
+ try {
684
+ const lastPulled = ctx.state.getState().lastPulled[stateKey];
685
+ const since = lastPulled ? new Date(lastPulled) : /* @__PURE__ */ new Date(0);
686
+ ctx.logger.debug(`[dync] pull:start stateKey=${stateKey} since=${since.toISOString()}`);
687
+ const serverData = await api.list(since);
688
+ const changed = await processPullData(stateKey, serverData, since, ctx);
689
+ if (changed) changedTables.push(stateKey);
690
+ } catch (err) {
691
+ firstSyncError = firstSyncError ?? err;
692
+ ctx.logger.error(`[dync] pull:error stateKey=${stateKey}`, err);
693
+ }
694
+ }
695
+ return { error: firstSyncError, changedTables };
696
+ }
697
+ async function handleRemoteItemUpdate(table, stateKey, localItem, remote, ctx) {
698
+ const pendingChange = ctx.state.getState().pendingChanges.find((p) => p.stateKey === stateKey && p.localId === localItem._localId);
699
+ const conflictStrategy = ctx.conflictResolutionStrategy;
700
+ if (pendingChange) {
701
+ ctx.logger.debug(`[dync] pull:conflict-strategy:${conflictStrategy} stateKey=${stateKey} id=${remote.id}`);
702
+ switch (conflictStrategy) {
703
+ case "local-wins":
704
+ break;
705
+ case "remote-wins": {
706
+ const merged = { ...remote, _localId: localItem._localId };
707
+ await table.raw.update(localItem._localId, merged);
708
+ await ctx.state.removePendingChange(localItem._localId, stateKey);
709
+ break;
710
+ }
711
+ case "try-shallow-merge": {
712
+ const changes = pendingChange.changes || {};
713
+ const before = pendingChange.before || {};
714
+ const fields = Object.entries(changes).filter(([k, localValue]) => k in before && k in remote && before[k] !== remote[k] && localValue !== remote[k]).map(([key, localValue]) => ({ key, localValue, remoteValue: remote[key] }));
715
+ if (fields.length > 0) {
716
+ ctx.logger.warn(`[dync] pull:${conflictStrategy}:conflicts-found`, JSON.stringify(fields, null, 4));
717
+ await ctx.state.setState((syncState) => ({
718
+ ...syncState,
719
+ conflicts: {
720
+ ...syncState.conflicts || {},
721
+ [localItem._localId]: { stateKey, fields }
722
+ }
723
+ }));
724
+ } else {
725
+ const localChangedKeys = Object.keys(changes);
726
+ const preservedLocal = { _localId: localItem._localId };
727
+ for (const k of localChangedKeys) {
728
+ if (k in localItem) preservedLocal[k] = localItem[k];
729
+ }
730
+ const merged = { ...remote, ...preservedLocal };
731
+ await table.raw.update(localItem._localId, merged);
732
+ await ctx.state.setState((syncState) => {
733
+ const ss = { ...syncState };
734
+ delete ss.conflicts?.[localItem._localId];
735
+ return ss;
736
+ });
737
+ }
738
+ break;
739
+ }
740
+ }
741
+ } else {
742
+ const merged = { ...localItem, ...remote };
743
+ await table.raw.update(localItem._localId, merged);
744
+ ctx.logger.debug(`[dync] pull:merge-remote stateKey=${stateKey} id=${remote.id}`);
745
+ }
746
+ }
747
+ async function pullAllBatch(ctx) {
748
+ let firstSyncError;
749
+ const changedTables = [];
750
+ try {
751
+ const sinceMap = {};
752
+ for (const tableName of ctx.batchSync.syncTables) {
753
+ const lastPulled = ctx.state.getState().lastPulled[tableName];
754
+ sinceMap[tableName] = lastPulled ? new Date(lastPulled) : /* @__PURE__ */ new Date(0);
755
+ }
756
+ ctx.logger.debug(`[dync] pull:batch:start tables=${[...ctx.batchSync.syncTables].join(",")}`, sinceMap);
757
+ const serverDataByTable = await ctx.batchSync.pull(sinceMap);
758
+ for (const [stateKey, serverData] of Object.entries(serverDataByTable)) {
759
+ if (!ctx.batchSync.syncTables.includes(stateKey)) {
760
+ ctx.logger.warn(`[dync] pull:batch:unknown-table stateKey=${stateKey}`);
761
+ continue;
762
+ }
763
+ try {
764
+ const changed = await processPullData(stateKey, serverData, sinceMap[stateKey], ctx);
765
+ if (changed) changedTables.push(stateKey);
766
+ } catch (err) {
767
+ firstSyncError = firstSyncError ?? err;
768
+ ctx.logger.error(`[dync] pull:batch:error stateKey=${stateKey}`, err);
769
+ }
770
+ }
771
+ } catch (err) {
772
+ firstSyncError = err;
773
+ ctx.logger.error(`[dync] pull:batch:error`, err);
774
+ }
775
+ return { error: firstSyncError, changedTables };
776
+ }
777
+ async function processPullData(stateKey, serverData, since, ctx) {
778
+ if (!serverData?.length) return false;
779
+ ctx.logger.debug(`[dync] pull:process stateKey=${stateKey} count=${serverData.length}`);
780
+ let newest = since;
781
+ let hasChanges = false;
782
+ await ctx.withTransaction("rw", [stateKey, DYNC_STATE_TABLE], async (tables) => {
783
+ const txTable = tables[stateKey];
784
+ const pendingRemovalById = new Set(
785
+ ctx.state.getState().pendingChanges.filter((p) => p.stateKey === stateKey && p.action === "remove" /* Remove */).map((p) => p.id)
786
+ );
787
+ for (const remote of serverData) {
788
+ const remoteUpdated = new Date(remote.updated_at);
789
+ if (remoteUpdated > newest) newest = remoteUpdated;
790
+ if (pendingRemovalById.has(remote.id)) {
791
+ ctx.logger.debug(`[dync] pull:skip-pending-remove stateKey=${stateKey} id=${remote.id}`);
792
+ continue;
793
+ }
794
+ const localItem = await txTable.where("id").equals(remote.id).first();
795
+ if (remote.deleted) {
796
+ if (localItem) {
797
+ await txTable.raw.delete(localItem._localId);
798
+ ctx.logger.debug(`[dync] pull:remove stateKey=${stateKey} id=${remote.id}`);
799
+ hasChanges = true;
800
+ }
801
+ continue;
802
+ }
803
+ delete remote.deleted;
804
+ if (localItem) {
805
+ await handleRemoteItemUpdate(txTable, stateKey, localItem, remote, ctx);
806
+ hasChanges = true;
807
+ } else {
808
+ const newLocalItem = { ...remote, _localId: createLocalId() };
809
+ await txTable.raw.add(newLocalItem);
810
+ ctx.logger.debug(`[dync] pull:add stateKey=${stateKey} id=${remote.id}`);
811
+ hasChanges = true;
812
+ }
813
+ }
814
+ await ctx.state.setState((syncState) => ({
815
+ ...syncState,
816
+ lastPulled: {
817
+ ...syncState.lastPulled,
818
+ [stateKey]: newest.toISOString()
819
+ }
820
+ }));
821
+ });
822
+ return hasChanges;
823
+ }
824
+
825
+ // src/core/pushOperations.ts
826
+ async function handleRemoveSuccess(change, ctx) {
827
+ const { stateKey, localId, id } = change;
828
+ ctx.logger.debug(`[dync] push:remove:success stateKey=${stateKey} localId=${localId} id=${id}`);
829
+ await ctx.state.removePendingChange(localId, stateKey);
830
+ }
831
+ async function handleUpdateSuccess(change, ctx) {
832
+ const { stateKey, localId, version, changes } = change;
833
+ ctx.logger.debug(`[dync] push:update:success stateKey=${stateKey} localId=${localId} id=${change.id}`);
834
+ if (ctx.state.samePendingVersion(stateKey, localId, version)) {
835
+ await ctx.state.removePendingChange(localId, stateKey);
836
+ } else {
837
+ await ctx.state.setPendingChangeBefore(stateKey, localId, changes);
838
+ }
839
+ }
840
+ async function handleCreateSuccess(change, serverResult, ctx) {
841
+ const { stateKey, localId, version, changes, id } = change;
842
+ ctx.logger.debug(`[dync] push:create:success stateKey=${stateKey} localId=${localId} id=${id ?? serverResult.id}`);
843
+ await ctx.withTransaction("rw", [stateKey, DYNC_STATE_TABLE], async (tables) => {
844
+ const txTable = tables[stateKey];
845
+ const wasChanged = await txTable.raw.update(localId, serverResult) ?? 0;
846
+ if (wasChanged && ctx.state.samePendingVersion(stateKey, localId, version)) {
847
+ await ctx.state.removePendingChange(localId, stateKey);
848
+ } else {
849
+ const nextAction = wasChanged ? "update" /* Update */ : "remove" /* Remove */;
850
+ await ctx.state.updatePendingChange(stateKey, localId, nextAction, serverResult.id);
851
+ if (nextAction === "remove" /* Remove */) return;
852
+ }
853
+ });
854
+ const finalItem = { ...changes, ...serverResult, _localId: localId };
855
+ ctx.syncOptions.onAfterRemoteAdd?.(stateKey, finalItem);
856
+ }
857
+ async function pushAll(ctx) {
858
+ let firstSyncError;
859
+ const changesSnapshot = [...ctx.state.getState().pendingChanges].sort((a, b) => orderFor(a.action) - orderFor(b.action));
860
+ for (const change of changesSnapshot) {
861
+ try {
862
+ await pushOne(change, ctx);
863
+ } catch (err) {
864
+ firstSyncError = firstSyncError ?? err;
865
+ ctx.logger.error(`[dync] push:error change=${JSON.stringify(change)}`, err);
866
+ }
867
+ }
868
+ return firstSyncError;
869
+ }
870
+ async function pushOne(change, ctx) {
871
+ const api = ctx.syncApis[change.stateKey];
872
+ if (!api) return;
873
+ ctx.logger.debug(`[dync] push:attempt action=${change.action} stateKey=${change.stateKey} localId=${change.localId}`);
874
+ const { action, stateKey, localId, id, changes, after } = change;
875
+ switch (action) {
876
+ case "remove" /* Remove */:
877
+ if (!id) {
878
+ ctx.logger.warn(`[dync] push:remove:no-id stateKey=${stateKey} localId=${localId}`);
879
+ await ctx.state.removePendingChange(localId, stateKey);
880
+ return;
881
+ }
882
+ await api.remove(id);
883
+ await handleRemoveSuccess(change, ctx);
884
+ break;
885
+ case "update" /* Update */: {
886
+ if (ctx.state.hasConflicts(localId)) {
887
+ ctx.logger.warn(`[dync] push:update:skipping-with-conflicts stateKey=${stateKey} localId=${localId} id=${id}`);
888
+ return;
889
+ }
890
+ const exists = await api.update(id, changes, after);
891
+ if (exists) {
892
+ await handleUpdateSuccess(change, ctx);
893
+ } else {
894
+ await handleMissingRemoteRecord(change, ctx);
895
+ }
896
+ break;
897
+ }
898
+ case "create" /* Create */: {
899
+ const result = await api.add(changes);
900
+ if (result) {
901
+ await handleCreateSuccess(change, result, ctx);
902
+ } else {
903
+ ctx.logger.warn(`[dync] push:create:no-result stateKey=${stateKey} localId=${localId} id=${id}`);
904
+ if (ctx.state.samePendingVersion(stateKey, localId, change.version)) {
905
+ await ctx.state.removePendingChange(localId, stateKey);
906
+ }
907
+ }
908
+ break;
909
+ }
910
+ }
911
+ }
912
+ async function handleMissingRemoteRecord(change, ctx) {
913
+ const { stateKey, localId } = change;
914
+ const strategy = ctx.syncOptions.missingRemoteRecordDuringUpdateStrategy;
915
+ let localItem;
916
+ await ctx.withTransaction("rw", [stateKey, DYNC_STATE_TABLE], async (tables) => {
917
+ const txTable = tables[stateKey];
918
+ localItem = await txTable.get(localId);
919
+ if (!localItem) {
920
+ ctx.logger.warn(`[dync] push:missing-remote:no-local-item stateKey=${stateKey} localId=${localId}`);
921
+ await ctx.state.removePendingChange(localId, stateKey);
922
+ return;
923
+ }
924
+ switch (strategy) {
925
+ case "delete-local-record":
926
+ await txTable.raw.delete(localId);
927
+ ctx.logger.debug(`[dync] push:missing-remote:${strategy} stateKey=${stateKey} id=${localItem.id}`);
928
+ break;
929
+ case "insert-remote-record": {
930
+ const newItem = {
931
+ ...localItem,
932
+ _localId: createLocalId(),
933
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
934
+ };
935
+ await txTable.raw.add(newItem);
936
+ await txTable.raw.delete(localId);
937
+ await ctx.state.addPendingChange({
938
+ action: "create" /* Create */,
939
+ stateKey,
940
+ localId: newItem._localId,
941
+ changes: newItem,
942
+ before: null
943
+ });
944
+ ctx.logger.debug(`[dync] push:missing-remote:${strategy} stateKey=${stateKey} id=${newItem.id}`);
945
+ break;
946
+ }
947
+ case "ignore":
948
+ ctx.logger.debug(`[dync] push:missing-remote:${strategy} stateKey=${stateKey} id=${localItem.id}`);
949
+ break;
950
+ default:
951
+ ctx.logger.error(`[dync] push:missing-remote:unknown-strategy stateKey=${stateKey} id=${localItem.id} strategy=${strategy}`);
952
+ break;
953
+ }
954
+ await ctx.state.removePendingChange(localId, stateKey);
955
+ });
956
+ ctx.syncOptions.onAfterMissingRemoteRecordDuringUpdate?.(strategy, localItem);
957
+ }
958
+ async function pushAllBatch(ctx) {
959
+ let firstSyncError;
960
+ try {
961
+ const changesSnapshot = [...ctx.state.getState().pendingChanges].filter((change) => ctx.batchSync.syncTables.includes(change.stateKey)).sort((a, b) => orderFor(a.action) - orderFor(b.action));
962
+ if (changesSnapshot.length === 0) {
963
+ ctx.logger.debug("[dync] push:batch:no-changes");
964
+ return void 0;
965
+ }
966
+ const changesToPush = changesSnapshot.filter((change) => {
967
+ if (change.action === "update" /* Update */ && ctx.state.hasConflicts(change.localId)) {
968
+ ctx.logger.warn(`[dync] push:batch:skipping-with-conflicts stateKey=${change.stateKey} localId=${change.localId}`);
969
+ return false;
970
+ }
971
+ return true;
972
+ });
973
+ if (changesToPush.length === 0) {
974
+ ctx.logger.debug("[dync] push:batch:all-skipped");
975
+ return void 0;
976
+ }
977
+ const payloads = changesToPush.map((change) => ({
978
+ table: change.stateKey,
979
+ action: change.action === "create" /* Create */ ? "add" : change.action === "update" /* Update */ ? "update" : "remove",
980
+ localId: change.localId,
981
+ id: change.id,
982
+ data: change.action === "remove" /* Remove */ ? void 0 : change.changes
983
+ }));
984
+ ctx.logger.debug(`[dync] push:batch:start count=${payloads.length}`);
985
+ const results = await ctx.batchSync.push(payloads);
986
+ const resultMap = /* @__PURE__ */ new Map();
987
+ for (const result of results) {
988
+ resultMap.set(result.localId, result);
989
+ }
990
+ for (const change of changesToPush) {
991
+ const result = resultMap.get(change.localId);
992
+ if (!result) {
993
+ ctx.logger.warn(`[dync] push:batch:missing-result localId=${change.localId}`);
994
+ continue;
995
+ }
996
+ try {
997
+ await processBatchPushResult(change, result, ctx);
998
+ } catch (err) {
999
+ firstSyncError = firstSyncError ?? err;
1000
+ ctx.logger.error(`[dync] push:batch:error localId=${change.localId}`, err);
1001
+ }
1002
+ }
1003
+ } catch (err) {
1004
+ firstSyncError = err;
1005
+ ctx.logger.error("[dync] push:batch:error", err);
1006
+ }
1007
+ return firstSyncError;
1008
+ }
1009
+ async function processBatchPushResult(change, result, ctx) {
1010
+ const { action, stateKey, localId } = change;
1011
+ if (!result.success) {
1012
+ if (action === "update" /* Update */) {
1013
+ await handleMissingRemoteRecord(change, ctx);
1014
+ } else {
1015
+ ctx.logger.warn(`[dync] push:batch:failed stateKey=${stateKey} localId=${localId} error=${result.error}`);
1016
+ }
1017
+ return;
1018
+ }
1019
+ switch (action) {
1020
+ case "remove" /* Remove */:
1021
+ handleRemoveSuccess(change, ctx);
1022
+ break;
1023
+ case "update" /* Update */:
1024
+ handleUpdateSuccess(change, ctx);
1025
+ break;
1026
+ case "create" /* Create */: {
1027
+ const serverResult = { id: result.id };
1028
+ if (result.updated_at) {
1029
+ serverResult.updated_at = result.updated_at;
1030
+ }
1031
+ await handleCreateSuccess(change, serverResult, ctx);
1032
+ break;
1033
+ }
1034
+ }
1035
+ }
1036
+
1037
+ // src/core/firstLoad.ts
1038
+ var yieldToEventLoop = () => sleep(0);
1039
+ var WRITE_BATCH_SIZE = 200;
1040
+ async function startFirstLoad(ctx) {
1041
+ ctx.logger.debug("[dync] Starting first load...");
1042
+ if (ctx.state.getState().firstLoadDone) {
1043
+ ctx.logger.debug("[dync] First load already completed");
1044
+ return;
1045
+ }
1046
+ let error;
1047
+ for (const [stateKey, api] of Object.entries(ctx.syncApis)) {
1048
+ if (!api.firstLoad) {
1049
+ ctx.logger.error(`[dync] firstLoad:no-api-function stateKey=${stateKey}`);
1050
+ continue;
1051
+ }
1052
+ try {
1053
+ ctx.logger.info(`[dync] firstLoad:start stateKey=${stateKey}`);
1054
+ let lastId;
1055
+ let isEmptyTable = true;
1056
+ let batchCount = 0;
1057
+ let totalInserted = 0;
1058
+ let totalUpdated = 0;
1059
+ while (true) {
1060
+ const batch = await api.firstLoad(lastId);
1061
+ if (!batch?.length) break;
1062
+ batchCount++;
1063
+ const { inserted, updated } = await processBatchInChunks(ctx, stateKey, batch, isEmptyTable, lastId === void 0);
1064
+ totalInserted += inserted;
1065
+ totalUpdated += updated;
1066
+ if (ctx.onProgress) {
1067
+ ctx.onProgress({
1068
+ table: stateKey,
1069
+ inserted: totalInserted,
1070
+ updated: totalUpdated,
1071
+ total: totalInserted + totalUpdated
1072
+ });
1073
+ }
1074
+ if (lastId === void 0) {
1075
+ isEmptyTable = await ctx.table(stateKey).count() === batch.length;
1076
+ }
1077
+ if (lastId !== void 0 && lastId === batch[batch.length - 1].id) {
1078
+ throw new Error(`Duplicate records downloaded, stopping to prevent infinite loop`);
1079
+ }
1080
+ lastId = batch[batch.length - 1].id;
1081
+ if (batchCount % 5 === 0) {
1082
+ await yieldToEventLoop();
1083
+ }
1084
+ }
1085
+ ctx.logger.info(`[dync] firstLoad:done stateKey=${stateKey} inserted=${totalInserted} updated=${totalUpdated}`);
1086
+ } catch (err) {
1087
+ error = error ?? err;
1088
+ ctx.logger.error(`[dync] firstLoad:error stateKey=${stateKey}`, err);
1089
+ }
1090
+ }
1091
+ await ctx.state.setState((syncState) => ({
1092
+ ...syncState,
1093
+ firstLoadDone: true,
1094
+ error
1095
+ }));
1096
+ ctx.logger.debug("[dync] First load completed");
1097
+ }
1098
+ async function processBatchInChunks(ctx, stateKey, batch, isEmptyTable, isFirstBatch) {
1099
+ let newest = new Date(ctx.state.getState().lastPulled[stateKey] || 0);
1100
+ return ctx.withTransaction("rw", [stateKey, DYNC_STATE_TABLE], async (tables) => {
1101
+ const txTable = tables[stateKey];
1102
+ let tableIsEmpty = isEmptyTable;
1103
+ if (isFirstBatch) {
1104
+ const count = await txTable.count();
1105
+ tableIsEmpty = count === 0;
1106
+ }
1107
+ const activeRecords = [];
1108
+ for (const remote of batch) {
1109
+ const remoteUpdated = new Date(remote.updated_at || 0);
1110
+ if (remoteUpdated > newest) newest = remoteUpdated;
1111
+ if (remote.deleted) continue;
1112
+ delete remote.deleted;
1113
+ remote._localId = createLocalId();
1114
+ activeRecords.push(remote);
1115
+ }
1116
+ let inserted = 0;
1117
+ let updated = 0;
1118
+ if (tableIsEmpty) {
1119
+ for (let i = 0; i < activeRecords.length; i += WRITE_BATCH_SIZE) {
1120
+ const chunk = activeRecords.slice(i, i + WRITE_BATCH_SIZE);
1121
+ await txTable.raw.bulkAdd(chunk);
1122
+ inserted += chunk.length;
1123
+ }
1124
+ } else {
1125
+ for (let i = 0; i < activeRecords.length; i += WRITE_BATCH_SIZE) {
1126
+ const chunk = activeRecords.slice(i, i + WRITE_BATCH_SIZE);
1127
+ const chunkResult = await processChunkWithLookup(txTable, chunk);
1128
+ inserted += chunkResult.inserted;
1129
+ updated += chunkResult.updated;
1130
+ }
1131
+ }
1132
+ await ctx.state.setState((syncState) => ({
1133
+ ...syncState,
1134
+ lastPulled: {
1135
+ ...syncState.lastPulled,
1136
+ [stateKey]: newest.toISOString()
1137
+ }
1138
+ }));
1139
+ return { inserted, updated };
1140
+ });
1141
+ }
1142
+ async function processChunkWithLookup(txTable, chunk) {
1143
+ const serverIds = chunk.filter((r) => r.id != null).map((r) => r.id);
1144
+ const existingByServerId = /* @__PURE__ */ new Map();
1145
+ if (serverIds.length > 0) {
1146
+ const existingRecords = await txTable.where("id").anyOf(serverIds).toArray();
1147
+ for (const existing of existingRecords) {
1148
+ existingByServerId.set(existing.id, existing);
1149
+ }
1150
+ }
1151
+ const toAdd = [];
1152
+ let updated = 0;
1153
+ for (const remote of chunk) {
1154
+ const existing = remote.id != null ? existingByServerId.get(remote.id) : void 0;
1155
+ if (existing) {
1156
+ const merged = Object.assign({}, existing, remote, { _localId: existing._localId });
1157
+ await txTable.raw.update(existing._localId, merged);
1158
+ updated++;
1159
+ } else {
1160
+ toAdd.push(remote);
1161
+ }
1162
+ }
1163
+ if (toAdd.length > 0) {
1164
+ await txTable.raw.bulkAdd(toAdd);
1165
+ }
1166
+ existingByServerId.clear();
1167
+ return { inserted: toAdd.length, updated };
1168
+ }
1169
+ async function startFirstLoadBatch(ctx) {
1170
+ ctx.logger.debug("[dync] Starting batch first load...");
1171
+ if (ctx.state.getState().firstLoadDone) {
1172
+ ctx.logger.debug("[dync] First load already completed");
1173
+ return;
1174
+ }
1175
+ if (!ctx.batchSync.firstLoad) {
1176
+ ctx.logger.warn("[dync] firstLoad:batch:no-firstLoad-function");
1177
+ await ctx.state.setState((syncState) => ({
1178
+ ...syncState,
1179
+ firstLoadDone: true
1180
+ }));
1181
+ return;
1182
+ }
1183
+ let error;
1184
+ try {
1185
+ ctx.logger.info(`[dync] firstLoad:batch:start tables=${[...ctx.batchSync.syncTables].join(",")}`);
1186
+ const progress = {};
1187
+ for (const tableName of ctx.batchSync.syncTables) {
1188
+ progress[tableName] = { inserted: 0, updated: 0 };
1189
+ }
1190
+ let cursors = {};
1191
+ for (const tableName of ctx.batchSync.syncTables) {
1192
+ cursors[tableName] = void 0;
1193
+ }
1194
+ let batchCount = 0;
1195
+ while (true) {
1196
+ const result = await ctx.batchSync.firstLoad(cursors);
1197
+ if (!result.hasMore && Object.values(result.data).every((d) => !d?.length)) {
1198
+ break;
1199
+ }
1200
+ batchCount++;
1201
+ for (const [stateKey, batch] of Object.entries(result.data)) {
1202
+ if (!ctx.batchSync.syncTables.includes(stateKey)) {
1203
+ ctx.logger.warn(`[dync] firstLoad:batch:unknown-table stateKey=${stateKey}`);
1204
+ continue;
1205
+ }
1206
+ if (!batch?.length) continue;
1207
+ const isFirstBatch = progress[stateKey].inserted === 0 && progress[stateKey].updated === 0;
1208
+ const isEmptyTable = isFirstBatch && await ctx.table(stateKey).count() === 0;
1209
+ const { inserted, updated } = await processBatchInChunks(ctx, stateKey, batch, isEmptyTable, isFirstBatch);
1210
+ progress[stateKey].inserted += inserted;
1211
+ progress[stateKey].updated += updated;
1212
+ if (ctx.onProgress) {
1213
+ ctx.onProgress({
1214
+ table: stateKey,
1215
+ inserted: progress[stateKey].inserted,
1216
+ updated: progress[stateKey].updated,
1217
+ total: progress[stateKey].inserted + progress[stateKey].updated
1218
+ });
1219
+ }
1220
+ }
1221
+ cursors = result.cursors;
1222
+ if (batchCount % 5 === 0) {
1223
+ await yieldToEventLoop();
1224
+ }
1225
+ if (!result.hasMore) {
1226
+ break;
1227
+ }
1228
+ }
1229
+ for (const [stateKey, p] of Object.entries(progress)) {
1230
+ ctx.logger.info(`[dync] firstLoad:batch:done stateKey=${stateKey} inserted=${p.inserted} updated=${p.updated}`);
1231
+ }
1232
+ } catch (err) {
1233
+ error = err;
1234
+ ctx.logger.error("[dync] firstLoad:batch:error", err);
1235
+ }
1236
+ await ctx.state.setState((syncState) => ({
1237
+ ...syncState,
1238
+ firstLoadDone: true,
1239
+ error
1240
+ }));
1241
+ ctx.logger.debug("[dync] Batch first load completed");
1242
+ }
1243
+
1244
+ // src/index.shared.ts
1245
+ var DEFAULT_SYNC_INTERVAL_MILLIS = 2e3;
1246
+ var DEFAULT_LOGGER = console;
1247
+ var DEFAULT_MIN_LOG_LEVEL = "debug";
1248
+ var DEFAULT_MISSING_REMOTE_RECORD_STRATEGY = "insert-remote-record";
1249
+ var DEFAULT_CONFLICT_RESOLUTION_STRATEGY = "try-shallow-merge";
1250
+ var DyncBase = class {
1251
+ adapter;
1252
+ tableCache = /* @__PURE__ */ new Map();
1253
+ mutationWrappedTables = /* @__PURE__ */ new Set();
1254
+ syncEnhancedTables = /* @__PURE__ */ new Set();
1255
+ mutationListeners = /* @__PURE__ */ new Set();
1256
+ visibilitySubscription;
1257
+ openPromise;
1258
+ disableSyncPromise;
1259
+ disableSyncPromiseResolver;
1260
+ sleepAbortController;
1261
+ closing = false;
1262
+ // Per-table sync mode
1263
+ syncApis = {};
1264
+ // Batch sync mode
1265
+ batchSync;
1266
+ syncedTables = /* @__PURE__ */ new Set();
1267
+ syncOptions;
1268
+ logger;
1269
+ syncTimerStarted = false;
1270
+ mutationsDuringSync = false;
1271
+ state;
1272
+ name;
1273
+ constructor(databaseName, syncApisOrBatchSync, storageAdapter, options) {
1274
+ const isBatchMode = typeof syncApisOrBatchSync.push === "function";
1275
+ if (isBatchMode) {
1276
+ this.batchSync = syncApisOrBatchSync;
1277
+ this.syncedTables = new Set(this.batchSync.syncTables);
1278
+ } else {
1279
+ this.syncApis = syncApisOrBatchSync;
1280
+ this.syncedTables = new Set(Object.keys(this.syncApis));
1281
+ }
1282
+ this.adapter = storageAdapter;
1283
+ this.name = databaseName;
1284
+ this.syncOptions = {
1285
+ syncInterval: DEFAULT_SYNC_INTERVAL_MILLIS,
1286
+ logger: DEFAULT_LOGGER,
1287
+ minLogLevel: DEFAULT_MIN_LOG_LEVEL,
1288
+ missingRemoteRecordDuringUpdateStrategy: DEFAULT_MISSING_REMOTE_RECORD_STRATEGY,
1289
+ conflictResolutionStrategy: DEFAULT_CONFLICT_RESOLUTION_STRATEGY,
1290
+ ...options ?? {}
1291
+ };
1292
+ this.logger = newLogger(this.syncOptions.logger, this.syncOptions.minLogLevel);
1293
+ this.state = new StateManager({
1294
+ storageAdapter: this.adapter
1295
+ });
1296
+ const driverInfo = "driverType" in this.adapter ? ` (Driver: ${this.adapter.driverType})` : "";
1297
+ this.logger.debug(`[dync] Initialized with ${this.adapter.type}${driverInfo}`);
1298
+ }
1299
+ version(versionNumber) {
1300
+ const self = this;
1301
+ const schemaOptions = {};
1302
+ let storesDefined = false;
1303
+ const builder = {
1304
+ stores(schema) {
1305
+ const usesStructuredSchema = Object.values(schema).some((def) => typeof def !== "string");
1306
+ const stateTableSchema = usesStructuredSchema ? {
1307
+ columns: {
1308
+ [LOCAL_PK]: { type: "TEXT" },
1309
+ value: { type: "TEXT" }
1310
+ }
1311
+ } : LOCAL_PK;
1312
+ const fullSchema = {
1313
+ ...schema,
1314
+ [DYNC_STATE_TABLE]: stateTableSchema
1315
+ };
1316
+ for (const [tableName, tableSchema] of Object.entries(schema)) {
1317
+ const isSyncTable = self.syncedTables.has(tableName);
1318
+ if (typeof tableSchema === "string") {
1319
+ if (isSyncTable) {
1320
+ fullSchema[tableName] = `${LOCAL_PK}, &${SERVER_PK}, ${tableSchema}, ${UPDATED_AT}`;
1321
+ }
1322
+ self.logger.debug(
1323
+ `[dync] Defining ${isSyncTable ? "" : "non-"}sync table '${tableName}' with primary key & indexes '${fullSchema[tableName]}'`
1324
+ );
1325
+ } else {
1326
+ if (isSyncTable) {
1327
+ fullSchema[tableName] = self.injectSyncColumns(tableSchema);
1328
+ }
1329
+ const schemaColumns = Object.keys(fullSchema[tableName].columns ?? {}).join(", ");
1330
+ const schemaIndexes = (fullSchema[tableName].indexes ?? []).map((idx) => idx.columns.join("+")).join(", ");
1331
+ self.logger.debug(
1332
+ `[dync] Defining ${isSyncTable ? "" : "non-"}sync table '${tableName}' with columns ${schemaColumns} and indexes ${schemaIndexes}`
1333
+ );
1334
+ }
1335
+ }
1336
+ storesDefined = true;
1337
+ self.adapter.defineSchema(versionNumber, fullSchema, schemaOptions);
1338
+ self.setupEnhancedTables(Object.keys(schema));
1339
+ return builder;
1340
+ },
1341
+ sqlite(configure) {
1342
+ if (!storesDefined) {
1343
+ throw new Error("Call stores() before registering sqlite migrations");
1344
+ }
1345
+ const sqliteOptions = schemaOptions.sqlite ??= {};
1346
+ const migrations = sqliteOptions.migrations ??= {};
1347
+ const configurator = {
1348
+ upgrade(handler) {
1349
+ migrations.upgrade = handler;
1350
+ },
1351
+ downgrade(handler) {
1352
+ migrations.downgrade = handler;
1353
+ }
1354
+ };
1355
+ configure(configurator);
1356
+ return builder;
1357
+ }
1358
+ };
1359
+ return builder;
1360
+ }
1361
+ async open() {
1362
+ if (this.closing) {
1363
+ return;
1364
+ }
1365
+ if (this.openPromise) {
1366
+ return this.openPromise;
1367
+ }
1368
+ this.openPromise = (async () => {
1369
+ if (this.closing) return;
1370
+ await this.adapter.open();
1371
+ if (this.closing) return;
1372
+ await this.state.hydrate();
1373
+ })();
1374
+ return this.openPromise;
1375
+ }
1376
+ async close() {
1377
+ this.closing = true;
1378
+ if (this.openPromise) {
1379
+ await this.openPromise.catch(() => {
1380
+ });
1381
+ this.openPromise = void 0;
1382
+ }
1383
+ await this.enableSync(false);
1384
+ await this.adapter.close();
1385
+ this.tableCache.clear();
1386
+ this.mutationWrappedTables.clear();
1387
+ this.syncEnhancedTables.clear();
1388
+ }
1389
+ async delete() {
1390
+ await this.adapter.delete();
1391
+ this.tableCache.clear();
1392
+ this.mutationWrappedTables.clear();
1393
+ this.syncEnhancedTables.clear();
1394
+ }
1395
+ async query(callback) {
1396
+ return this.adapter.query(callback);
1397
+ }
1398
+ table(name) {
1399
+ if (this.tableCache.has(name)) {
1400
+ return this.tableCache.get(name);
1401
+ }
1402
+ const table = this.adapter.table(name);
1403
+ const isSyncTable = this.syncedTables.has(name);
1404
+ if (isSyncTable && !this.syncEnhancedTables.has(name)) {
1405
+ this.enhanceSyncTable(table, name);
1406
+ } else if (!isSyncTable && !this.mutationWrappedTables.has(name) && name !== DYNC_STATE_TABLE) {
1407
+ wrapWithMutationEmitter(table, name, this.emitMutation.bind(this));
1408
+ this.mutationWrappedTables.add(name);
1409
+ }
1410
+ this.tableCache.set(name, table);
1411
+ return table;
1412
+ }
1413
+ async withTransaction(mode, tableNames, fn) {
1414
+ await this.open();
1415
+ return this.adapter.transaction(mode, tableNames, async () => {
1416
+ const tables = {};
1417
+ for (const tableName of tableNames) {
1418
+ tables[tableName] = this.table(tableName);
1419
+ }
1420
+ return fn(tables);
1421
+ });
1422
+ }
1423
+ setupEnhancedTables(tableNames) {
1424
+ setupEnhancedTables(
1425
+ {
1426
+ owner: this,
1427
+ tableCache: this.tableCache,
1428
+ enhancedTables: this.syncEnhancedTables,
1429
+ getTable: (name) => this.table(name)
1430
+ },
1431
+ tableNames
1432
+ );
1433
+ for (const tableName of tableNames) {
1434
+ this.mutationWrappedTables.delete(tableName);
1435
+ }
1436
+ }
1437
+ injectSyncColumns(schema) {
1438
+ const columns = schema.columns ?? {};
1439
+ if (columns[LOCAL_PK]) {
1440
+ throw new Error(`Column '${LOCAL_PK}' is auto-injected for sync tables and cannot be defined manually.`);
1441
+ }
1442
+ if (columns[SERVER_PK]) {
1443
+ throw new Error(`Column '${SERVER_PK}' is auto-injected for sync tables and cannot be defined manually.`);
1444
+ }
1445
+ if (columns[UPDATED_AT]) {
1446
+ throw new Error(`Column '${UPDATED_AT}' is auto-injected for sync tables and cannot be defined manually.`);
1447
+ }
1448
+ const injectedColumns = {
1449
+ ...columns,
1450
+ [LOCAL_PK]: { type: "TEXT" },
1451
+ [SERVER_PK]: { type: "INTEGER", unique: true },
1452
+ [UPDATED_AT]: { type: "TEXT" }
1453
+ };
1454
+ const userIndexes = schema.indexes ?? [];
1455
+ const hasUpdatedAtIndex = userIndexes.some((idx) => idx.columns.length === 1 && idx.columns[0] === UPDATED_AT);
1456
+ const injectedIndexes = hasUpdatedAtIndex ? userIndexes : [...userIndexes, { columns: [UPDATED_AT] }];
1457
+ return {
1458
+ ...schema,
1459
+ columns: injectedColumns,
1460
+ indexes: injectedIndexes
1461
+ };
1462
+ }
1463
+ enhanceSyncTable(table, tableName) {
1464
+ enhanceSyncTable({
1465
+ table,
1466
+ tableName,
1467
+ withTransaction: this.withTransaction.bind(this),
1468
+ state: this.state,
1469
+ enhancedTables: this.syncEnhancedTables,
1470
+ emitMutation: this.emitMutation.bind(this)
1471
+ });
1472
+ }
1473
+ async syncOnce() {
1474
+ if (this.closing) {
1475
+ return;
1476
+ }
1477
+ if (this.syncStatus === "syncing") {
1478
+ this.mutationsDuringSync = true;
1479
+ return;
1480
+ }
1481
+ this.syncStatus = "syncing";
1482
+ this.mutationsDuringSync = false;
1483
+ const pullResult = await this.pullAll();
1484
+ const firstPushSyncError = await this.pushAll();
1485
+ for (const tableName of pullResult.changedTables) {
1486
+ this.emitMutation({ type: "pull", tableName });
1487
+ }
1488
+ this.syncStatus = "idle";
1489
+ await this.state.setState((syncState) => ({
1490
+ ...syncState,
1491
+ error: pullResult.error ?? firstPushSyncError
1492
+ }));
1493
+ if (this.mutationsDuringSync) {
1494
+ this.mutationsDuringSync = false;
1495
+ this.syncOnce().catch(() => {
1496
+ });
1497
+ }
1498
+ }
1499
+ async pullAll() {
1500
+ const baseContext = {
1501
+ logger: this.logger,
1502
+ state: this.state,
1503
+ table: this.table.bind(this),
1504
+ withTransaction: this.withTransaction.bind(this),
1505
+ conflictResolutionStrategy: this.syncOptions.conflictResolutionStrategy
1506
+ };
1507
+ if (this.batchSync) {
1508
+ return pullAllBatch({
1509
+ ...baseContext,
1510
+ batchSync: this.batchSync
1511
+ });
1512
+ }
1513
+ return pullAll({
1514
+ ...baseContext,
1515
+ syncApis: this.syncApis
1516
+ });
1517
+ }
1518
+ async pushAll() {
1519
+ const baseContext = {
1520
+ logger: this.logger,
1521
+ state: this.state,
1522
+ table: this.table.bind(this),
1523
+ withTransaction: this.withTransaction.bind(this),
1524
+ syncOptions: this.syncOptions
1525
+ };
1526
+ if (this.batchSync) {
1527
+ return pushAllBatch({
1528
+ ...baseContext,
1529
+ batchSync: this.batchSync
1530
+ });
1531
+ }
1532
+ return pushAll({
1533
+ ...baseContext,
1534
+ syncApis: this.syncApis
1535
+ });
1536
+ }
1537
+ startSyncTimer(start) {
1538
+ if (start) {
1539
+ void this.tryStart();
1540
+ } else {
1541
+ this.syncTimerStarted = false;
1542
+ }
1543
+ }
1544
+ async tryStart() {
1545
+ if (this.syncTimerStarted) return;
1546
+ this.syncTimerStarted = true;
1547
+ while (this.syncTimerStarted) {
1548
+ this.sleepAbortController = new AbortController();
1549
+ await this.syncOnce();
1550
+ await sleep(this.syncOptions.syncInterval, this.sleepAbortController.signal);
1551
+ }
1552
+ this.syncStatus = "disabled";
1553
+ this.disableSyncPromiseResolver?.();
1554
+ }
1555
+ setupVisibilityListener(add) {
1556
+ this.visibilitySubscription = addVisibilityChangeListener(add, this.visibilitySubscription, (isVisible) => this.handleVisibilityChange(isVisible));
1557
+ }
1558
+ handleVisibilityChange(isVisible) {
1559
+ if (isVisible) {
1560
+ this.logger.debug("[dync] sync:start-in-foreground");
1561
+ this.startSyncTimer(true);
1562
+ } else {
1563
+ this.logger.debug("[dync] sync:pause-in-background");
1564
+ this.startSyncTimer(false);
1565
+ }
1566
+ }
1567
+ async startFirstLoad(onProgress) {
1568
+ await this.open();
1569
+ const baseContext = {
1570
+ logger: this.logger,
1571
+ state: this.state,
1572
+ table: this.table.bind(this),
1573
+ withTransaction: this.withTransaction.bind(this),
1574
+ onProgress
1575
+ };
1576
+ if (this.batchSync) {
1577
+ await startFirstLoadBatch({
1578
+ ...baseContext,
1579
+ batchSync: this.batchSync
1580
+ });
1581
+ } else {
1582
+ await startFirstLoad({
1583
+ ...baseContext,
1584
+ syncApis: this.syncApis
1585
+ });
1586
+ }
1587
+ for (const tableName of this.syncedTables) {
1588
+ this.emitMutation({ type: "pull", tableName });
1589
+ }
1590
+ }
1591
+ getSyncState() {
1592
+ return this.state.getSyncState();
1593
+ }
1594
+ async resolveConflict(localId, keepLocal) {
1595
+ const conflict = this.state.getState().conflicts?.[localId];
1596
+ if (!conflict) {
1597
+ this.logger.warn(`[dync] No conflict found for localId: ${localId}`);
1598
+ return;
1599
+ }
1600
+ await this.withTransaction("rw", [conflict.stateKey, DYNC_STATE_TABLE], async (tables) => {
1601
+ const txTable = tables[conflict.stateKey];
1602
+ if (!keepLocal) {
1603
+ const item = await txTable.get(localId);
1604
+ if (item) {
1605
+ for (const field of conflict.fields) {
1606
+ item[field.key] = field.remoteValue;
1607
+ }
1608
+ await txTable.raw.update(localId, item);
1609
+ } else {
1610
+ this.logger.warn(`[dync] No local item found for localId: ${localId} to apply remote values`);
1611
+ }
1612
+ await this.state.setState((syncState) => ({
1613
+ ...syncState,
1614
+ pendingChanges: syncState.pendingChanges.filter((p) => !(p.localId === localId && p.stateKey === conflict.stateKey))
1615
+ }));
1616
+ }
1617
+ await this.state.setState((syncState) => {
1618
+ const ss = { ...syncState };
1619
+ delete ss.conflicts?.[localId];
1620
+ return ss;
1621
+ });
1622
+ });
1623
+ }
1624
+ async enableSync(enabled) {
1625
+ if (!enabled) {
1626
+ if (this.syncTimerStarted) {
1627
+ this.disableSyncPromise = new Promise((resolve) => {
1628
+ this.disableSyncPromiseResolver = resolve;
1629
+ });
1630
+ this.sleepAbortController?.abort();
1631
+ this.syncStatus = "disabling";
1632
+ this.startSyncTimer(false);
1633
+ this.setupVisibilityListener(false);
1634
+ return this.disableSyncPromise;
1635
+ }
1636
+ this.syncStatus = "disabled";
1637
+ this.setupVisibilityListener(false);
1638
+ return Promise.resolve();
1639
+ }
1640
+ this.syncStatus = "idle";
1641
+ this.startSyncTimer(true);
1642
+ this.setupVisibilityListener(true);
1643
+ return Promise.resolve();
1644
+ }
1645
+ get syncStatus() {
1646
+ return this.state.getSyncStatus();
1647
+ }
1648
+ set syncStatus(status) {
1649
+ this.state.setSyncStatus(status);
1650
+ }
1651
+ onSyncStateChange(fn) {
1652
+ return this.state.subscribe(fn);
1653
+ }
1654
+ onMutation(fn) {
1655
+ this.mutationListeners.add(fn);
1656
+ return () => this.mutationListeners.delete(fn);
1657
+ }
1658
+ emitMutation(event) {
1659
+ if (event.type === "add" || event.type === "update" || event.type === "delete") {
1660
+ if (this.syncTimerStarted) {
1661
+ this.syncOnce().catch(() => {
1662
+ });
1663
+ }
1664
+ }
1665
+ for (const listener of this.mutationListeners) {
1666
+ listener(event);
1667
+ }
1668
+ }
1669
+ // Public API
1670
+ sync = {
1671
+ enable: this.enableSync.bind(this),
1672
+ startFirstLoad: this.startFirstLoad.bind(this),
1673
+ getState: this.getSyncState.bind(this),
1674
+ resolveConflict: this.resolveConflict.bind(this),
1675
+ onStateChange: this.onSyncStateChange.bind(this),
1676
+ onMutation: this.onMutation.bind(this)
1677
+ };
1678
+ };
1679
+ var DyncConstructor = DyncBase;
1680
+ var Dync = DyncConstructor;
1681
+
1682
+ // src/react/useDync.ts
1683
+ function makeDync(config) {
1684
+ const db = "syncApis" in config ? new Dync(config.databaseName, config.syncApis, config.storageAdapter, config.options) : new Dync(config.databaseName, config.batchSync, config.storageAdapter, config.options);
1685
+ let cachedState = db.sync.getState();
1686
+ const subscribe = (listener) => db.sync.onStateChange((nextState) => {
1687
+ cachedState = nextState;
1688
+ listener();
1689
+ });
1690
+ const getSnapshot = () => {
1691
+ const fresh = db.sync.getState();
1692
+ if (JSON.stringify(fresh) !== JSON.stringify(cachedState)) {
1693
+ cachedState = fresh;
1694
+ }
1695
+ return cachedState;
1696
+ };
1697
+ const useDync = () => {
1698
+ const syncState = (0, import_react.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
1699
+ return { db, syncState };
1700
+ };
1701
+ const boundUseLiveQuery = (querier, deps = [], tables) => {
1702
+ return useLiveQueryImpl(db, querier, deps, tables);
1703
+ };
1704
+ return {
1705
+ db,
1706
+ useDync,
1707
+ useLiveQuery: boundUseLiveQuery
1708
+ };
1709
+ }
1710
+ function useLiveQueryImpl(db, querier, deps = [], tables) {
1711
+ const [result, setResult] = (0, import_react.useState)(void 0);
1712
+ const [, setError] = (0, import_react.useState)(null);
1713
+ const isMountedRef = (0, import_react.useRef)(true);
1714
+ const queryVersionRef = (0, import_react.useRef)(0);
1715
+ const querierRef = (0, import_react.useRef)(querier);
1716
+ const tablesRef = (0, import_react.useRef)(tables);
1717
+ querierRef.current = querier;
1718
+ tablesRef.current = tables;
1719
+ const runQuery = (0, import_react.useCallback)(async () => {
1720
+ const currentVersion = ++queryVersionRef.current;
1721
+ try {
1722
+ const queryResult = await querierRef.current(db);
1723
+ if (isMountedRef.current && currentVersion === queryVersionRef.current) {
1724
+ setResult(queryResult);
1725
+ setError(null);
1726
+ }
1727
+ } catch (err) {
1728
+ if (isMountedRef.current && currentVersion === queryVersionRef.current) {
1729
+ setError(err);
1730
+ }
1731
+ }
1732
+ }, [db]);
1733
+ (0, import_react.useEffect)(() => {
1734
+ runQuery();
1735
+ }, [...deps, runQuery]);
1736
+ (0, import_react.useEffect)(() => {
1737
+ isMountedRef.current = true;
1738
+ const unsubscribe = db.sync.onMutation((event) => {
1739
+ if (!tablesRef.current || tablesRef.current.includes(event.tableName)) {
1740
+ runQuery();
1741
+ }
1742
+ });
1743
+ return () => {
1744
+ isMountedRef.current = false;
1745
+ unsubscribe();
1746
+ };
1747
+ }, [db, runQuery]);
1748
+ return result;
1749
+ }
1750
+ // Annotate the CommonJS export names for ESM import in node:
1751
+ 0 && (module.exports = {
1752
+ makeDync
1753
+ });
1754
+ //# sourceMappingURL=index.cjs.map