@anfenn/zync 0.1.9 → 0.1.10

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:${action}: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:missingRemote", {
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 started");
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 for 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 for change: ${change}`, err);
162
394
  }
163
395
  }
164
396
  set((state2) => ({
@@ -172,40 +404,97 @@ 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 for 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 for stateKey: ${stateKey}`);
456
+ } catch (err) {
457
+ syncError = syncError ?? err;
458
+ logger.error(`[Zync] First load pull error for 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", {
475
+ logger.error("[Zync] queueToSync:no-local-item", {
189
476
  stateKey,
190
477
  localId
191
478
  });
192
479
  continue;
193
480
  }
194
- const queueItem = queue.find((p) => p.localId === localId && p.stateKey === stateKey);
481
+ const queueItem = pendingChanges.find((p) => p.localId === localId && p.stateKey === stateKey);
195
482
  if (queueItem) {
196
483
  queueItem.version += 1;
197
484
  if (queueItem.action === "createOrUpdate" /* CreateOrUpdate */ && action === "remove" /* Remove */ && item.id) {
198
485
  queueItem.action = "remove" /* Remove */;
199
486
  queueItem.id = item.id;
200
487
  }
488
+ logger.debug(`[Zync] queueToSync:adjusted ${queueItem.version} ${action} ${item.id} ${stateKey} ${localId}`);
201
489
  } else {
202
- queue.push({ action, stateKey, localId, id: item.id, version: 1 });
490
+ pendingChanges.push({ action, stateKey, localId, id: item.id, version: 1 });
491
+ logger.debug(`[Zync] queueToSync:added ${action} ${item.id} ${stateKey} ${localId}`);
203
492
  }
204
493
  }
205
494
  return {
206
495
  syncState: {
207
496
  ...state.syncState || {},
208
- pendingChanges: queue
497
+ pendingChanges
209
498
  }
210
499
  };
211
500
  });
@@ -246,75 +535,13 @@ function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}
246
535
  }
247
536
  function onVisibilityChange() {
248
537
  if (document.visibilityState === "visible") {
249
- logger.debug("[persistWithSync] Sync started now app is in foreground");
538
+ logger.debug("[Zync] Sync started now app is in foreground");
250
539
  enableSyncTimer(true);
251
540
  } else {
252
- logger.debug("[persistWithSync] Sync paused now app is in background");
541
+ logger.debug("[Zync] Sync paused now app is in background");
253
542
  enableSyncTimer(false);
254
543
  }
255
544
  }
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
545
  storeApi.sync = {
319
546
  enable,
320
547
  startFirstLoad
@@ -335,219 +562,10 @@ function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}
335
562
  };
336
563
  return persist(creator, wrappedPersistOptions);
337
564
  }
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
565
  export {
548
566
  SyncAction,
549
567
  createIndexedDBStorage,
550
- createStoreWithSync,
568
+ createWithSync,
551
569
  nextLocalId,
552
570
  persistWithSync
553
571
  };