@anfenn/zync 0.1.3 → 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
@@ -21,29 +21,95 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  SyncAction: () => SyncAction,
24
+ createIndexedDBStorage: () => createIndexedDBStorage,
25
+ createStoreWithSync: () => createStoreWithSync,
24
26
  nextLocalId: () => nextLocalId,
25
27
  persistWithSync: () => persistWithSync
26
28
  });
27
29
  module.exports = __toCommonJS(index_exports);
28
30
  var import_zustand = require("zustand");
29
31
  var import_middleware = require("zustand/middleware");
32
+
33
+ // src/indexedDBStorage.ts
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);
42
+ }
43
+ }
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);
56
+ }
57
+ });
58
+ await dbPromise;
59
+ }
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);
70
+ }
71
+ throw err;
72
+ }
73
+ }
74
+ return {
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
+ });
82
+ },
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
+ });
94
+ }
95
+ };
96
+ }
97
+
98
+ // src/index.ts
30
99
  var SyncAction = /* @__PURE__ */ ((SyncAction2) => {
31
- SyncAction2["Create"] = "create";
32
- SyncAction2["Update"] = "update";
100
+ SyncAction2["CreateOrUpdate"] = "createOrUpdate";
33
101
  SyncAction2["Remove"] = "remove";
34
102
  return SyncAction2;
35
103
  })(SyncAction || {});
36
104
  var DEFAULT_SYNC_INTERVAL_MILLIS = 5e3;
37
105
  var DEFAULT_LOGGER = console;
38
106
  var DEFAULT_MIN_LOG_LEVEL = "debug";
39
- var DEFAULT_MISSING_REMOTE_RECORD_ON_UPDATE_STRATEGY = "insertNewRemoteRecord";
107
+ var DEFAULT_MISSING_REMOTE_RECORD_ON_UPDATE_STRATEGY = "ignore";
40
108
  var SYNC_FIELDS = ["id", "_localId", "updated_at", "deleted"];
41
109
  function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}) {
42
110
  const syncInterval = syncOptions.syncInterval ?? DEFAULT_SYNC_INTERVAL_MILLIS;
43
111
  const missingStrategy = syncOptions.missingRemoteRecordDuringUpdateStrategy ?? DEFAULT_MISSING_REMOTE_RECORD_ON_UPDATE_STRATEGY;
44
112
  const logger = newLogger(syncOptions.logger ?? DEFAULT_LOGGER, syncOptions.minLogLevel ?? DEFAULT_MIN_LOG_LEVEL);
45
- let startSync;
46
- let syncIntervalId;
47
113
  const baseOnRehydrate = persistOptions?.onRehydrateStorage;
48
114
  const basePartialize = persistOptions?.partialize;
49
115
  const wrappedPersistOptions = {
@@ -55,47 +121,52 @@ function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}
55
121
  logger.error("[persistWithSync] Rehydration failed", error);
56
122
  } else {
57
123
  baseOnRehydrate?.(state, error);
124
+ state.syncState.status = "idle";
58
125
  logger.debug("[persistWithSync] Rehydration complete");
59
- startSync?.();
60
126
  }
61
127
  };
62
128
  },
63
129
  partialize: (s) => {
64
130
  const base = basePartialize ? basePartialize(s) : s;
65
- const { syncState: _sync, ...rest } = base || {};
131
+ const { syncState, ...rest } = base || {};
66
132
  return {
67
133
  ...rest,
68
134
  syncState: {
69
- firstLoadDone: _sync?.firstLoadDone ?? false
70
- }
71
- };
72
- },
73
- merge: (persisted, current) => {
74
- const p = persisted || {};
75
- const c = current || {};
76
- return {
77
- ...c,
78
- ...p,
79
- syncState: {
80
- ...c.syncState,
81
- firstLoadDone: p.syncState?.firstLoadDone ?? c.syncState?.firstLoadDone ?? false,
82
- status: "idle"
135
+ firstLoadDone: syncState.firstLoadDone,
136
+ pendingChanges: syncState.pendingChanges,
137
+ lastPulled: syncState.lastPulled
83
138
  }
84
139
  };
85
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
+ // },
86
158
  };
87
- const creator = (set, get) => {
159
+ const creator = (set, get, storeApi) => {
160
+ let syncIntervalId;
88
161
  async function syncOnce() {
89
162
  const state = get();
90
163
  if (!state.syncState.enabled || state.syncState.status !== "idle") return;
91
- set((state2) => {
92
- return {
93
- syncState: {
94
- ...state2.syncState,
95
- status: "syncing"
96
- }
97
- };
98
- });
164
+ set((state2) => ({
165
+ syncState: {
166
+ ...state2.syncState || {},
167
+ status: "syncing"
168
+ }
169
+ }));
99
170
  let syncError;
100
171
  for (const stateKey of Object.keys(syncApi)) {
101
172
  try {
@@ -106,7 +177,7 @@ function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}
106
177
  logger.error(`[persistWithSync] Pull error for stateKey: ${stateKey}`, err);
107
178
  }
108
179
  }
109
- const snapshot = [...get()._pendingChanges || []];
180
+ const snapshot = [...get().syncState.pendingChanges || []];
110
181
  snapshot.sort((a, b) => orderFor(a.action) - orderFor(b.action));
111
182
  for (const change of snapshot) {
112
183
  try {
@@ -127,83 +198,95 @@ function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}
127
198
  logger.error(`[persistWithSync] Push error for change: ${change}`, err);
128
199
  }
129
200
  }
130
- set((state2) => {
131
- return {
132
- syncState: {
133
- ...state2.syncState,
134
- status: "idle",
135
- error: syncError
136
- }
137
- };
138
- });
201
+ set((state2) => ({
202
+ syncState: {
203
+ ...state2.syncState || {},
204
+ status: "idle",
205
+ error: syncError
206
+ }
207
+ }));
139
208
  }
140
- startSync = async () => {
141
- clearInterval(syncIntervalId);
142
- syncIntervalId = void 0;
143
- await syncOnce();
144
- syncIntervalId = setInterval(syncOnce, syncInterval);
145
- };
146
- function queueToSync(action, localId, stateKey, changes) {
209
+ function queueToSync(action, stateKey, ...localIds) {
147
210
  set((state) => {
148
- const queue = state._pendingChanges || [];
149
- const idx = queue.findIndex((p) => p.localId === localId && p.stateKey === stateKey);
150
- if (idx >= 0) {
151
- const existing = queue[idx];
152
- switch (existing?.action) {
153
- case "create" /* Create */:
154
- if (action === "update" /* Update */) {
155
- existing.changes = { ...existing.changes, ...changes };
156
- } else if (action === "remove" /* Remove */) {
157
- queue.splice(idx, 1);
158
- }
159
- break;
160
- case "update" /* Update */:
161
- if (action === "update" /* Update */) {
162
- existing.changes = { ...existing.changes, ...changes };
163
- } else if (action === "remove" /* Remove */) {
164
- existing.action = "remove" /* Remove */;
165
- delete existing.changes;
166
- }
167
- break;
168
- case "remove" /* Remove */:
169
- 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;
170
220
  }
171
- } else {
172
- if (action === "remove" /* Remove */) {
173
- const item = state[stateKey].find((i) => i._localId === localId);
174
- 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 });
175
237
  }
176
- queue.push({ stateKey, localId, action, changes });
177
238
  }
178
- state._pendingChanges = queue;
179
- return state;
180
- });
181
- syncOnce();
182
- }
183
- function setAndSync(partial) {
184
- set(partial);
185
- syncOnce();
186
- }
187
- function enableSync(enabled) {
188
- set((state) => {
189
239
  return {
190
240
  syncState: {
191
- ...state.syncState,
192
- enabled
241
+ ...state.syncState || {},
242
+ pendingChanges: queue
193
243
  }
194
244
  };
195
245
  });
196
246
  syncOnce();
197
247
  }
198
- 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) {
199
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() {
200
282
  if (document.visibilityState === "visible") {
201
- logger.debug("[persistWithSync] Sync starting now app is in foreground");
202
- await startSync?.();
283
+ logger.debug("[persistWithSync] Sync started now app is in foreground");
284
+ enableSyncTimer(true);
203
285
  } else {
204
286
  logger.debug("[persistWithSync] Sync paused now app is in background");
287
+ enableSyncTimer(false);
205
288
  }
206
- });
289
+ }
207
290
  async function startFirstLoad() {
208
291
  let syncError;
209
292
  for (const stateKey of Object.keys(syncApi)) {
@@ -217,7 +300,7 @@ function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}
217
300
  set((state) => {
218
301
  const local = state[stateKey] || [];
219
302
  const localById = new Map(local.filter((l) => l.id).map((l) => [l.id, l]));
220
- let newest = new Date(state._lastPulled?.[stateKey] || 0);
303
+ let newest = new Date(state.syncState.lastPulled[stateKey] || 0);
221
304
  const next = [...local];
222
305
  for (const remote of batch) {
223
306
  const remoteUpdated = new Date(remote.updated_at || 0);
@@ -225,19 +308,30 @@ function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}
225
308
  if (remote.deleted) continue;
226
309
  const localItem = remote.id ? localById.get(remote.id) : void 0;
227
310
  if (localItem) {
228
- const merged = { ...localItem, ...remote, _localId: localItem._localId };
311
+ const merged = {
312
+ ...localItem,
313
+ ...remote,
314
+ _localId: localItem._localId
315
+ };
229
316
  const idx = next.findIndex((i) => i._localId === localItem._localId);
230
317
  if (idx >= 0) next[idx] = merged;
231
318
  } else {
232
- next.push({ ...remote, _localId: nextLocalId() });
319
+ next.push({
320
+ ...remote,
321
+ _localId: nextLocalId()
322
+ });
233
323
  }
234
324
  }
235
- state[stateKey] = next;
236
- state._lastPulled = {
237
- ...state._lastPulled || {},
238
- [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
+ }
239
334
  };
240
- return state;
241
335
  });
242
336
  lastId = batch[batch.length - 1].id;
243
337
  }
@@ -247,204 +341,209 @@ function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}
247
341
  logger.error(`[persistWithSync] First load pull error for stateKey: ${stateKey}`, err);
248
342
  }
249
343
  }
250
- set((state) => {
251
- return {
252
- syncState: { ...state.syncState, firstLoadDone: true },
253
- syncError
254
- };
255
- });
344
+ set((state) => ({
345
+ syncState: {
346
+ ...state.syncState || {},
347
+ firstLoadDone: true,
348
+ error: syncError
349
+ }
350
+ }));
256
351
  }
352
+ storeApi.sync = {
353
+ enable,
354
+ startFirstLoad
355
+ };
257
356
  const userState = stateCreator(setAndSync, get, queueToSync);
258
- const syncState = userState.syncState || {};
259
357
  return {
260
358
  ...userState,
261
359
  syncState: {
262
- status: syncState.status ?? "hydrating",
263
- error: syncState.error,
264
- enabled: syncState.enabled ?? false,
265
- firstLoadDone: syncState.firstLoadDone ?? false,
266
- enableSync,
267
- startFirstLoad
268
- },
269
- _pendingChanges: userState._pendingChanges ?? [],
270
- _lastPulled: userState._lastPulled ?? {}
360
+ // set defaults
361
+ status: "hydrating",
362
+ error: void 0,
363
+ enabled: false,
364
+ firstLoadDone: false,
365
+ pendingChanges: [],
366
+ lastPulled: {}
367
+ }
271
368
  };
272
369
  };
273
370
  return (0, import_middleware.persist)(creator, wrappedPersistOptions);
274
371
  }
372
+ function createStoreWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}) {
373
+ return (0, import_zustand.create)(persistWithSync(stateCreator, persistOptions, syncApi, syncOptions));
374
+ }
275
375
  function orderFor(a) {
276
376
  switch (a) {
277
- case "create" /* Create */:
377
+ case "createOrUpdate" /* CreateOrUpdate */:
278
378
  return 1;
279
- case "update" /* Update */:
280
- return 2;
281
379
  case "remove" /* Remove */:
282
- return 3;
380
+ return 2;
283
381
  }
284
382
  }
285
383
  async function pull(set, get, stateKey, api, logger) {
286
- const lastPulled = get()._lastPulled || {};
384
+ const lastPulled = get().syncState.lastPulled || {};
287
385
  const lastPulledAt = new Date(lastPulled[stateKey] || /* @__PURE__ */ new Date(0));
288
386
  logger.debug(`[persistWithSync] pull:start stateKey=${stateKey} since=${lastPulledAt.toISOString()}`);
289
387
  const serverData = await api.list(lastPulledAt);
290
388
  if (!serverData?.length) return;
291
389
  let newest = lastPulledAt;
292
390
  set((state) => {
293
- const _pendingChanges = state._pendingChanges || [];
294
- const local = state[stateKey] || [];
295
- const localById = new Map(local.filter((l) => l.id).map((l) => [l.id, l]));
296
- const pendingRemovals = new Set(
297
- _pendingChanges.filter(
298
- (p) => p.stateKey === stateKey && p.action === "remove" /* Remove */ && p.changes && typeof p.changes.id !== "undefined"
299
- ).map((p) => p.changes.id)
300
- );
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
+ }
301
402
  for (const remote of serverData) {
302
403
  const remoteUpdated = new Date(remote.updated_at);
303
404
  if (remoteUpdated > newest) newest = remoteUpdated;
304
405
  const localItem = localById.get(remote.id);
305
- if (pendingRemovals.has(remote.id)) {
406
+ if (pendingRemovalIds.has(remote.id)) {
306
407
  logger.debug(`[persistWithSync] pull:skip-pending-remove stateKey=${stateKey} id=${remote.id}`);
307
408
  continue;
308
409
  }
309
410
  if (remote.deleted) {
310
411
  if (localItem) {
311
- state[stateKey] = state[stateKey].filter((i) => i.id !== remote.id);
412
+ nextItems = nextItems.filter((i) => i.id !== remote.id);
312
413
  logger.debug(`[persistWithSync] pull:remove stateKey=${stateKey} id=${remote.id}`);
313
414
  }
314
415
  continue;
315
416
  }
316
- const pending = _pendingChanges.some(
317
- (p) => p.stateKey === stateKey && localItem && p.localId === localItem._localId
318
- );
417
+ const pending = localItem && pendingChanges.some((p) => p.stateKey === stateKey && p.localId === localItem._localId);
319
418
  if (localItem && !pending) {
320
- const merged = { ...localItem, ...remote, _localId: localItem._localId };
321
- 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);
322
425
  logger.debug(`[persistWithSync] pull:merge stateKey=${stateKey} id=${remote.id}`);
323
426
  } else if (!localItem) {
324
- state[stateKey] = [...state[stateKey], { ...remote, _localId: nextLocalId() }];
427
+ nextItems = [...nextItems, { ...remote, _localId: nextLocalId() }];
325
428
  logger.debug(`[persistWithSync] pull:add stateKey=${stateKey} id=${remote.id}`);
326
429
  }
327
430
  }
328
- state._lastPulled = {
329
- ...state._lastPulled,
330
- [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
+ }
331
440
  };
332
- return state;
333
441
  });
334
442
  }
335
443
  async function pushOne(set, get, change, api, logger, queueToSync, missingStrategy, onMissingRemoteRecordDuringUpdate, onAfterRemoteAdd) {
336
- logger.debug(
337
- `[persistWithSync] push:attempt action=${change.action} stateKey=${change.stateKey} localId=${change.localId}`
338
- );
339
- const { stateKey, localId, action, changes } = change;
340
- const state = get();
341
- const items = state[stateKey] || [];
342
- 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;
343
446
  switch (action) {
344
- 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);
345
456
  if (!item) {
457
+ logger.warn(`[persistWithSync] push:${action}:no-local-item`, {
458
+ stateKey,
459
+ localId
460
+ });
346
461
  removeFromPendingChanges(set, localId, stateKey);
347
462
  return;
348
463
  }
464
+ let omittedItem = omitSyncFields(item);
349
465
  if (item.id) {
350
- if (changes && Object.keys(changes).length) {
351
- 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);
352
501
  }
353
- removeFromPendingChanges(set, localId, stateKey);
354
502
  return;
355
503
  }
356
- const payload = changes ? { ...item, ...changes } : item;
357
- const result = await api.add(omitSyncFields(payload));
504
+ const result = await api.add(omittedItem);
358
505
  if (result) {
359
- logger.debug("[persistWithSync] push:create:success", { stateKey, localId, id: result.id });
360
- set((s) => {
361
- s[stateKey] = (s[stateKey] || []).map(
362
- (i) => i._localId === localId ? { ...i, ...result } : i
363
- );
364
- 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
365
520
  });
366
- onAfterRemoteAdd?.(set, get, queueToSync, stateKey, { ...item, ...result });
367
521
  } else {
368
- logger.warn("[persistWithSync] push:create:no-result", { stateKey, localId });
369
- }
370
- removeFromPendingChanges(set, localId, stateKey);
371
- break;
372
- }
373
- case "update" /* Update */: {
374
- if (!item) {
375
- removeFromPendingChanges(set, localId, stateKey);
376
- return;
377
- }
378
- if (!item.id) {
379
- set((s) => {
380
- const q = s._pendingChanges || [];
381
- const e = q.find((p) => p.localId === localId && p.stateKey === stateKey);
382
- if (e) e.action = "create" /* Create */;
383
- return s;
522
+ logger.warn("[persistWithSync] push:create:no-result", {
523
+ stateKey,
524
+ localId
384
525
  });
385
- return;
386
- }
387
- const changed = await api.update(item.id, omitSyncFields({ ...changes }));
388
- if (!changed) {
389
- logger.warn("[persistWithSync] push:update:missingRemote", { stateKey, localId, id: item.id });
390
- const oldRecord = { ...item };
391
- let newLocalId;
392
- switch (missingStrategy) {
393
- case "deleteLocalRecord":
394
- set((s) => {
395
- s[stateKey] = (s[stateKey] || []).filter((i) => i._localId !== localId);
396
- return s;
397
- });
398
- removeFromPendingChanges(set, localId, stateKey);
399
- break;
400
- case "insertNewRemoteRecord": {
401
- const freshLocalId = nextLocalId();
402
- newLocalId = freshLocalId;
403
- set((s) => {
404
- s[stateKey] = (s[stateKey] || []).filter((i) => i._localId !== localId);
405
- s[stateKey].push({
406
- ...omitSyncFields(oldRecord),
407
- _localId: freshLocalId,
408
- updated_at: (/* @__PURE__ */ new Date()).toISOString()
409
- });
410
- const q = s._pendingChanges || [];
411
- const e = q.find((p) => p.localId === localId && p.stateKey === stateKey);
412
- if (e) {
413
- e.localId = freshLocalId;
414
- e.action = "create" /* Create */;
415
- } else {
416
- q.push({ stateKey, localId: freshLocalId, action: "create" /* Create */, changes });
417
- }
418
- s._pendingChanges = q;
419
- return s;
420
- });
421
- break;
422
- }
526
+ if (samePendingVersion(get, stateKey, localId, version)) {
527
+ removeFromPendingChanges(set, localId, stateKey);
423
528
  }
424
- onMissingRemoteRecordDuringUpdate?.(missingStrategy, oldRecord, newLocalId);
425
- } else {
426
- logger.debug("[persistWithSync] push:update:success", { stateKey, localId, id: item.id });
427
- removeFromPendingChanges(set, localId, stateKey);
428
529
  }
429
530
  break;
430
- }
431
- case "remove" /* Remove */: {
432
- const id = changes?.id;
433
- if (id) {
434
- await api.remove(id);
435
- logger.debug("[persistWithSync] push:remove:success", { stateKey, localId, id });
436
- }
437
- removeFromPendingChanges(set, localId, stateKey);
438
- break;
439
- }
440
531
  }
441
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
+ }
442
538
  function removeFromPendingChanges(set, localId, stateKey) {
443
539
  set((s) => {
444
- s._pendingChanges = (s._pendingChanges || []).filter(
445
- (p) => !(p.localId === localId && p.stateKey === stateKey)
446
- );
447
- 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
+ };
448
547
  });
449
548
  }
450
549
  function omitSyncFields(item) {
@@ -456,7 +555,13 @@ function nextLocalId() {
456
555
  return crypto.randomUUID();
457
556
  }
458
557
  function newLogger(logger, min) {
459
- 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
+ };
460
565
  const threshold = order[min];
461
566
  const enabled = (lvl) => order[lvl] >= threshold;
462
567
  return {
@@ -476,6 +581,8 @@ function findApi(stateKey, syncApi) {
476
581
  // Annotate the CommonJS export names for ESM import in node:
477
582
  0 && (module.exports = {
478
583
  SyncAction,
584
+ createIndexedDBStorage,
585
+ createStoreWithSync,
479
586
  nextLocalId,
480
587
  persistWithSync
481
588
  });