@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 +340 -233
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +41 -9
- package/dist/index.d.ts +41 -9
- package/dist/index.js +339 -234
- package/dist/index.js.map +1 -1
- package/package.json +9 -4
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["
|
|
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 = "
|
|
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
|
|
131
|
+
const { syncState, ...rest } = base || {};
|
|
66
132
|
return {
|
|
67
133
|
...rest,
|
|
68
134
|
syncState: {
|
|
69
|
-
firstLoadDone:
|
|
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
|
-
|
|
93
|
-
syncState
|
|
94
|
-
|
|
95
|
-
|
|
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().
|
|
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
|
-
|
|
132
|
-
syncState
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
};
|
|
138
|
-
});
|
|
201
|
+
set((state2) => ({
|
|
202
|
+
syncState: {
|
|
203
|
+
...state2.syncState || {},
|
|
204
|
+
status: "idle",
|
|
205
|
+
error: syncError
|
|
206
|
+
}
|
|
207
|
+
}));
|
|
139
208
|
}
|
|
140
|
-
|
|
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.
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
241
|
+
...state.syncState || {},
|
|
242
|
+
pendingChanges: queue
|
|
193
243
|
}
|
|
194
244
|
};
|
|
195
245
|
});
|
|
196
246
|
syncOnce();
|
|
197
247
|
}
|
|
198
|
-
|
|
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
|
|
202
|
-
|
|
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.
|
|
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 = {
|
|
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({
|
|
319
|
+
next.push({
|
|
320
|
+
...remote,
|
|
321
|
+
_localId: nextLocalId()
|
|
322
|
+
});
|
|
233
323
|
}
|
|
234
324
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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 "
|
|
377
|
+
case "createOrUpdate" /* CreateOrUpdate */:
|
|
278
378
|
return 1;
|
|
279
|
-
case "update" /* Update */:
|
|
280
|
-
return 2;
|
|
281
379
|
case "remove" /* Remove */:
|
|
282
|
-
return
|
|
380
|
+
return 2;
|
|
283
381
|
}
|
|
284
382
|
}
|
|
285
383
|
async function pull(set, get, stateKey, api, logger) {
|
|
286
|
-
const lastPulled = get().
|
|
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
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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 (
|
|
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
|
-
|
|
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 =
|
|
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 = {
|
|
321
|
-
|
|
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
|
-
|
|
427
|
+
nextItems = [...nextItems, { ...remote, _localId: nextLocalId() }];
|
|
325
428
|
logger.debug(`[persistWithSync] pull:add stateKey=${stateKey} id=${remote.id}`);
|
|
326
429
|
}
|
|
327
430
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
-
|
|
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 "
|
|
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
|
-
|
|
351
|
-
|
|
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
|
|
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", {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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", {
|
|
369
|
-
|
|
370
|
-
|
|
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
|
-
|
|
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
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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 = {
|
|
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
|
});
|