@anfenn/zync 0.1.9 → 0.1.11

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:create-or-update: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:missing-remote", {
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;
@@ -102,7 +582,14 @@ var DEFAULT_SYNC_INTERVAL_MILLIS = 5e3;
102
582
  var DEFAULT_LOGGER = console;
103
583
  var DEFAULT_MIN_LOG_LEVEL = "debug";
104
584
  var DEFAULT_MISSING_REMOTE_RECORD_ON_UPDATE_STRATEGY = "ignore";
105
- 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
+ }
106
593
  function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}) {
107
594
  const syncInterval = syncOptions.syncInterval ?? DEFAULT_SYNC_INTERVAL_MILLIS;
108
595
  const missingStrategy = syncOptions.missingRemoteRecordDuringUpdateStrategy ?? DEFAULT_MISSING_REMOTE_RECORD_ON_UPDATE_STRATEGY;
@@ -112,13 +599,13 @@ function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}
112
599
  const wrappedPersistOptions = {
113
600
  ...persistOptions,
114
601
  onRehydrateStorage: () => {
115
- logger.debug("[persistWithSync] Rehydration started");
602
+ logger.debug("[zync] rehydration:start");
116
603
  return (state, error) => {
117
604
  if (error) {
118
- logger.error("[persistWithSync] Rehydration failed", error);
605
+ logger.error("[zync] rehydration:failed", error);
119
606
  } else {
120
607
  baseOnRehydrate?.(state, error);
121
- logger.debug("[persistWithSync] Rehydration complete", state.syncState);
608
+ logger.debug("[zync] rehydration:complete", state);
122
609
  }
123
610
  };
124
611
  },
@@ -135,12 +622,11 @@ function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}
135
622
  };
136
623
  },
137
624
  merge: (persisted, current) => {
625
+ const state = { ...current, ...persisted };
138
626
  return {
139
- ...current,
140
- ...persisted,
627
+ ...state,
141
628
  syncState: {
142
- ...current?.syncState,
143
- ...persisted?.syncState,
629
+ ...state.syncState,
144
630
  status: "idle"
145
631
  // this confirms 'hydrating' is done
146
632
  }
@@ -165,7 +651,7 @@ function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}
165
651
  await pull(set, get, stateKey, api, logger);
166
652
  } catch (err) {
167
653
  syncError = syncError ?? err;
168
- logger.error(`[persistWithSync] Pull error for stateKey: ${stateKey}`, err);
654
+ logger.error(`[zync] pull:error stateKey=${stateKey}`, err);
169
655
  }
170
656
  }
171
657
  const snapshot = [...get().syncState.pendingChanges || []];
@@ -186,7 +672,7 @@ function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}
186
672
  );
187
673
  } catch (err) {
188
674
  syncError = syncError ?? err;
189
- logger.error(`[persistWithSync] Push error for change: ${change}`, err);
675
+ logger.error(`[zync] push:error change=${change}`, err);
190
676
  }
191
677
  }
192
678
  set((state2) => ({
@@ -200,92 +686,11 @@ function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}
200
686
  await syncOnce();
201
687
  }
202
688
  }
203
- function queueToSync(action, stateKey, ...localIds) {
204
- set((state) => {
205
- const queue = state.syncState.pendingChanges || [];
206
- for (const localId of localIds) {
207
- const item = state[stateKey].find((i) => i._localId === localId);
208
- if (!item) {
209
- logger.error("[persistWithSync] queueToSync:no-local-item", {
210
- stateKey,
211
- localId
212
- });
213
- continue;
214
- }
215
- if (action === "remove" /* Remove */ && !item.id) {
216
- logger.warn("[persistWithSync] queueToSync:remove-no-id", {
217
- stateKey,
218
- localId
219
- });
220
- continue;
221
- }
222
- const queueItem = queue.find((p) => p.localId === localId && p.stateKey === stateKey);
223
- if (queueItem) {
224
- queueItem.version += 1;
225
- if (queueItem.action === "createOrUpdate" /* CreateOrUpdate */ && action === "remove" /* Remove */ && item.id) {
226
- queueItem.action = "remove" /* Remove */;
227
- queueItem.id = item.id;
228
- }
229
- } else {
230
- queue.push({ action, stateKey, localId, id: item.id, version: 1 });
231
- }
232
- }
233
- return {
234
- syncState: {
235
- ...state.syncState || {},
236
- pendingChanges: queue
237
- }
238
- };
239
- });
240
- syncOnce();
241
- }
242
- function setAndSync(partial) {
243
- if (typeof partial === "function") {
244
- set((state) => ({ ...partial(state) }));
245
- } else {
246
- set(partial);
247
- }
248
- syncOnce();
249
- }
250
- function enable(enabled) {
251
- set((state) => ({
252
- syncState: {
253
- ...state.syncState || {},
254
- enabled
255
- }
256
- }));
257
- enableSyncTimer(enabled);
258
- addVisibilityChangeListener(enabled);
259
- }
260
- function enableSyncTimer(enabled) {
261
- clearInterval(syncIntervalId);
262
- syncIntervalId = void 0;
263
- if (enabled) {
264
- syncIntervalId = setInterval(syncOnce, syncInterval);
265
- syncOnce();
266
- }
267
- }
268
- function addVisibilityChangeListener(add) {
269
- if (add) {
270
- document.addEventListener("visibilitychange", onVisibilityChange);
271
- } else {
272
- document.removeEventListener("visibilitychange", onVisibilityChange);
273
- }
274
- }
275
- function onVisibilityChange() {
276
- if (document.visibilityState === "visible") {
277
- logger.debug("[persistWithSync] Sync started now app is in foreground");
278
- enableSyncTimer(true);
279
- } else {
280
- logger.debug("[persistWithSync] Sync paused now app is in background");
281
- enableSyncTimer(false);
282
- }
283
- }
284
689
  async function startFirstLoad() {
285
690
  let syncError;
286
691
  for (const stateKey of Object.keys(syncApi)) {
287
692
  try {
288
- logger.info(`[persistWithSync] firstLoad:start for stateKey: ${stateKey}`);
693
+ logger.info(`[zync] firstLoad:start stateKey=${stateKey}`);
289
694
  const api = findApi(stateKey, syncApi);
290
695
  let lastId;
291
696
  while (true) {
@@ -329,10 +734,10 @@ function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}
329
734
  });
330
735
  lastId = batch[batch.length - 1].id;
331
736
  }
332
- logger.info(`[persistWithSync] firstLoad:done for stateKey: ${stateKey}`);
737
+ logger.info(`[zync] firstLoad:done stateKey=${stateKey}`);
333
738
  } catch (err) {
334
739
  syncError = syncError ?? err;
335
- logger.error(`[persistWithSync] First load pull error for stateKey: ${stateKey}`, err);
740
+ logger.error(`[zync] firstLoad:error stateKey=${stateKey}`, err);
336
741
  }
337
742
  }
338
743
  set((state) => ({
@@ -343,6 +748,79 @@ function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}
343
748
  }
344
749
  }));
345
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 localId=${localId}`);
758
+ continue;
759
+ }
760
+ const queueItem = pendingChanges.find((p) => p.localId === localId && p.stateKey === stateKey);
761
+ if (queueItem) {
762
+ queueItem.version += 1;
763
+ if (queueItem.action === "createOrUpdate" /* CreateOrUpdate */ && action === "remove" /* Remove */ && item.id) {
764
+ queueItem.action = "remove" /* Remove */;
765
+ queueItem.id = item.id;
766
+ }
767
+ logger.debug(`[zync] queueToSync:adjusted v=${queueItem.version} action=${action} localId=${localId}`);
768
+ } else {
769
+ pendingChanges.push({ action, stateKey, localId, id: item.id, version: 1 });
770
+ logger.debug(`[zync] queueToSync:added action=${action} localId=${localId}`);
771
+ }
772
+ }
773
+ return {
774
+ syncState: {
775
+ ...state.syncState || {},
776
+ pendingChanges
777
+ }
778
+ };
779
+ });
780
+ syncOnce();
781
+ }
782
+ function setAndSync(partial) {
783
+ if (typeof partial === "function") {
784
+ set((state) => ({ ...partial(state) }));
785
+ } else {
786
+ set(partial);
787
+ }
788
+ syncOnce();
789
+ }
790
+ function enable(enabled) {
791
+ set((state) => ({
792
+ syncState: {
793
+ ...state.syncState || {},
794
+ enabled
795
+ }
796
+ }));
797
+ enableSyncTimer(enabled);
798
+ addVisibilityChangeListener(enabled);
799
+ }
800
+ function enableSyncTimer(enabled) {
801
+ clearInterval(syncIntervalId);
802
+ syncIntervalId = void 0;
803
+ if (enabled) {
804
+ syncIntervalId = setInterval(syncOnce, syncInterval);
805
+ syncOnce();
806
+ }
807
+ }
808
+ function addVisibilityChangeListener(add) {
809
+ if (add) {
810
+ document.addEventListener("visibilitychange", onVisibilityChange);
811
+ } else {
812
+ document.removeEventListener("visibilitychange", onVisibilityChange);
813
+ }
814
+ }
815
+ function onVisibilityChange() {
816
+ if (document.visibilityState === "visible") {
817
+ logger.debug("[zync] sync:start-in-foreground");
818
+ enableSyncTimer(true);
819
+ } else {
820
+ logger.debug("[zync] sync:pause-in-background");
821
+ enableSyncTimer(false);
822
+ }
823
+ }
346
824
  storeApi.sync = {
347
825
  enable,
348
826
  startFirstLoad
@@ -363,220 +841,11 @@ function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}
363
841
  };
364
842
  return (0, import_middleware.persist)(creator, wrappedPersistOptions);
365
843
  }
366
- function createStoreWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}) {
367
- return (0, import_zustand.create)(persistWithSync(stateCreator, persistOptions, syncApi, syncOptions));
368
- }
369
- function orderFor(a) {
370
- switch (a) {
371
- case "createOrUpdate" /* CreateOrUpdate */:
372
- return 1;
373
- case "remove" /* Remove */:
374
- return 2;
375
- }
376
- }
377
- async function pull(set, get, stateKey, api, logger) {
378
- const lastPulled = get().syncState.lastPulled || {};
379
- const lastPulledAt = new Date(lastPulled[stateKey] || /* @__PURE__ */ new Date(0));
380
- logger.debug(`[persistWithSync] pull:start stateKey=${stateKey} since=${lastPulledAt.toISOString()}`);
381
- const serverData = await api.list(lastPulledAt);
382
- if (!serverData?.length) return;
383
- let newest = lastPulledAt;
384
- set((state) => {
385
- const pendingChanges = state.syncState.pendingChanges || [];
386
- const localItems = state[stateKey] || [];
387
- let nextItems = [...localItems];
388
- const localById = new Map(localItems.filter((l) => l.id).map((l) => [l.id, l]));
389
- const pendingRemovalIds = /* @__PURE__ */ new Set();
390
- for (const change of pendingChanges) {
391
- if (change.stateKey === stateKey && change.action === "remove" /* Remove */) {
392
- const item = localItems.find((i) => i._localId === change.localId);
393
- if (item && item.id) pendingRemovalIds.add(item.id);
394
- }
395
- }
396
- for (const remote of serverData) {
397
- const remoteUpdated = new Date(remote.updated_at);
398
- if (remoteUpdated > newest) newest = remoteUpdated;
399
- const localItem = localById.get(remote.id);
400
- if (pendingRemovalIds.has(remote.id)) {
401
- logger.debug(`[persistWithSync] pull:skip-pending-remove stateKey=${stateKey} id=${remote.id}`);
402
- continue;
403
- }
404
- if (remote.deleted) {
405
- if (localItem) {
406
- nextItems = nextItems.filter((i) => i.id !== remote.id);
407
- logger.debug(`[persistWithSync] pull:remove stateKey=${stateKey} id=${remote.id}`);
408
- }
409
- continue;
410
- }
411
- const pending = localItem && pendingChanges.some((p) => p.stateKey === stateKey && p.localId === localItem._localId);
412
- if (localItem && !pending) {
413
- const merged = {
414
- ...localItem,
415
- ...remote,
416
- _localId: localItem._localId
417
- };
418
- nextItems = nextItems.map((i) => i._localId === localItem._localId ? merged : i);
419
- logger.debug(`[persistWithSync] pull:merge stateKey=${stateKey} id=${remote.id}`);
420
- } else if (!localItem) {
421
- nextItems = [...nextItems, { ...remote, _localId: nextLocalId() }];
422
- logger.debug(`[persistWithSync] pull:add stateKey=${stateKey} id=${remote.id}`);
423
- }
424
- }
425
- return {
426
- [stateKey]: nextItems,
427
- syncState: {
428
- ...state.syncState || {},
429
- lastPulled: {
430
- ...state.syncState.lastPulled || {},
431
- [stateKey]: newest.toISOString()
432
- }
433
- }
434
- };
435
- });
436
- }
437
- async function pushOne(set, get, change, api, logger, queueToSync, missingStrategy, onMissingRemoteRecordDuringUpdate, onAfterRemoteAdd) {
438
- logger.debug(`[persistWithSync] push:attempt action=${change.action} stateKey=${change.stateKey} localId=${change.localId}`);
439
- const { action, stateKey, localId, id, version } = change;
440
- switch (action) {
441
- case "remove" /* Remove */:
442
- await api.remove(id);
443
- logger.debug(`[persistWithSync] push:remove:success ${stateKey} ${localId} ${id}`);
444
- removeFromPendingChanges(set, localId, stateKey);
445
- break;
446
- case "createOrUpdate" /* CreateOrUpdate */:
447
- const state = get();
448
- const items = state[stateKey] || [];
449
- const item = items.find((i) => i._localId === localId);
450
- if (!item) {
451
- logger.warn(`[persistWithSync] push:${action}:no-local-item`, {
452
- stateKey,
453
- localId
454
- });
455
- removeFromPendingChanges(set, localId, stateKey);
456
- return;
457
- }
458
- let omittedItem = omitSyncFields(item);
459
- if (item.id) {
460
- const changed = await api.update(item.id, omittedItem);
461
- if (changed) {
462
- logger.debug("[persistWithSync] push:update:success", {
463
- stateKey,
464
- localId,
465
- id: item.id
466
- });
467
- if (samePendingVersion(get, stateKey, localId, version)) {
468
- removeFromPendingChanges(set, localId, stateKey);
469
- }
470
- return;
471
- } else {
472
- logger.warn("[persistWithSync] push:update:missingRemote", {
473
- stateKey,
474
- localId,
475
- id: item.id
476
- });
477
- switch (missingStrategy) {
478
- case "deleteLocalRecord":
479
- set((s) => ({
480
- [stateKey]: (s[stateKey] || []).filter((i) => i._localId !== localId)
481
- }));
482
- break;
483
- case "insertNewRemoteRecord": {
484
- omittedItem._localId = nextLocalId();
485
- omittedItem.updated_at = (/* @__PURE__ */ new Date()).toISOString();
486
- set((s) => ({
487
- [stateKey]: (s[stateKey] || []).map((i) => i._localId === localId ? omittedItem : i)
488
- }));
489
- queueToSync("createOrUpdate" /* CreateOrUpdate */, stateKey, omittedItem._localId);
490
- break;
491
- }
492
- }
493
- removeFromPendingChanges(set, localId, stateKey);
494
- onMissingRemoteRecordDuringUpdate?.(missingStrategy, omittedItem, omittedItem._localId);
495
- }
496
- return;
497
- }
498
- const result = await api.add(omittedItem);
499
- if (result) {
500
- logger.debug("[persistWithSync] push:create:success", {
501
- stateKey,
502
- localId,
503
- id: result.id
504
- });
505
- set((s) => ({
506
- [stateKey]: (s[stateKey] || []).map((i) => i._localId === localId ? { ...i, ...result } : i)
507
- }));
508
- if (samePendingVersion(get, stateKey, localId, version)) {
509
- removeFromPendingChanges(set, localId, stateKey);
510
- }
511
- onAfterRemoteAdd?.(set, get, queueToSync, stateKey, {
512
- ...item,
513
- ...result
514
- });
515
- } else {
516
- logger.warn("[persistWithSync] push:create:no-result", {
517
- stateKey,
518
- localId
519
- });
520
- if (samePendingVersion(get, stateKey, localId, version)) {
521
- removeFromPendingChanges(set, localId, stateKey);
522
- }
523
- }
524
- break;
525
- }
526
- }
527
- function samePendingVersion(get, stateKey, localId, version) {
528
- const q = get().syncState.pendingChanges || [];
529
- const curChange = q.find((p) => p.localId === localId && p.stateKey === stateKey);
530
- return curChange?.version === version;
531
- }
532
- function removeFromPendingChanges(set, localId, stateKey) {
533
- set((s) => {
534
- const queue = (s.syncState.pendingChanges || []).filter((p) => !(p.localId === localId && p.stateKey === stateKey));
535
- return {
536
- syncState: {
537
- ...s.syncState || {},
538
- pendingChanges: queue
539
- }
540
- };
541
- });
542
- }
543
- function omitSyncFields(item) {
544
- const result = { ...item };
545
- for (const k of SYNC_FIELDS) delete result[k];
546
- return result;
547
- }
548
- function nextLocalId() {
549
- return crypto.randomUUID();
550
- }
551
- function newLogger(logger, min) {
552
- const order = {
553
- debug: 10,
554
- info: 20,
555
- warn: 30,
556
- error: 40,
557
- none: 100
558
- };
559
- const threshold = order[min];
560
- const enabled = (lvl) => order[lvl] >= threshold;
561
- return {
562
- debug: (...a) => enabled("debug") && logger.debug?.(...a),
563
- info: (...a) => enabled("info") && logger.info?.(...a),
564
- warn: (...a) => enabled("warn") && logger.warn?.(...a),
565
- error: (...a) => enabled("error") && logger.error?.(...a)
566
- };
567
- }
568
- function findApi(stateKey, syncApi) {
569
- const api = syncApi[stateKey];
570
- if (!api || !api.add || !api.update || !api.remove || !api.list || !api.firstLoad) {
571
- throw new Error(`Missing API function(s) for state key: ${stateKey}.`);
572
- }
573
- return api;
574
- }
575
844
  // Annotate the CommonJS export names for ESM import in node:
576
845
  0 && (module.exports = {
577
846
  SyncAction,
578
847
  createIndexedDBStorage,
579
- createStoreWithSync,
848
+ createWithSync,
580
849
  nextLocalId,
581
850
  persistWithSync
582
851
  });