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