@anfenn/zync 0.1.8 → 0.1.10

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.
package/dist/index.cjs CHANGED
@@ -3,6 +3,9 @@ var __defProp = Object.defineProperty;
3
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
5
5
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __esm = (fn, res) => function __init() {
7
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
8
+ };
6
9
  var __export = (target, all) => {
7
10
  for (var name in all)
8
11
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -17,12 +20,269 @@ var __copyProps = (to, from, except, desc) => {
17
20
  };
18
21
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
22
 
23
+ // node_modules/.pnpm/idb@8.0.3/node_modules/idb/build/index.js
24
+ var build_exports = {};
25
+ __export(build_exports, {
26
+ deleteDB: () => deleteDB,
27
+ openDB: () => openDB,
28
+ unwrap: () => unwrap,
29
+ wrap: () => wrap
30
+ });
31
+ function getIdbProxyableTypes() {
32
+ return idbProxyableTypes || (idbProxyableTypes = [
33
+ IDBDatabase,
34
+ IDBObjectStore,
35
+ IDBIndex,
36
+ IDBCursor,
37
+ IDBTransaction
38
+ ]);
39
+ }
40
+ function getCursorAdvanceMethods() {
41
+ return cursorAdvanceMethods || (cursorAdvanceMethods = [
42
+ IDBCursor.prototype.advance,
43
+ IDBCursor.prototype.continue,
44
+ IDBCursor.prototype.continuePrimaryKey
45
+ ]);
46
+ }
47
+ function promisifyRequest(request) {
48
+ const promise = new Promise((resolve, reject) => {
49
+ const unlisten = () => {
50
+ request.removeEventListener("success", success);
51
+ request.removeEventListener("error", error);
52
+ };
53
+ const success = () => {
54
+ resolve(wrap(request.result));
55
+ unlisten();
56
+ };
57
+ const error = () => {
58
+ reject(request.error);
59
+ unlisten();
60
+ };
61
+ request.addEventListener("success", success);
62
+ request.addEventListener("error", error);
63
+ });
64
+ reverseTransformCache.set(promise, request);
65
+ return promise;
66
+ }
67
+ function cacheDonePromiseForTransaction(tx) {
68
+ if (transactionDoneMap.has(tx))
69
+ return;
70
+ const done = new Promise((resolve, reject) => {
71
+ const unlisten = () => {
72
+ tx.removeEventListener("complete", complete);
73
+ tx.removeEventListener("error", error);
74
+ tx.removeEventListener("abort", error);
75
+ };
76
+ const complete = () => {
77
+ resolve();
78
+ unlisten();
79
+ };
80
+ const error = () => {
81
+ reject(tx.error || new DOMException("AbortError", "AbortError"));
82
+ unlisten();
83
+ };
84
+ tx.addEventListener("complete", complete);
85
+ tx.addEventListener("error", error);
86
+ tx.addEventListener("abort", error);
87
+ });
88
+ transactionDoneMap.set(tx, done);
89
+ }
90
+ function replaceTraps(callback) {
91
+ idbProxyTraps = callback(idbProxyTraps);
92
+ }
93
+ function wrapFunction(func) {
94
+ if (getCursorAdvanceMethods().includes(func)) {
95
+ return function(...args) {
96
+ func.apply(unwrap(this), args);
97
+ return wrap(this.request);
98
+ };
99
+ }
100
+ return function(...args) {
101
+ return wrap(func.apply(unwrap(this), args));
102
+ };
103
+ }
104
+ function transformCachableValue(value) {
105
+ if (typeof value === "function")
106
+ return wrapFunction(value);
107
+ if (value instanceof IDBTransaction)
108
+ cacheDonePromiseForTransaction(value);
109
+ if (instanceOfAny(value, getIdbProxyableTypes()))
110
+ return new Proxy(value, idbProxyTraps);
111
+ return value;
112
+ }
113
+ function wrap(value) {
114
+ if (value instanceof IDBRequest)
115
+ return promisifyRequest(value);
116
+ if (transformCache.has(value))
117
+ return transformCache.get(value);
118
+ const newValue = transformCachableValue(value);
119
+ if (newValue !== value) {
120
+ transformCache.set(value, newValue);
121
+ reverseTransformCache.set(newValue, value);
122
+ }
123
+ return newValue;
124
+ }
125
+ function openDB(name, version, { blocked, upgrade, blocking, terminated } = {}) {
126
+ const request = indexedDB.open(name, version);
127
+ const openPromise = wrap(request);
128
+ if (upgrade) {
129
+ request.addEventListener("upgradeneeded", (event) => {
130
+ upgrade(wrap(request.result), event.oldVersion, event.newVersion, wrap(request.transaction), event);
131
+ });
132
+ }
133
+ if (blocked) {
134
+ request.addEventListener("blocked", (event) => blocked(
135
+ // Casting due to https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/1405
136
+ event.oldVersion,
137
+ event.newVersion,
138
+ event
139
+ ));
140
+ }
141
+ openPromise.then((db) => {
142
+ if (terminated)
143
+ db.addEventListener("close", () => terminated());
144
+ if (blocking) {
145
+ db.addEventListener("versionchange", (event) => blocking(event.oldVersion, event.newVersion, event));
146
+ }
147
+ }).catch(() => {
148
+ });
149
+ return openPromise;
150
+ }
151
+ function deleteDB(name, { blocked } = {}) {
152
+ const request = indexedDB.deleteDatabase(name);
153
+ if (blocked) {
154
+ request.addEventListener("blocked", (event) => blocked(
155
+ // Casting due to https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/1405
156
+ event.oldVersion,
157
+ event
158
+ ));
159
+ }
160
+ return wrap(request).then(() => void 0);
161
+ }
162
+ function getMethod(target, prop) {
163
+ if (!(target instanceof IDBDatabase && !(prop in target) && typeof prop === "string")) {
164
+ return;
165
+ }
166
+ if (cachedMethods.get(prop))
167
+ return cachedMethods.get(prop);
168
+ const targetFuncName = prop.replace(/FromIndex$/, "");
169
+ const useIndex = prop !== targetFuncName;
170
+ const isWrite = writeMethods.includes(targetFuncName);
171
+ if (
172
+ // Bail if the target doesn't exist on the target. Eg, getAll isn't in Edge.
173
+ !(targetFuncName in (useIndex ? IDBIndex : IDBObjectStore).prototype) || !(isWrite || readMethods.includes(targetFuncName))
174
+ ) {
175
+ return;
176
+ }
177
+ const method = async function(storeName, ...args) {
178
+ const tx = this.transaction(storeName, isWrite ? "readwrite" : "readonly");
179
+ let target2 = tx.store;
180
+ if (useIndex)
181
+ target2 = target2.index(args.shift());
182
+ return (await Promise.all([
183
+ target2[targetFuncName](...args),
184
+ isWrite && tx.done
185
+ ]))[0];
186
+ };
187
+ cachedMethods.set(prop, method);
188
+ return method;
189
+ }
190
+ async function* iterate(...args) {
191
+ let cursor = this;
192
+ if (!(cursor instanceof IDBCursor)) {
193
+ cursor = await cursor.openCursor(...args);
194
+ }
195
+ if (!cursor)
196
+ return;
197
+ cursor = cursor;
198
+ const proxiedCursor = new Proxy(cursor, cursorIteratorTraps);
199
+ ittrProxiedCursorToOriginalProxy.set(proxiedCursor, cursor);
200
+ reverseTransformCache.set(proxiedCursor, unwrap(cursor));
201
+ while (cursor) {
202
+ yield proxiedCursor;
203
+ cursor = await (advanceResults.get(proxiedCursor) || cursor.continue());
204
+ advanceResults.delete(proxiedCursor);
205
+ }
206
+ }
207
+ function isIteratorProp(target, prop) {
208
+ return prop === Symbol.asyncIterator && instanceOfAny(target, [IDBIndex, IDBObjectStore, IDBCursor]) || prop === "iterate" && instanceOfAny(target, [IDBIndex, IDBObjectStore]);
209
+ }
210
+ var instanceOfAny, idbProxyableTypes, cursorAdvanceMethods, transactionDoneMap, transformCache, reverseTransformCache, idbProxyTraps, unwrap, readMethods, writeMethods, cachedMethods, advanceMethodProps, methodMap, advanceResults, ittrProxiedCursorToOriginalProxy, cursorIteratorTraps;
211
+ var init_build = __esm({
212
+ "node_modules/.pnpm/idb@8.0.3/node_modules/idb/build/index.js"() {
213
+ "use strict";
214
+ instanceOfAny = (object, constructors) => constructors.some((c) => object instanceof c);
215
+ transactionDoneMap = /* @__PURE__ */ new WeakMap();
216
+ transformCache = /* @__PURE__ */ new WeakMap();
217
+ reverseTransformCache = /* @__PURE__ */ new WeakMap();
218
+ idbProxyTraps = {
219
+ get(target, prop, receiver) {
220
+ if (target instanceof IDBTransaction) {
221
+ if (prop === "done")
222
+ return transactionDoneMap.get(target);
223
+ if (prop === "store") {
224
+ return receiver.objectStoreNames[1] ? void 0 : receiver.objectStore(receiver.objectStoreNames[0]);
225
+ }
226
+ }
227
+ return wrap(target[prop]);
228
+ },
229
+ set(target, prop, value) {
230
+ target[prop] = value;
231
+ return true;
232
+ },
233
+ has(target, prop) {
234
+ if (target instanceof IDBTransaction && (prop === "done" || prop === "store")) {
235
+ return true;
236
+ }
237
+ return prop in target;
238
+ }
239
+ };
240
+ unwrap = (value) => reverseTransformCache.get(value);
241
+ readMethods = ["get", "getKey", "getAll", "getAllKeys", "count"];
242
+ writeMethods = ["put", "add", "delete", "clear"];
243
+ cachedMethods = /* @__PURE__ */ new Map();
244
+ replaceTraps((oldTraps) => ({
245
+ ...oldTraps,
246
+ get: (target, prop, receiver) => getMethod(target, prop) || oldTraps.get(target, prop, receiver),
247
+ has: (target, prop) => !!getMethod(target, prop) || oldTraps.has(target, prop)
248
+ }));
249
+ advanceMethodProps = ["continue", "continuePrimaryKey", "advance"];
250
+ methodMap = {};
251
+ advanceResults = /* @__PURE__ */ new WeakMap();
252
+ ittrProxiedCursorToOriginalProxy = /* @__PURE__ */ new WeakMap();
253
+ cursorIteratorTraps = {
254
+ get(target, prop) {
255
+ if (!advanceMethodProps.includes(prop))
256
+ return target[prop];
257
+ let cachedFunc = methodMap[prop];
258
+ if (!cachedFunc) {
259
+ cachedFunc = methodMap[prop] = function(...args) {
260
+ advanceResults.set(this, ittrProxiedCursorToOriginalProxy.get(this)[prop](...args));
261
+ };
262
+ }
263
+ return cachedFunc;
264
+ }
265
+ };
266
+ replaceTraps((oldTraps) => ({
267
+ ...oldTraps,
268
+ get(target, prop, receiver) {
269
+ if (isIteratorProp(target, prop))
270
+ return iterate;
271
+ return oldTraps.get(target, prop, receiver);
272
+ },
273
+ has(target, prop) {
274
+ return isIteratorProp(target, prop) || oldTraps.has(target, prop);
275
+ }
276
+ }));
277
+ }
278
+ });
279
+
20
280
  // src/index.ts
21
281
  var index_exports = {};
22
282
  __export(index_exports, {
23
283
  SyncAction: () => SyncAction,
24
284
  createIndexedDBStorage: () => createIndexedDBStorage,
25
- createStoreWithSync: () => createStoreWithSync,
285
+ createWithSync: () => createWithSync,
26
286
  nextLocalId: () => nextLocalId,
27
287
  persistWithSync: () => persistWithSync
28
288
  });
@@ -30,27 +290,247 @@ module.exports = __toCommonJS(index_exports);
30
290
  var import_zustand = require("zustand");
31
291
  var import_middleware = require("zustand/middleware");
32
292
 
293
+ // src/logger.ts
294
+ function newLogger(base, min) {
295
+ const order = {
296
+ debug: 10,
297
+ info: 20,
298
+ warn: 30,
299
+ error: 40,
300
+ none: 100
301
+ };
302
+ const threshold = order[min];
303
+ const enabled = (lvl) => order[lvl] >= threshold;
304
+ return {
305
+ debug: (...a) => enabled("debug") && base.debug?.(...a),
306
+ info: (...a) => enabled("info") && base.info?.(...a),
307
+ warn: (...a) => enabled("warn") && base.warn?.(...a),
308
+ error: (...a) => enabled("error") && base.error?.(...a)
309
+ };
310
+ }
311
+
312
+ // src/helpers.ts
313
+ function nextLocalId() {
314
+ return crypto.randomUUID();
315
+ }
316
+ function orderFor(a) {
317
+ switch (a) {
318
+ case "createOrUpdate" /* CreateOrUpdate */:
319
+ return 1;
320
+ case "remove" /* Remove */:
321
+ return 2;
322
+ }
323
+ }
324
+ function omitSyncFields(item, fields) {
325
+ const result = { ...item };
326
+ for (const k of fields) delete result[k];
327
+ return result;
328
+ }
329
+ function samePendingVersion(get, stateKey, localId, version) {
330
+ const q = get().syncState.pendingChanges || [];
331
+ const curChange = q.find((p) => p.localId === localId && p.stateKey === stateKey);
332
+ return curChange?.version === version;
333
+ }
334
+ function removeFromPendingChanges(set, localId, stateKey) {
335
+ set((s) => {
336
+ const queue = (s.syncState.pendingChanges || []).filter((p) => !(p.localId === localId && p.stateKey === stateKey));
337
+ return {
338
+ syncState: {
339
+ ...s.syncState || {},
340
+ pendingChanges: queue
341
+ }
342
+ };
343
+ });
344
+ }
345
+ function findApi(stateKey, syncApi) {
346
+ const api = syncApi[stateKey];
347
+ if (!api || !api.add || !api.update || !api.remove || !api.list || !api.firstLoad) {
348
+ throw new Error(`Missing API function(s) for state key: ${stateKey}.`);
349
+ }
350
+ return api;
351
+ }
352
+
353
+ // src/pull.ts
354
+ async function pull(set, get, stateKey, api, logger) {
355
+ const lastPulled = get().syncState.lastPulled || {};
356
+ const lastPulledAt = new Date(lastPulled[stateKey] || /* @__PURE__ */ new Date(0));
357
+ logger.debug(`[Zync] pull:start stateKey=${stateKey} since=${lastPulledAt.toISOString()}`);
358
+ const serverData = await api.list(lastPulledAt);
359
+ if (!serverData?.length) return;
360
+ let newest = lastPulledAt;
361
+ set((state) => {
362
+ const pendingChanges = state.syncState.pendingChanges || [];
363
+ const localItems = state[stateKey] || [];
364
+ let nextItems = [...localItems];
365
+ const localById = new Map(localItems.filter((l) => l.id).map((l) => [l.id, l]));
366
+ const pendingRemovalById = new Set(pendingChanges.filter((p) => p.stateKey === stateKey && p.action === "remove" /* Remove */).map((p) => p.id));
367
+ for (const remote of serverData) {
368
+ const remoteUpdated = new Date(remote.updated_at);
369
+ if (remoteUpdated > newest) newest = remoteUpdated;
370
+ if (pendingRemovalById.has(remote.id)) {
371
+ logger.debug(`[Zync] pull:skip-pending-remove stateKey=${stateKey} id=${remote.id}`);
372
+ continue;
373
+ }
374
+ const localItem = localById.get(remote.id);
375
+ if (remote.deleted) {
376
+ if (localItem) {
377
+ nextItems = nextItems.filter((i) => i.id !== remote.id);
378
+ logger.debug(`[Zync] pull:remove stateKey=${stateKey} id=${remote.id}`);
379
+ }
380
+ continue;
381
+ }
382
+ const pending = localItem && pendingChanges.some((p) => p.stateKey === stateKey && p.localId === localItem._localId);
383
+ if (localItem && !pending) {
384
+ const merged = {
385
+ ...localItem,
386
+ ...remote,
387
+ _localId: localItem._localId
388
+ };
389
+ nextItems = nextItems.map((i) => i._localId === localItem._localId ? merged : i);
390
+ logger.debug(`[Zync] pull:merge stateKey=${stateKey} id=${remote.id}`);
391
+ } else if (!localItem) {
392
+ nextItems = [...nextItems, { ...remote, _localId: nextLocalId() }];
393
+ logger.debug(`[Zync] pull:add stateKey=${stateKey} id=${remote.id}`);
394
+ }
395
+ }
396
+ return {
397
+ [stateKey]: nextItems,
398
+ syncState: {
399
+ ...state.syncState || {},
400
+ lastPulled: {
401
+ ...state.syncState.lastPulled || {},
402
+ [stateKey]: newest.toISOString()
403
+ }
404
+ }
405
+ };
406
+ });
407
+ }
408
+
409
+ // src/push.ts
410
+ var SYNC_FIELDS = ["id", "_localId", "updated_at", "deleted"];
411
+ async function pushOne(set, get, change, api, logger, queueToSync, missingStrategy, onMissingRemoteRecordDuringUpdate, onAfterRemoteAdd) {
412
+ logger.debug(`[Zync] push:attempt action=${change.action} stateKey=${change.stateKey} localId=${change.localId}`);
413
+ const { action, stateKey, localId, id, version } = change;
414
+ switch (action) {
415
+ case "remove" /* Remove */:
416
+ await api.remove(id);
417
+ logger.debug(`[Zync] push:remove:success ${stateKey} ${localId} ${id}`);
418
+ removeFromPendingChanges(set, localId, stateKey);
419
+ break;
420
+ case "createOrUpdate" /* CreateOrUpdate */: {
421
+ const state = get();
422
+ const items = state[stateKey] || [];
423
+ const item = items.find((i) => i._localId === localId);
424
+ if (!item) {
425
+ logger.warn(`[Zync] push:${action}:no-local-item`, {
426
+ stateKey,
427
+ localId
428
+ });
429
+ removeFromPendingChanges(set, localId, stateKey);
430
+ return;
431
+ }
432
+ const omittedItem = omitSyncFields(item, SYNC_FIELDS);
433
+ if (item.id) {
434
+ const changed = await api.update(item.id, omittedItem);
435
+ if (changed) {
436
+ logger.debug("[Zync] push:update:success", {
437
+ stateKey,
438
+ localId,
439
+ id: item.id
440
+ });
441
+ if (samePendingVersion(get, stateKey, localId, version)) {
442
+ removeFromPendingChanges(set, localId, stateKey);
443
+ }
444
+ return;
445
+ } else {
446
+ logger.warn("[Zync] push:update:missingRemote", {
447
+ stateKey,
448
+ localId,
449
+ id: item.id
450
+ });
451
+ switch (missingStrategy) {
452
+ case "deleteLocalRecord":
453
+ set((s) => ({
454
+ [stateKey]: (s[stateKey] || []).filter((i) => i._localId !== localId)
455
+ }));
456
+ break;
457
+ case "insertNewRemoteRecord": {
458
+ omittedItem._localId = crypto.randomUUID();
459
+ omittedItem.updated_at = (/* @__PURE__ */ new Date()).toISOString();
460
+ set((s) => ({
461
+ [stateKey]: (s[stateKey] || []).map((i) => i._localId === localId ? omittedItem : i)
462
+ }));
463
+ queueToSync("createOrUpdate" /* CreateOrUpdate */, stateKey, omittedItem._localId);
464
+ break;
465
+ }
466
+ }
467
+ removeFromPendingChanges(set, localId, stateKey);
468
+ onMissingRemoteRecordDuringUpdate?.(missingStrategy, omittedItem, omittedItem._localId);
469
+ }
470
+ return;
471
+ }
472
+ const result = await api.add(omittedItem);
473
+ if (result) {
474
+ logger.debug("[Zync] push:create:success", {
475
+ stateKey,
476
+ localId,
477
+ id: result.id
478
+ });
479
+ set((s) => ({
480
+ [stateKey]: (s[stateKey] || []).map((i) => i._localId === localId ? { ...i, ...result } : i)
481
+ }));
482
+ if (samePendingVersion(get, stateKey, localId, version)) {
483
+ removeFromPendingChanges(set, localId, stateKey);
484
+ }
485
+ onAfterRemoteAdd?.(set, get, queueToSync, stateKey, {
486
+ ...item,
487
+ ...result
488
+ });
489
+ } else {
490
+ logger.warn("[Zync] push:create:no-result", {
491
+ stateKey,
492
+ localId
493
+ });
494
+ if (samePendingVersion(get, stateKey, localId, version)) {
495
+ removeFromPendingChanges(set, localId, stateKey);
496
+ }
497
+ }
498
+ break;
499
+ }
500
+ }
501
+ }
502
+
33
503
  // src/indexedDBStorage.ts
34
- var import_idb = require("idb");
35
504
  function createIndexedDBStorage(options) {
36
505
  const dbName = options.dbName;
37
506
  const storeName = options.storeName;
38
- let dbPromise = (0, import_idb.openDB)(dbName, 1, {
39
- upgrade(db) {
40
- if (!db.objectStoreNames.contains(storeName)) {
41
- db.createObjectStore(storeName);
42
- }
507
+ let dbPromise = null;
508
+ async function initDB() {
509
+ if (dbPromise) return dbPromise;
510
+ try {
511
+ const idb = await Promise.resolve().then(() => (init_build(), build_exports));
512
+ dbPromise = idb.openDB(dbName, 1, {
513
+ upgrade(db) {
514
+ if (!db.objectStoreNames.contains(storeName)) {
515
+ db.createObjectStore(storeName);
516
+ }
517
+ }
518
+ });
519
+ return dbPromise;
520
+ } catch (_e) {
521
+ throw new Error('Missing optional dependency "idb". Install it to use IndexedDB storage: npm install idb');
43
522
  }
44
- });
523
+ }
45
524
  async function ensureStore() {
46
- const db = await dbPromise;
525
+ const db = await initDB();
47
526
  if (db.objectStoreNames.contains(storeName)) return;
48
527
  const nextVersion = (db.version || 0) + 1;
49
528
  try {
50
529
  db.close();
51
- } catch (e) {
530
+ } catch (_e) {
52
531
  }
53
- dbPromise = (0, import_idb.openDB)(dbName, nextVersion, {
532
+ const idb = await Promise.resolve().then(() => (init_build(), build_exports));
533
+ dbPromise = idb.openDB(dbName, nextVersion, {
54
534
  upgrade(upg) {
55
535
  if (!upg.objectStoreNames.contains(storeName)) upg.createObjectStore(storeName);
56
536
  }
@@ -59,13 +539,13 @@ function createIndexedDBStorage(options) {
59
539
  }
60
540
  async function withRetry(fn) {
61
541
  try {
62
- const db = await dbPromise;
542
+ const db = await initDB();
63
543
  return await fn(db);
64
544
  } catch (err) {
65
545
  const msg = String(err && err.message ? err.message : err);
66
546
  if (err && (err.name === "NotFoundError" || /objectStore/i.test(msg))) {
67
547
  await ensureStore();
68
- const db2 = await dbPromise;
548
+ const db2 = await initDB();
69
549
  return await fn(db2);
70
550
  }
71
551
  throw err;
@@ -76,19 +556,16 @@ function createIndexedDBStorage(options) {
76
556
  return withRetry(async (db) => {
77
557
  let v = await db.get(storeName, name);
78
558
  v = v ?? null;
79
- console.log("getItem:", db.objectStoreNames, storeName, name, v);
80
559
  return v;
81
560
  });
82
561
  },
83
562
  setItem: async (name, value) => {
84
563
  return withRetry(async (db) => {
85
- console.log("setItem", name, value);
86
564
  await db.put(storeName, value, name);
87
565
  });
88
566
  },
89
567
  removeItem: async (name) => {
90
568
  return withRetry(async (db) => {
91
- console.log("removeItem", name);
92
569
  await db.delete(storeName, name);
93
570
  });
94
571
  }
@@ -105,7 +582,14 @@ var DEFAULT_SYNC_INTERVAL_MILLIS = 5e3;
105
582
  var DEFAULT_LOGGER = console;
106
583
  var DEFAULT_MIN_LOG_LEVEL = "debug";
107
584
  var DEFAULT_MISSING_REMOTE_RECORD_ON_UPDATE_STRATEGY = "ignore";
108
- var SYNC_FIELDS = ["id", "_localId", "updated_at", "deleted"];
585
+ function createWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}) {
586
+ const store = (0, import_zustand.create)(persistWithSync(stateCreator, persistOptions, syncApi, syncOptions));
587
+ return new Promise((resolve) => {
588
+ store.persist.onFinishHydration((_state) => {
589
+ resolve(store);
590
+ });
591
+ });
592
+ }
109
593
  function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}) {
110
594
  const syncInterval = syncOptions.syncInterval ?? DEFAULT_SYNC_INTERVAL_MILLIS;
111
595
  const missingStrategy = syncOptions.missingRemoteRecordDuringUpdateStrategy ?? DEFAULT_MISSING_REMOTE_RECORD_ON_UPDATE_STRATEGY;
@@ -115,14 +599,13 @@ function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}
115
599
  const wrappedPersistOptions = {
116
600
  ...persistOptions,
117
601
  onRehydrateStorage: () => {
118
- logger.debug("[persistWithSync] Rehydration started");
602
+ logger.debug("[Zync] Rehydration started");
119
603
  return (state, error) => {
120
604
  if (error) {
121
- logger.error("[persistWithSync] Rehydration failed", error);
605
+ logger.error("[Zync] Rehydration failed", error);
122
606
  } else {
123
607
  baseOnRehydrate?.(state, error);
124
- state.syncState.status = "idle";
125
- logger.debug("[persistWithSync] Rehydration complete");
608
+ logger.debug("[Zync] Rehydration complete", state);
126
609
  }
127
610
  };
128
611
  },
@@ -137,24 +620,18 @@ function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}
137
620
  lastPulled: syncState.lastPulled
138
621
  }
139
622
  };
623
+ },
624
+ merge: (persisted, current) => {
625
+ const state = { ...current, ...persisted };
626
+ return {
627
+ ...state,
628
+ syncState: {
629
+ ...state.syncState,
630
+ status: "idle"
631
+ // this confirms 'hydrating' is done
632
+ }
633
+ };
140
634
  }
141
- // merge: (persisted: any, current: any) => {
142
- // // Add unpersistable fields back e.g. functions or memory-only fields
143
- // const p = persisted || {};
144
- // const c = current || {};
145
- // return {
146
- // ...c,
147
- // ...p,
148
- // syncState: {
149
- // ...c.syncState,
150
- // ...p.syncState,
151
- // status: 'idle',
152
- // //firstLoadDone: p.syncState?.firstLoadDone ?? c.syncState.firstLoadDone ?? false,
153
- // //pendingChanges: p.syncState?.pendingChanges ?? c.syncState.pendingChanges ?? [],
154
- // //lastPulled: p.syncState?.lastPulled ?? c.syncState.lastPulled ?? {},
155
- // },
156
- // };
157
- // },
158
635
  };
159
636
  const creator = (set, get, storeApi) => {
160
637
  let syncIntervalId;
@@ -174,7 +651,7 @@ function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}
174
651
  await pull(set, get, stateKey, api, logger);
175
652
  } catch (err) {
176
653
  syncError = syncError ?? err;
177
- logger.error(`[persistWithSync] Pull error for stateKey: ${stateKey}`, err);
654
+ logger.error(`[Zync] Pull error for stateKey: ${stateKey}`, err);
178
655
  }
179
656
  }
180
657
  const snapshot = [...get().syncState.pendingChanges || []];
@@ -195,7 +672,7 @@ function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}
195
672
  );
196
673
  } catch (err) {
197
674
  syncError = syncError ?? err;
198
- logger.error(`[persistWithSync] Push error for change: ${change}`, err);
675
+ logger.error(`[Zync] Push error for change: ${change}`, err);
199
676
  }
200
677
  }
201
678
  set((state2) => ({
@@ -205,41 +682,101 @@ function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}
205
682
  error: syncError
206
683
  }
207
684
  }));
685
+ if (get().syncState.pendingChanges.length > 0 && !syncError) {
686
+ await syncOnce();
687
+ }
208
688
  }
209
- function queueToSync(action, stateKey, ...localIds) {
210
- set((state) => {
211
- const queue = state.syncState.pendingChanges || [];
212
- for (const localId of localIds) {
213
- const item = state[stateKey].find((i) => i._localId === localId);
214
- if (!item) {
215
- logger.error("[persistWithSync] queueToSync:no-local-item", {
216
- stateKey,
217
- localId
689
+ async function startFirstLoad() {
690
+ let syncError;
691
+ for (const stateKey of Object.keys(syncApi)) {
692
+ try {
693
+ logger.info(`[Zync] firstLoad:start for stateKey: ${stateKey}`);
694
+ const api = findApi(stateKey, syncApi);
695
+ let lastId;
696
+ while (true) {
697
+ const batch = await api.firstLoad(lastId);
698
+ if (!batch?.length) break;
699
+ set((state) => {
700
+ const local = state[stateKey] || [];
701
+ const localById = new Map(local.filter((l) => l.id).map((l) => [l.id, l]));
702
+ let newest = new Date(state.syncState.lastPulled[stateKey] || 0);
703
+ const next = [...local];
704
+ for (const remote of batch) {
705
+ const remoteUpdated = new Date(remote.updated_at || 0);
706
+ if (remoteUpdated > newest) newest = remoteUpdated;
707
+ if (remote.deleted) continue;
708
+ const localItem = remote.id ? localById.get(remote.id) : void 0;
709
+ if (localItem) {
710
+ const merged = {
711
+ ...localItem,
712
+ ...remote,
713
+ _localId: localItem._localId
714
+ };
715
+ const idx = next.findIndex((i) => i._localId === localItem._localId);
716
+ if (idx >= 0) next[idx] = merged;
717
+ } else {
718
+ next.push({
719
+ ...remote,
720
+ _localId: nextLocalId()
721
+ });
722
+ }
723
+ }
724
+ return {
725
+ [stateKey]: next,
726
+ syncState: {
727
+ ...state.syncState || {},
728
+ lastPulled: {
729
+ ...state.syncState.lastPulled || {},
730
+ [stateKey]: newest.toISOString()
731
+ }
732
+ }
733
+ };
218
734
  });
219
- continue;
735
+ lastId = batch[batch.length - 1].id;
220
736
  }
221
- if (action === "remove" /* Remove */ && !item.id) {
222
- logger.warn("[persistWithSync] queueToSync:remove-no-id", {
737
+ logger.info(`[Zync] firstLoad:done for stateKey: ${stateKey}`);
738
+ } catch (err) {
739
+ syncError = syncError ?? err;
740
+ logger.error(`[Zync] First load pull error for stateKey: ${stateKey}`, err);
741
+ }
742
+ }
743
+ set((state) => ({
744
+ syncState: {
745
+ ...state.syncState || {},
746
+ firstLoadDone: true,
747
+ error: syncError
748
+ }
749
+ }));
750
+ }
751
+ function queueToSync(action, stateKey, ...localIds) {
752
+ set((state) => {
753
+ const pendingChanges = state.syncState.pendingChanges || [];
754
+ for (const localId of localIds) {
755
+ const item = state[stateKey].find((i) => i._localId === localId);
756
+ if (!item) {
757
+ logger.error("[Zync] queueToSync:no-local-item", {
223
758
  stateKey,
224
759
  localId
225
760
  });
226
761
  continue;
227
762
  }
228
- const queueItem = queue.find((p) => p.localId === localId && p.stateKey === stateKey);
763
+ const queueItem = pendingChanges.find((p) => p.localId === localId && p.stateKey === stateKey);
229
764
  if (queueItem) {
230
765
  queueItem.version += 1;
231
766
  if (queueItem.action === "createOrUpdate" /* CreateOrUpdate */ && action === "remove" /* Remove */ && item.id) {
232
767
  queueItem.action = "remove" /* Remove */;
233
768
  queueItem.id = item.id;
234
769
  }
770
+ logger.debug(`[Zync] queueToSync:adjusted ${queueItem.version} ${action} ${item.id} ${stateKey} ${localId}`);
235
771
  } else {
236
- queue.push({ action, stateKey, localId, id: item.id, version: 1 });
772
+ pendingChanges.push({ action, stateKey, localId, id: item.id, version: 1 });
773
+ logger.debug(`[Zync] queueToSync:added ${action} ${item.id} ${stateKey} ${localId}`);
237
774
  }
238
775
  }
239
776
  return {
240
777
  syncState: {
241
778
  ...state.syncState || {},
242
- pendingChanges: queue
779
+ pendingChanges
243
780
  }
244
781
  };
245
782
  });
@@ -280,75 +817,13 @@ function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}
280
817
  }
281
818
  function onVisibilityChange() {
282
819
  if (document.visibilityState === "visible") {
283
- logger.debug("[persistWithSync] Sync started now app is in foreground");
820
+ logger.debug("[Zync] Sync started now app is in foreground");
284
821
  enableSyncTimer(true);
285
822
  } else {
286
- logger.debug("[persistWithSync] Sync paused now app is in background");
823
+ logger.debug("[Zync] Sync paused now app is in background");
287
824
  enableSyncTimer(false);
288
825
  }
289
826
  }
290
- async function startFirstLoad() {
291
- let syncError;
292
- for (const stateKey of Object.keys(syncApi)) {
293
- try {
294
- logger.info(`[persistWithSync] firstLoad:start for stateKey: ${stateKey}`);
295
- const api = findApi(stateKey, syncApi);
296
- let lastId;
297
- while (true) {
298
- const batch = await api.firstLoad(lastId);
299
- if (!batch?.length) break;
300
- set((state) => {
301
- const local = state[stateKey] || [];
302
- const localById = new Map(local.filter((l) => l.id).map((l) => [l.id, l]));
303
- let newest = new Date(state.syncState.lastPulled[stateKey] || 0);
304
- const next = [...local];
305
- for (const remote of batch) {
306
- const remoteUpdated = new Date(remote.updated_at || 0);
307
- if (remoteUpdated > newest) newest = remoteUpdated;
308
- if (remote.deleted) continue;
309
- const localItem = remote.id ? localById.get(remote.id) : void 0;
310
- if (localItem) {
311
- const merged = {
312
- ...localItem,
313
- ...remote,
314
- _localId: localItem._localId
315
- };
316
- const idx = next.findIndex((i) => i._localId === localItem._localId);
317
- if (idx >= 0) next[idx] = merged;
318
- } else {
319
- next.push({
320
- ...remote,
321
- _localId: nextLocalId()
322
- });
323
- }
324
- }
325
- return {
326
- [stateKey]: next,
327
- syncState: {
328
- ...state.syncState || {},
329
- lastPulled: {
330
- ...state.syncState.lastPulled || {},
331
- [stateKey]: newest.toISOString()
332
- }
333
- }
334
- };
335
- });
336
- lastId = batch[batch.length - 1].id;
337
- }
338
- logger.info(`[persistWithSync] firstLoad:done for stateKey: ${stateKey}`);
339
- } catch (err) {
340
- syncError = syncError ?? err;
341
- logger.error(`[persistWithSync] First load pull error for stateKey: ${stateKey}`, err);
342
- }
343
- }
344
- set((state) => ({
345
- syncState: {
346
- ...state.syncState || {},
347
- firstLoadDone: true,
348
- error: syncError
349
- }
350
- }));
351
- }
352
827
  storeApi.sync = {
353
828
  enable,
354
829
  startFirstLoad
@@ -369,220 +844,11 @@ function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}
369
844
  };
370
845
  return (0, import_middleware.persist)(creator, wrappedPersistOptions);
371
846
  }
372
- function createStoreWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}) {
373
- return (0, import_zustand.create)(persistWithSync(stateCreator, persistOptions, syncApi, syncOptions));
374
- }
375
- function orderFor(a) {
376
- switch (a) {
377
- case "createOrUpdate" /* CreateOrUpdate */:
378
- return 1;
379
- case "remove" /* Remove */:
380
- return 2;
381
- }
382
- }
383
- async function pull(set, get, stateKey, api, logger) {
384
- const lastPulled = get().syncState.lastPulled || {};
385
- const lastPulledAt = new Date(lastPulled[stateKey] || /* @__PURE__ */ new Date(0));
386
- logger.debug(`[persistWithSync] pull:start stateKey=${stateKey} since=${lastPulledAt.toISOString()}`);
387
- const serverData = await api.list(lastPulledAt);
388
- if (!serverData?.length) return;
389
- let newest = lastPulledAt;
390
- set((state) => {
391
- const pendingChanges = state.syncState.pendingChanges || [];
392
- const localItems = state[stateKey] || [];
393
- let nextItems = [...localItems];
394
- const localById = new Map(localItems.filter((l) => l.id).map((l) => [l.id, l]));
395
- const pendingRemovalIds = /* @__PURE__ */ new Set();
396
- for (const change of pendingChanges) {
397
- if (change.stateKey === stateKey && change.action === "remove" /* Remove */) {
398
- const item = localItems.find((i) => i._localId === change.localId);
399
- if (item && item.id) pendingRemovalIds.add(item.id);
400
- }
401
- }
402
- for (const remote of serverData) {
403
- const remoteUpdated = new Date(remote.updated_at);
404
- if (remoteUpdated > newest) newest = remoteUpdated;
405
- const localItem = localById.get(remote.id);
406
- if (pendingRemovalIds.has(remote.id)) {
407
- logger.debug(`[persistWithSync] pull:skip-pending-remove stateKey=${stateKey} id=${remote.id}`);
408
- continue;
409
- }
410
- if (remote.deleted) {
411
- if (localItem) {
412
- nextItems = nextItems.filter((i) => i.id !== remote.id);
413
- logger.debug(`[persistWithSync] pull:remove stateKey=${stateKey} id=${remote.id}`);
414
- }
415
- continue;
416
- }
417
- const pending = localItem && pendingChanges.some((p) => p.stateKey === stateKey && p.localId === localItem._localId);
418
- if (localItem && !pending) {
419
- const merged = {
420
- ...localItem,
421
- ...remote,
422
- _localId: localItem._localId
423
- };
424
- nextItems = nextItems.map((i) => i._localId === localItem._localId ? merged : i);
425
- logger.debug(`[persistWithSync] pull:merge stateKey=${stateKey} id=${remote.id}`);
426
- } else if (!localItem) {
427
- nextItems = [...nextItems, { ...remote, _localId: nextLocalId() }];
428
- logger.debug(`[persistWithSync] pull:add stateKey=${stateKey} id=${remote.id}`);
429
- }
430
- }
431
- return {
432
- [stateKey]: nextItems,
433
- syncState: {
434
- ...state.syncState || {},
435
- lastPulled: {
436
- ...state.syncState.lastPulled || {},
437
- [stateKey]: newest.toISOString()
438
- }
439
- }
440
- };
441
- });
442
- }
443
- async function pushOne(set, get, change, api, logger, queueToSync, missingStrategy, onMissingRemoteRecordDuringUpdate, onAfterRemoteAdd) {
444
- logger.debug(`[persistWithSync] push:attempt action=${change.action} stateKey=${change.stateKey} localId=${change.localId}`);
445
- const { action, stateKey, localId, id, version } = change;
446
- switch (action) {
447
- case "remove" /* Remove */:
448
- await api.remove(id);
449
- logger.debug(`[persistWithSync] push:remove:success ${stateKey} ${localId} ${id}`);
450
- removeFromPendingChanges(set, localId, stateKey);
451
- break;
452
- case "createOrUpdate" /* CreateOrUpdate */:
453
- const state = get();
454
- const items = state[stateKey] || [];
455
- const item = items.find((i) => i._localId === localId);
456
- if (!item) {
457
- logger.warn(`[persistWithSync] push:${action}:no-local-item`, {
458
- stateKey,
459
- localId
460
- });
461
- removeFromPendingChanges(set, localId, stateKey);
462
- return;
463
- }
464
- let omittedItem = omitSyncFields(item);
465
- if (item.id) {
466
- const changed = await api.update(item.id, omittedItem);
467
- if (changed) {
468
- logger.debug("[persistWithSync] push:update:success", {
469
- stateKey,
470
- localId,
471
- id: item.id
472
- });
473
- if (samePendingVersion(get, stateKey, localId, version)) {
474
- removeFromPendingChanges(set, localId, stateKey);
475
- }
476
- return;
477
- } else {
478
- logger.warn("[persistWithSync] push:update:missingRemote", {
479
- stateKey,
480
- localId,
481
- id: item.id
482
- });
483
- switch (missingStrategy) {
484
- case "deleteLocalRecord":
485
- set((s) => ({
486
- [stateKey]: (s[stateKey] || []).filter((i) => i._localId !== localId)
487
- }));
488
- break;
489
- case "insertNewRemoteRecord": {
490
- omittedItem._localId = nextLocalId();
491
- omittedItem.updated_at = (/* @__PURE__ */ new Date()).toISOString();
492
- set((s) => ({
493
- [stateKey]: (s[stateKey] || []).map((i) => i._localId === localId ? omittedItem : i)
494
- }));
495
- queueToSync("createOrUpdate" /* CreateOrUpdate */, stateKey, omittedItem._localId);
496
- break;
497
- }
498
- }
499
- removeFromPendingChanges(set, localId, stateKey);
500
- onMissingRemoteRecordDuringUpdate?.(missingStrategy, omittedItem, omittedItem._localId);
501
- }
502
- return;
503
- }
504
- const result = await api.add(omittedItem);
505
- if (result) {
506
- logger.debug("[persistWithSync] push:create:success", {
507
- stateKey,
508
- localId,
509
- id: result.id
510
- });
511
- set((s) => ({
512
- [stateKey]: (s[stateKey] || []).map((i) => i._localId === localId ? { ...i, ...result } : i)
513
- }));
514
- if (samePendingVersion(get, stateKey, localId, version)) {
515
- removeFromPendingChanges(set, localId, stateKey);
516
- }
517
- onAfterRemoteAdd?.(set, get, queueToSync, stateKey, {
518
- ...item,
519
- ...result
520
- });
521
- } else {
522
- logger.warn("[persistWithSync] push:create:no-result", {
523
- stateKey,
524
- localId
525
- });
526
- if (samePendingVersion(get, stateKey, localId, version)) {
527
- removeFromPendingChanges(set, localId, stateKey);
528
- }
529
- }
530
- break;
531
- }
532
- }
533
- function samePendingVersion(get, stateKey, localId, version) {
534
- const q = get().syncState.pendingChanges || [];
535
- const curChange = q.find((p) => p.localId === localId && p.stateKey === stateKey);
536
- return curChange?.version === version;
537
- }
538
- function removeFromPendingChanges(set, localId, stateKey) {
539
- set((s) => {
540
- const queue = (s.syncState.pendingChanges || []).filter((p) => !(p.localId === localId && p.stateKey === stateKey));
541
- return {
542
- syncState: {
543
- ...s.syncState || {},
544
- pendingChanges: queue
545
- }
546
- };
547
- });
548
- }
549
- function omitSyncFields(item) {
550
- const result = { ...item };
551
- for (const k of SYNC_FIELDS) delete result[k];
552
- return result;
553
- }
554
- function nextLocalId() {
555
- return crypto.randomUUID();
556
- }
557
- function newLogger(logger, min) {
558
- const order = {
559
- debug: 10,
560
- info: 20,
561
- warn: 30,
562
- error: 40,
563
- none: 100
564
- };
565
- const threshold = order[min];
566
- const enabled = (lvl) => order[lvl] >= threshold;
567
- return {
568
- debug: (...a) => enabled("debug") && logger.debug?.(...a),
569
- info: (...a) => enabled("info") && logger.info?.(...a),
570
- warn: (...a) => enabled("warn") && logger.warn?.(...a),
571
- error: (...a) => enabled("error") && logger.error?.(...a)
572
- };
573
- }
574
- function findApi(stateKey, syncApi) {
575
- const api = syncApi[stateKey];
576
- if (!api || !api.add || !api.update || !api.remove || !api.list || !api.firstLoad) {
577
- throw new Error(`Missing API function(s) for state key: ${stateKey}.`);
578
- }
579
- return api;
580
- }
581
847
  // Annotate the CommonJS export names for ESM import in node:
582
848
  0 && (module.exports = {
583
849
  SyncAction,
584
850
  createIndexedDBStorage,
585
- createStoreWithSync,
851
+ createWithSync,
586
852
  nextLocalId,
587
853
  persistWithSync
588
854
  });