@anfenn/zync 0.1.4 → 0.1.5

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
@@ -22,6 +22,7 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  SyncAction: () => SyncAction,
24
24
  createIndexedDBStorage: () => createIndexedDBStorage,
25
+ createStoreWithSync: () => createStoreWithSync,
25
26
  nextLocalId: () => nextLocalId,
26
27
  persistWithSync: () => persistWithSync
27
28
  });
@@ -30,171 +31,85 @@ var import_zustand = require("zustand");
30
31
  var import_middleware = require("zustand/middleware");
31
32
 
32
33
  // src/indexedDBStorage.ts
33
- function createIndexedDBStorage(options = {}) {
34
- if (typeof globalThis.localStorage === "undefined") {
35
- const mem = {};
36
- globalThis.localStorage = {
37
- getItem: (k) => k in mem ? mem[k] : null,
38
- setItem: (k, v) => {
39
- mem[k] = v;
40
- },
41
- removeItem: (k) => {
42
- delete mem[k];
43
- },
44
- clear: () => {
45
- Object.keys(mem).forEach((k) => delete mem[k]);
34
+ var import_idb = require("idb");
35
+ function createIndexedDBStorage(options) {
36
+ const dbName = options.dbName;
37
+ 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);
46
42
  }
47
- };
48
- }
49
- const dbName = options.dbName ?? "zustand-persist";
50
- const storeName = options.storeName ?? "keyval";
51
- const version = options.version ?? 1;
52
- const log = options.logger ?? { warn: () => {
53
- }, error: () => {
54
- } };
55
- const canUseIDB = typeof window !== "undefined" && "indexedDB" in window;
56
- if (!canUseIDB) {
57
- log.warn("[indexedDBStorage] IndexedDB not available \u2013 falling back to localStorage");
58
- return localStorageFallback();
59
- }
60
- let dbPromise;
61
- function openDB() {
62
- if (dbPromise) return dbPromise;
63
- dbPromise = new Promise((resolve, reject) => {
64
- try {
65
- const req = indexedDB.open(dbName, version);
66
- req.onupgradeneeded = () => {
67
- const db = req.result;
68
- if (!db.objectStoreNames.contains(storeName)) {
69
- db.createObjectStore(storeName);
70
- }
71
- };
72
- req.onsuccess = () => resolve(req.result);
73
- req.onerror = () => reject(req.error);
74
- req.onblocked = () => log.warn("[indexedDBStorage] open blocked");
75
- } catch (e) {
76
- reject(e);
77
- }
78
- }).catch((e) => {
79
- log.error("[indexedDBStorage] Failed to open DB, falling back to localStorage", e);
80
- return void 0;
81
- });
82
- return dbPromise;
83
- }
84
- async function withStore(mode, fn) {
85
- const db = await openDB();
86
- if (!db) {
87
- return fn(null);
88
43
  }
89
- return new Promise((resolve, reject) => {
90
- let tx;
91
- try {
92
- tx = db.transaction(storeName, mode);
93
- } catch (_e) {
94
- dbPromise = void 0;
95
- openDB().then((fresh) => {
96
- try {
97
- if (!fresh) {
98
- const result3 = fn(null);
99
- Promise.resolve(result3).then(resolve, reject);
100
- return;
101
- }
102
- const retryTx = fresh.transaction(storeName, mode);
103
- const store2 = retryTx.objectStore(storeName);
104
- const result2 = fn(store2);
105
- Promise.resolve(result2).then(resolve, reject);
106
- } catch (err) {
107
- reject(err);
108
- }
109
- }, reject);
110
- return;
111
- }
112
- const store = tx.objectStore(storeName);
113
- let result;
114
- try {
115
- result = fn(store);
116
- } catch (err) {
117
- reject(err);
118
- return;
119
- }
120
- tx.oncomplete = () => resolve(result);
121
- tx.onerror = () => reject(tx.error);
122
- tx.onabort = () => reject(tx.error || new Error("Transaction aborted"));
123
- });
124
- }
125
- async function getItem(name) {
126
- return withStore("readonly", (store) => {
127
- console.log("READ STORE");
128
- if (!store) return localStorage.getItem(name);
129
- return new Promise((resolve, reject) => {
130
- const req = store.get(name);
131
- req.onsuccess = () => resolve(req.result ?? null);
132
- req.onerror = () => reject(req.error);
133
- });
134
- });
135
- }
136
- async function setItem(name, value) {
137
- return withStore("readwrite", (store) => {
138
- console.log("WRITE STORE");
139
- if (!store) {
140
- localStorage.setItem(name, value);
141
- return;
44
+ });
45
+ async function ensureStore() {
46
+ const db = await dbPromise;
47
+ if (db.objectStoreNames.contains(storeName)) return;
48
+ const nextVersion = (db.version || 0) + 1;
49
+ try {
50
+ db.close();
51
+ } catch (e) {
52
+ }
53
+ dbPromise = (0, import_idb.openDB)(dbName, nextVersion, {
54
+ upgrade(upg) {
55
+ if (!upg.objectStoreNames.contains(storeName)) upg.createObjectStore(storeName);
142
56
  }
143
- return new Promise((resolve, reject) => {
144
- const req = store.put(value, name);
145
- req.onsuccess = () => resolve();
146
- req.onerror = () => reject(req.error);
147
- });
148
57
  });
58
+ await dbPromise;
149
59
  }
150
- async function removeItem(name) {
151
- return withStore("readwrite", (store) => {
152
- if (!store) {
153
- localStorage.removeItem(name);
154
- return;
60
+ async function withRetry(fn) {
61
+ try {
62
+ const db = await dbPromise;
63
+ return await fn(db);
64
+ } catch (err) {
65
+ const msg = String(err && err.message ? err.message : err);
66
+ if (err && (err.name === "NotFoundError" || /objectStore/i.test(msg))) {
67
+ await ensureStore();
68
+ const db2 = await dbPromise;
69
+ return await fn(db2);
155
70
  }
156
- return new Promise((resolve, reject) => {
157
- const req = store.delete(name);
158
- req.onsuccess = () => resolve();
159
- req.onerror = () => reject(req.error);
160
- });
161
- });
71
+ throw err;
72
+ }
162
73
  }
163
- return { getItem, setItem, removeItem };
164
- }
165
- function localStorageFallback() {
166
- const ls = globalThis.localStorage;
167
74
  return {
168
- getItem: (n) => Promise.resolve(ls?.getItem ? ls.getItem(n) : null),
169
- setItem: (n, v) => {
170
- ls?.setItem?.(n, v);
171
- return Promise.resolve();
75
+ getItem: async (name) => {
76
+ return withRetry(async (db) => {
77
+ let v = await db.get(storeName, name);
78
+ v = v ?? null;
79
+ console.log("getItem:", db.objectStoreNames, storeName, name, v);
80
+ return v;
81
+ });
172
82
  },
173
- removeItem: (n) => {
174
- ls?.removeItem?.(n);
175
- return Promise.resolve();
83
+ setItem: async (name, value) => {
84
+ return withRetry(async (db) => {
85
+ console.log("setItem", name, value);
86
+ await db.put(storeName, value, name);
87
+ });
88
+ },
89
+ removeItem: async (name) => {
90
+ return withRetry(async (db) => {
91
+ console.log("removeItem", name);
92
+ await db.delete(storeName, name);
93
+ });
176
94
  }
177
95
  };
178
96
  }
179
97
 
180
98
  // src/index.ts
181
99
  var SyncAction = /* @__PURE__ */ ((SyncAction2) => {
182
- SyncAction2["Create"] = "create";
183
- SyncAction2["Update"] = "update";
100
+ SyncAction2["CreateOrUpdate"] = "createOrUpdate";
184
101
  SyncAction2["Remove"] = "remove";
185
102
  return SyncAction2;
186
103
  })(SyncAction || {});
187
104
  var DEFAULT_SYNC_INTERVAL_MILLIS = 5e3;
188
105
  var DEFAULT_LOGGER = console;
189
106
  var DEFAULT_MIN_LOG_LEVEL = "debug";
190
- var DEFAULT_MISSING_REMOTE_RECORD_ON_UPDATE_STRATEGY = "insertNewRemoteRecord";
107
+ var DEFAULT_MISSING_REMOTE_RECORD_ON_UPDATE_STRATEGY = "ignore";
191
108
  var SYNC_FIELDS = ["id", "_localId", "updated_at", "deleted"];
192
109
  function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}) {
193
110
  const syncInterval = syncOptions.syncInterval ?? DEFAULT_SYNC_INTERVAL_MILLIS;
194
111
  const missingStrategy = syncOptions.missingRemoteRecordDuringUpdateStrategy ?? DEFAULT_MISSING_REMOTE_RECORD_ON_UPDATE_STRATEGY;
195
112
  const logger = newLogger(syncOptions.logger ?? DEFAULT_LOGGER, syncOptions.minLogLevel ?? DEFAULT_MIN_LOG_LEVEL);
196
- let startSync;
197
- let syncIntervalId;
198
113
  const baseOnRehydrate = persistOptions?.onRehydrateStorage;
199
114
  const basePartialize = persistOptions?.partialize;
200
115
  const wrappedPersistOptions = {
@@ -206,47 +121,52 @@ function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}
206
121
  logger.error("[persistWithSync] Rehydration failed", error);
207
122
  } else {
208
123
  baseOnRehydrate?.(state, error);
124
+ state.syncState.status = "idle";
209
125
  logger.debug("[persistWithSync] Rehydration complete");
210
- startSync?.();
211
126
  }
212
127
  };
213
128
  },
214
129
  partialize: (s) => {
215
130
  const base = basePartialize ? basePartialize(s) : s;
216
- const { syncState: _sync, ...rest } = base || {};
131
+ const { syncState, ...rest } = base || {};
217
132
  return {
218
133
  ...rest,
219
134
  syncState: {
220
- firstLoadDone: _sync?.firstLoadDone ?? false
221
- }
222
- };
223
- },
224
- merge: (persisted, current) => {
225
- const p = persisted || {};
226
- const c = current || {};
227
- return {
228
- ...c,
229
- ...p,
230
- syncState: {
231
- ...c.syncState,
232
- firstLoadDone: p.syncState?.firstLoadDone ?? c.syncState?.firstLoadDone ?? false,
233
- status: "idle"
135
+ firstLoadDone: syncState.firstLoadDone,
136
+ pendingChanges: syncState.pendingChanges,
137
+ lastPulled: syncState.lastPulled
234
138
  }
235
139
  };
236
140
  }
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
+ // },
237
158
  };
238
- const creator = (set, get) => {
159
+ const creator = (set, get, storeApi) => {
160
+ let syncIntervalId;
239
161
  async function syncOnce() {
240
162
  const state = get();
241
163
  if (!state.syncState.enabled || state.syncState.status !== "idle") return;
242
- set((state2) => {
243
- return {
244
- syncState: {
245
- ...state2.syncState,
246
- status: "syncing"
247
- }
248
- };
249
- });
164
+ set((state2) => ({
165
+ syncState: {
166
+ ...state2.syncState || {},
167
+ status: "syncing"
168
+ }
169
+ }));
250
170
  let syncError;
251
171
  for (const stateKey of Object.keys(syncApi)) {
252
172
  try {
@@ -257,7 +177,7 @@ function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}
257
177
  logger.error(`[persistWithSync] Pull error for stateKey: ${stateKey}`, err);
258
178
  }
259
179
  }
260
- const snapshot = [...get()._pendingChanges || []];
180
+ const snapshot = [...get().syncState.pendingChanges || []];
261
181
  snapshot.sort((a, b) => orderFor(a.action) - orderFor(b.action));
262
182
  for (const change of snapshot) {
263
183
  try {
@@ -278,83 +198,95 @@ function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}
278
198
  logger.error(`[persistWithSync] Push error for change: ${change}`, err);
279
199
  }
280
200
  }
281
- set((state2) => {
282
- return {
283
- syncState: {
284
- ...state2.syncState,
285
- status: "idle",
286
- error: syncError
287
- }
288
- };
289
- });
201
+ set((state2) => ({
202
+ syncState: {
203
+ ...state2.syncState || {},
204
+ status: "idle",
205
+ error: syncError
206
+ }
207
+ }));
290
208
  }
291
- startSync = async () => {
292
- clearInterval(syncIntervalId);
293
- syncIntervalId = void 0;
294
- await syncOnce();
295
- syncIntervalId = setInterval(syncOnce, syncInterval);
296
- };
297
- function queueToSync(action, localId, stateKey, changes) {
209
+ function queueToSync(action, stateKey, ...localIds) {
298
210
  set((state) => {
299
- const queue = state._pendingChanges || [];
300
- const idx = queue.findIndex((p) => p.localId === localId && p.stateKey === stateKey);
301
- if (idx >= 0) {
302
- const existing = queue[idx];
303
- switch (existing?.action) {
304
- case "create" /* Create */:
305
- if (action === "update" /* Update */) {
306
- existing.changes = { ...existing.changes, ...changes };
307
- } else if (action === "remove" /* Remove */) {
308
- queue.splice(idx, 1);
309
- }
310
- break;
311
- case "update" /* Update */:
312
- if (action === "update" /* Update */) {
313
- existing.changes = { ...existing.changes, ...changes };
314
- } else if (action === "remove" /* Remove */) {
315
- existing.action = "remove" /* Remove */;
316
- delete existing.changes;
317
- }
318
- break;
319
- case "remove" /* Remove */:
320
- break;
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
218
+ });
219
+ continue;
321
220
  }
322
- } else {
323
- if (action === "remove" /* Remove */) {
324
- const item = state[stateKey].find((i) => i._localId === localId);
325
- if (item) changes = { id: item.id };
221
+ if (action === "remove" /* Remove */ && !item.id) {
222
+ logger.warn("[persistWithSync] queueToSync:remove-no-id", {
223
+ stateKey,
224
+ localId
225
+ });
226
+ continue;
227
+ }
228
+ const queueItem = queue.find((p) => p.localId === localId && p.stateKey === stateKey);
229
+ if (queueItem) {
230
+ queueItem.version += 1;
231
+ if (queueItem.action === "createOrUpdate" /* CreateOrUpdate */ && action === "remove" /* Remove */ && item.id) {
232
+ queueItem.action = "remove" /* Remove */;
233
+ queueItem.id = item.id;
234
+ }
235
+ } else {
236
+ queue.push({ action, stateKey, localId, id: item.id, version: 1 });
326
237
  }
327
- queue.push({ stateKey, localId, action, changes });
328
238
  }
329
- state._pendingChanges = queue;
330
- return state;
331
- });
332
- syncOnce();
333
- }
334
- function setAndSync(partial) {
335
- set(partial);
336
- syncOnce();
337
- }
338
- function enableSync(enabled) {
339
- set((state) => {
340
239
  return {
341
240
  syncState: {
342
- ...state.syncState,
343
- enabled
241
+ ...state.syncState || {},
242
+ pendingChanges: queue
344
243
  }
345
244
  };
346
245
  });
347
246
  syncOnce();
348
247
  }
349
- document.addEventListener("visibilitychange", async () => {
248
+ function setAndSync(partial) {
249
+ if (typeof partial === "function") {
250
+ set((state) => ({ ...partial(state) }));
251
+ } else {
252
+ set(partial);
253
+ }
254
+ syncOnce();
255
+ }
256
+ function enable(enabled) {
257
+ set((state) => ({
258
+ syncState: {
259
+ ...state.syncState || {},
260
+ enabled
261
+ }
262
+ }));
263
+ enableSyncTimer(enabled);
264
+ addVisibilityChangeListener(enabled);
265
+ }
266
+ function enableSyncTimer(enabled) {
350
267
  clearInterval(syncIntervalId);
268
+ syncIntervalId = void 0;
269
+ if (enabled) {
270
+ syncIntervalId = setInterval(syncOnce, syncInterval);
271
+ syncOnce();
272
+ }
273
+ }
274
+ function addVisibilityChangeListener(add) {
275
+ if (add) {
276
+ document.addEventListener("visibilitychange", onVisibilityChange);
277
+ } else {
278
+ document.removeEventListener("visibilitychange", onVisibilityChange);
279
+ }
280
+ }
281
+ function onVisibilityChange() {
351
282
  if (document.visibilityState === "visible") {
352
- logger.debug("[persistWithSync] Sync starting now app is in foreground");
353
- await startSync?.();
283
+ logger.debug("[persistWithSync] Sync started now app is in foreground");
284
+ enableSyncTimer(true);
354
285
  } else {
355
286
  logger.debug("[persistWithSync] Sync paused now app is in background");
287
+ enableSyncTimer(false);
356
288
  }
357
- });
289
+ }
358
290
  async function startFirstLoad() {
359
291
  let syncError;
360
292
  for (const stateKey of Object.keys(syncApi)) {
@@ -368,7 +300,7 @@ function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}
368
300
  set((state) => {
369
301
  const local = state[stateKey] || [];
370
302
  const localById = new Map(local.filter((l) => l.id).map((l) => [l.id, l]));
371
- let newest = new Date(state._lastPulled?.[stateKey] || 0);
303
+ let newest = new Date(state.syncState.lastPulled[stateKey] || 0);
372
304
  const next = [...local];
373
305
  for (const remote of batch) {
374
306
  const remoteUpdated = new Date(remote.updated_at || 0);
@@ -376,19 +308,30 @@ function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}
376
308
  if (remote.deleted) continue;
377
309
  const localItem = remote.id ? localById.get(remote.id) : void 0;
378
310
  if (localItem) {
379
- const merged = { ...localItem, ...remote, _localId: localItem._localId };
311
+ const merged = {
312
+ ...localItem,
313
+ ...remote,
314
+ _localId: localItem._localId
315
+ };
380
316
  const idx = next.findIndex((i) => i._localId === localItem._localId);
381
317
  if (idx >= 0) next[idx] = merged;
382
318
  } else {
383
- next.push({ ...remote, _localId: nextLocalId() });
319
+ next.push({
320
+ ...remote,
321
+ _localId: nextLocalId()
322
+ });
384
323
  }
385
324
  }
386
- state[stateKey] = next;
387
- state._lastPulled = {
388
- ...state._lastPulled || {},
389
- [stateKey]: newest.toISOString()
325
+ return {
326
+ [stateKey]: next,
327
+ syncState: {
328
+ ...state.syncState || {},
329
+ lastPulled: {
330
+ ...state.syncState.lastPulled || {},
331
+ [stateKey]: newest.toISOString()
332
+ }
333
+ }
390
334
  };
391
- return state;
392
335
  });
393
336
  lastId = batch[batch.length - 1].id;
394
337
  }
@@ -398,204 +341,209 @@ function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}
398
341
  logger.error(`[persistWithSync] First load pull error for stateKey: ${stateKey}`, err);
399
342
  }
400
343
  }
401
- set((state) => {
402
- return {
403
- syncState: { ...state.syncState, firstLoadDone: true },
404
- syncError
405
- };
406
- });
344
+ set((state) => ({
345
+ syncState: {
346
+ ...state.syncState || {},
347
+ firstLoadDone: true,
348
+ error: syncError
349
+ }
350
+ }));
407
351
  }
352
+ storeApi.sync = {
353
+ enable,
354
+ startFirstLoad
355
+ };
408
356
  const userState = stateCreator(setAndSync, get, queueToSync);
409
- const syncState = userState.syncState || {};
410
357
  return {
411
358
  ...userState,
412
359
  syncState: {
413
- status: syncState.status ?? "hydrating",
414
- error: syncState.error,
415
- enabled: syncState.enabled ?? false,
416
- firstLoadDone: syncState.firstLoadDone ?? false,
417
- enableSync,
418
- startFirstLoad
419
- },
420
- _pendingChanges: userState._pendingChanges ?? [],
421
- _lastPulled: userState._lastPulled ?? {}
360
+ // set defaults
361
+ status: "hydrating",
362
+ error: void 0,
363
+ enabled: false,
364
+ firstLoadDone: false,
365
+ pendingChanges: [],
366
+ lastPulled: {}
367
+ }
422
368
  };
423
369
  };
424
370
  return (0, import_middleware.persist)(creator, wrappedPersistOptions);
425
371
  }
372
+ function createStoreWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}) {
373
+ return (0, import_zustand.create)(persistWithSync(stateCreator, persistOptions, syncApi, syncOptions));
374
+ }
426
375
  function orderFor(a) {
427
376
  switch (a) {
428
- case "create" /* Create */:
377
+ case "createOrUpdate" /* CreateOrUpdate */:
429
378
  return 1;
430
- case "update" /* Update */:
431
- return 2;
432
379
  case "remove" /* Remove */:
433
- return 3;
380
+ return 2;
434
381
  }
435
382
  }
436
383
  async function pull(set, get, stateKey, api, logger) {
437
- const lastPulled = get()._lastPulled || {};
384
+ const lastPulled = get().syncState.lastPulled || {};
438
385
  const lastPulledAt = new Date(lastPulled[stateKey] || /* @__PURE__ */ new Date(0));
439
386
  logger.debug(`[persistWithSync] pull:start stateKey=${stateKey} since=${lastPulledAt.toISOString()}`);
440
387
  const serverData = await api.list(lastPulledAt);
441
388
  if (!serverData?.length) return;
442
389
  let newest = lastPulledAt;
443
390
  set((state) => {
444
- const _pendingChanges = state._pendingChanges || [];
445
- const local = state[stateKey] || [];
446
- const localById = new Map(local.filter((l) => l.id).map((l) => [l.id, l]));
447
- const pendingRemovals = new Set(
448
- _pendingChanges.filter(
449
- (p) => p.stateKey === stateKey && p.action === "remove" /* Remove */ && p.changes && typeof p.changes.id !== "undefined"
450
- ).map((p) => p.changes.id)
451
- );
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
+ }
452
402
  for (const remote of serverData) {
453
403
  const remoteUpdated = new Date(remote.updated_at);
454
404
  if (remoteUpdated > newest) newest = remoteUpdated;
455
405
  const localItem = localById.get(remote.id);
456
- if (pendingRemovals.has(remote.id)) {
406
+ if (pendingRemovalIds.has(remote.id)) {
457
407
  logger.debug(`[persistWithSync] pull:skip-pending-remove stateKey=${stateKey} id=${remote.id}`);
458
408
  continue;
459
409
  }
460
410
  if (remote.deleted) {
461
411
  if (localItem) {
462
- state[stateKey] = state[stateKey].filter((i) => i.id !== remote.id);
412
+ nextItems = nextItems.filter((i) => i.id !== remote.id);
463
413
  logger.debug(`[persistWithSync] pull:remove stateKey=${stateKey} id=${remote.id}`);
464
414
  }
465
415
  continue;
466
416
  }
467
- const pending = _pendingChanges.some(
468
- (p) => p.stateKey === stateKey && localItem && p.localId === localItem._localId
469
- );
417
+ const pending = localItem && pendingChanges.some((p) => p.stateKey === stateKey && p.localId === localItem._localId);
470
418
  if (localItem && !pending) {
471
- const merged = { ...localItem, ...remote, _localId: localItem._localId };
472
- state[stateKey] = state[stateKey].map((i) => i._localId === localItem._localId ? merged : i);
419
+ const merged = {
420
+ ...localItem,
421
+ ...remote,
422
+ _localId: localItem._localId
423
+ };
424
+ nextItems = nextItems.map((i) => i._localId === localItem._localId ? merged : i);
473
425
  logger.debug(`[persistWithSync] pull:merge stateKey=${stateKey} id=${remote.id}`);
474
426
  } else if (!localItem) {
475
- state[stateKey] = [...state[stateKey], { ...remote, _localId: nextLocalId() }];
427
+ nextItems = [...nextItems, { ...remote, _localId: nextLocalId() }];
476
428
  logger.debug(`[persistWithSync] pull:add stateKey=${stateKey} id=${remote.id}`);
477
429
  }
478
430
  }
479
- state._lastPulled = {
480
- ...state._lastPulled,
481
- [stateKey]: newest.toISOString()
431
+ return {
432
+ [stateKey]: nextItems,
433
+ syncState: {
434
+ ...state.syncState || {},
435
+ lastPulled: {
436
+ ...state.syncState.lastPulled || {},
437
+ [stateKey]: newest.toISOString()
438
+ }
439
+ }
482
440
  };
483
- return state;
484
441
  });
485
442
  }
486
443
  async function pushOne(set, get, change, api, logger, queueToSync, missingStrategy, onMissingRemoteRecordDuringUpdate, onAfterRemoteAdd) {
487
- logger.debug(
488
- `[persistWithSync] push:attempt action=${change.action} stateKey=${change.stateKey} localId=${change.localId}`
489
- );
490
- const { stateKey, localId, action, changes } = change;
491
- const state = get();
492
- const items = state[stateKey] || [];
493
- const item = items.find((i) => i._localId === localId);
444
+ logger.debug(`[persistWithSync] push:attempt action=${change.action} stateKey=${change.stateKey} localId=${change.localId}`);
445
+ const { action, stateKey, localId, id, version } = change;
494
446
  switch (action) {
495
- case "create" /* Create */: {
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);
496
456
  if (!item) {
457
+ logger.warn(`[persistWithSync] push:${action}:no-local-item`, {
458
+ stateKey,
459
+ localId
460
+ });
497
461
  removeFromPendingChanges(set, localId, stateKey);
498
462
  return;
499
463
  }
464
+ let omittedItem = omitSyncFields(item);
500
465
  if (item.id) {
501
- if (changes && Object.keys(changes).length) {
502
- await api.update(item.id, omitSyncFields({ ...item, ...changes }));
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);
503
501
  }
504
- removeFromPendingChanges(set, localId, stateKey);
505
502
  return;
506
503
  }
507
- const payload = changes ? { ...item, ...changes } : item;
508
- const result = await api.add(omitSyncFields(payload));
504
+ const result = await api.add(omittedItem);
509
505
  if (result) {
510
- logger.debug("[persistWithSync] push:create:success", { stateKey, localId, id: result.id });
511
- set((s) => {
512
- s[stateKey] = (s[stateKey] || []).map(
513
- (i) => i._localId === localId ? { ...i, ...result } : i
514
- );
515
- return s;
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
516
520
  });
517
- onAfterRemoteAdd?.(set, get, queueToSync, stateKey, { ...item, ...result });
518
521
  } else {
519
- logger.warn("[persistWithSync] push:create:no-result", { stateKey, localId });
520
- }
521
- removeFromPendingChanges(set, localId, stateKey);
522
- break;
523
- }
524
- case "update" /* Update */: {
525
- if (!item) {
526
- removeFromPendingChanges(set, localId, stateKey);
527
- return;
528
- }
529
- if (!item.id) {
530
- set((s) => {
531
- const q = s._pendingChanges || [];
532
- const e = q.find((p) => p.localId === localId && p.stateKey === stateKey);
533
- if (e) e.action = "create" /* Create */;
534
- return s;
522
+ logger.warn("[persistWithSync] push:create:no-result", {
523
+ stateKey,
524
+ localId
535
525
  });
536
- return;
537
- }
538
- const changed = await api.update(item.id, omitSyncFields({ ...changes }));
539
- if (!changed) {
540
- logger.warn("[persistWithSync] push:update:missingRemote", { stateKey, localId, id: item.id });
541
- const oldRecord = { ...item };
542
- let newLocalId;
543
- switch (missingStrategy) {
544
- case "deleteLocalRecord":
545
- set((s) => {
546
- s[stateKey] = (s[stateKey] || []).filter((i) => i._localId !== localId);
547
- return s;
548
- });
549
- removeFromPendingChanges(set, localId, stateKey);
550
- break;
551
- case "insertNewRemoteRecord": {
552
- const freshLocalId = nextLocalId();
553
- newLocalId = freshLocalId;
554
- set((s) => {
555
- s[stateKey] = (s[stateKey] || []).filter((i) => i._localId !== localId);
556
- s[stateKey].push({
557
- ...omitSyncFields(oldRecord),
558
- _localId: freshLocalId,
559
- updated_at: (/* @__PURE__ */ new Date()).toISOString()
560
- });
561
- const q = s._pendingChanges || [];
562
- const e = q.find((p) => p.localId === localId && p.stateKey === stateKey);
563
- if (e) {
564
- e.localId = freshLocalId;
565
- e.action = "create" /* Create */;
566
- } else {
567
- q.push({ stateKey, localId: freshLocalId, action: "create" /* Create */, changes });
568
- }
569
- s._pendingChanges = q;
570
- return s;
571
- });
572
- break;
573
- }
526
+ if (samePendingVersion(get, stateKey, localId, version)) {
527
+ removeFromPendingChanges(set, localId, stateKey);
574
528
  }
575
- onMissingRemoteRecordDuringUpdate?.(missingStrategy, oldRecord, newLocalId);
576
- } else {
577
- logger.debug("[persistWithSync] push:update:success", { stateKey, localId, id: item.id });
578
- removeFromPendingChanges(set, localId, stateKey);
579
- }
580
- break;
581
- }
582
- case "remove" /* Remove */: {
583
- const id = changes?.id;
584
- if (id) {
585
- await api.remove(id);
586
- logger.debug("[persistWithSync] push:remove:success", { stateKey, localId, id });
587
529
  }
588
- removeFromPendingChanges(set, localId, stateKey);
589
530
  break;
590
- }
591
531
  }
592
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
+ }
593
538
  function removeFromPendingChanges(set, localId, stateKey) {
594
539
  set((s) => {
595
- s._pendingChanges = (s._pendingChanges || []).filter(
596
- (p) => !(p.localId === localId && p.stateKey === stateKey)
597
- );
598
- return 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
+ };
599
547
  });
600
548
  }
601
549
  function omitSyncFields(item) {
@@ -607,7 +555,13 @@ function nextLocalId() {
607
555
  return crypto.randomUUID();
608
556
  }
609
557
  function newLogger(logger, min) {
610
- const order = { debug: 10, info: 20, warn: 30, error: 40, none: 100 };
558
+ const order = {
559
+ debug: 10,
560
+ info: 20,
561
+ warn: 30,
562
+ error: 40,
563
+ none: 100
564
+ };
611
565
  const threshold = order[min];
612
566
  const enabled = (lvl) => order[lvl] >= threshold;
613
567
  return {
@@ -628,6 +582,7 @@ function findApi(stateKey, syncApi) {
628
582
  0 && (module.exports = {
629
583
  SyncAction,
630
584
  createIndexedDBStorage,
585
+ createStoreWithSync,
631
586
  nextLocalId,
632
587
  persistWithSync
633
588
  });