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