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