@anfenn/zync 0.1.4 → 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 +322 -367
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +39 -44
- package/dist/index.d.ts +39 -44
- package/dist/index.js +322 -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,95 @@ function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}
|
|
|
278
198
|
logger.error(`[persistWithSync] Push error for change: ${change}`, err);
|
|
279
199
|
}
|
|
280
200
|
}
|
|
281
|
-
set((state2) => {
|
|
282
|
-
|
|
283
|
-
syncState
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
};
|
|
289
|
-
});
|
|
201
|
+
set((state2) => ({
|
|
202
|
+
syncState: {
|
|
203
|
+
...state2.syncState || {},
|
|
204
|
+
status: "idle",
|
|
205
|
+
error: syncError
|
|
206
|
+
}
|
|
207
|
+
}));
|
|
290
208
|
}
|
|
291
|
-
|
|
292
|
-
clearInterval(syncIntervalId);
|
|
293
|
-
syncIntervalId = void 0;
|
|
294
|
-
await syncOnce();
|
|
295
|
-
syncIntervalId = setInterval(syncOnce, syncInterval);
|
|
296
|
-
};
|
|
297
|
-
function queueToSync(action, localId, stateKey, changes) {
|
|
209
|
+
function queueToSync(action, stateKey, ...localIds) {
|
|
298
210
|
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;
|
|
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;
|
|
321
220
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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 });
|
|
326
237
|
}
|
|
327
|
-
queue.push({ stateKey, localId, action, changes });
|
|
328
238
|
}
|
|
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
239
|
return {
|
|
341
240
|
syncState: {
|
|
342
|
-
...state.syncState,
|
|
343
|
-
|
|
241
|
+
...state.syncState || {},
|
|
242
|
+
pendingChanges: queue
|
|
344
243
|
}
|
|
345
244
|
};
|
|
346
245
|
});
|
|
347
246
|
syncOnce();
|
|
348
247
|
}
|
|
349
|
-
|
|
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) {
|
|
350
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() {
|
|
351
282
|
if (document.visibilityState === "visible") {
|
|
352
|
-
logger.debug("[persistWithSync] Sync
|
|
353
|
-
|
|
283
|
+
logger.debug("[persistWithSync] Sync started now app is in foreground");
|
|
284
|
+
enableSyncTimer(true);
|
|
354
285
|
} else {
|
|
355
286
|
logger.debug("[persistWithSync] Sync paused now app is in background");
|
|
287
|
+
enableSyncTimer(false);
|
|
356
288
|
}
|
|
357
|
-
}
|
|
289
|
+
}
|
|
358
290
|
async function startFirstLoad() {
|
|
359
291
|
let syncError;
|
|
360
292
|
for (const stateKey of Object.keys(syncApi)) {
|
|
@@ -368,7 +300,7 @@ function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}
|
|
|
368
300
|
set((state) => {
|
|
369
301
|
const local = state[stateKey] || [];
|
|
370
302
|
const localById = new Map(local.filter((l) => l.id).map((l) => [l.id, l]));
|
|
371
|
-
let newest = new Date(state.
|
|
303
|
+
let newest = new Date(state.syncState.lastPulled[stateKey] || 0);
|
|
372
304
|
const next = [...local];
|
|
373
305
|
for (const remote of batch) {
|
|
374
306
|
const remoteUpdated = new Date(remote.updated_at || 0);
|
|
@@ -376,19 +308,30 @@ function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}
|
|
|
376
308
|
if (remote.deleted) continue;
|
|
377
309
|
const localItem = remote.id ? localById.get(remote.id) : void 0;
|
|
378
310
|
if (localItem) {
|
|
379
|
-
const merged = {
|
|
311
|
+
const merged = {
|
|
312
|
+
...localItem,
|
|
313
|
+
...remote,
|
|
314
|
+
_localId: localItem._localId
|
|
315
|
+
};
|
|
380
316
|
const idx = next.findIndex((i) => i._localId === localItem._localId);
|
|
381
317
|
if (idx >= 0) next[idx] = merged;
|
|
382
318
|
} else {
|
|
383
|
-
next.push({
|
|
319
|
+
next.push({
|
|
320
|
+
...remote,
|
|
321
|
+
_localId: nextLocalId()
|
|
322
|
+
});
|
|
384
323
|
}
|
|
385
324
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
325
|
+
return {
|
|
326
|
+
[stateKey]: next,
|
|
327
|
+
syncState: {
|
|
328
|
+
...state.syncState || {},
|
|
329
|
+
lastPulled: {
|
|
330
|
+
...state.syncState.lastPulled || {},
|
|
331
|
+
[stateKey]: newest.toISOString()
|
|
332
|
+
}
|
|
333
|
+
}
|
|
390
334
|
};
|
|
391
|
-
return state;
|
|
392
335
|
});
|
|
393
336
|
lastId = batch[batch.length - 1].id;
|
|
394
337
|
}
|
|
@@ -398,204 +341,209 @@ function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}
|
|
|
398
341
|
logger.error(`[persistWithSync] First load pull error for stateKey: ${stateKey}`, err);
|
|
399
342
|
}
|
|
400
343
|
}
|
|
401
|
-
set((state) => {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
344
|
+
set((state) => ({
|
|
345
|
+
syncState: {
|
|
346
|
+
...state.syncState || {},
|
|
347
|
+
firstLoadDone: true,
|
|
348
|
+
error: syncError
|
|
349
|
+
}
|
|
350
|
+
}));
|
|
407
351
|
}
|
|
352
|
+
storeApi.sync = {
|
|
353
|
+
enable,
|
|
354
|
+
startFirstLoad
|
|
355
|
+
};
|
|
408
356
|
const userState = stateCreator(setAndSync, get, queueToSync);
|
|
409
|
-
const syncState = userState.syncState || {};
|
|
410
357
|
return {
|
|
411
358
|
...userState,
|
|
412
359
|
syncState: {
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
_lastPulled: userState._lastPulled ?? {}
|
|
360
|
+
// set defaults
|
|
361
|
+
status: "hydrating",
|
|
362
|
+
error: void 0,
|
|
363
|
+
enabled: false,
|
|
364
|
+
firstLoadDone: false,
|
|
365
|
+
pendingChanges: [],
|
|
366
|
+
lastPulled: {}
|
|
367
|
+
}
|
|
422
368
|
};
|
|
423
369
|
};
|
|
424
370
|
return (0, import_middleware.persist)(creator, wrappedPersistOptions);
|
|
425
371
|
}
|
|
372
|
+
function createStoreWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}) {
|
|
373
|
+
return (0, import_zustand.create)(persistWithSync(stateCreator, persistOptions, syncApi, syncOptions));
|
|
374
|
+
}
|
|
426
375
|
function orderFor(a) {
|
|
427
376
|
switch (a) {
|
|
428
|
-
case "
|
|
377
|
+
case "createOrUpdate" /* CreateOrUpdate */:
|
|
429
378
|
return 1;
|
|
430
|
-
case "update" /* Update */:
|
|
431
|
-
return 2;
|
|
432
379
|
case "remove" /* Remove */:
|
|
433
|
-
return
|
|
380
|
+
return 2;
|
|
434
381
|
}
|
|
435
382
|
}
|
|
436
383
|
async function pull(set, get, stateKey, api, logger) {
|
|
437
|
-
const lastPulled = get().
|
|
384
|
+
const lastPulled = get().syncState.lastPulled || {};
|
|
438
385
|
const lastPulledAt = new Date(lastPulled[stateKey] || /* @__PURE__ */ new Date(0));
|
|
439
386
|
logger.debug(`[persistWithSync] pull:start stateKey=${stateKey} since=${lastPulledAt.toISOString()}`);
|
|
440
387
|
const serverData = await api.list(lastPulledAt);
|
|
441
388
|
if (!serverData?.length) return;
|
|
442
389
|
let newest = lastPulledAt;
|
|
443
390
|
set((state) => {
|
|
444
|
-
const
|
|
445
|
-
const
|
|
446
|
-
|
|
447
|
-
const
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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
|
+
}
|
|
452
402
|
for (const remote of serverData) {
|
|
453
403
|
const remoteUpdated = new Date(remote.updated_at);
|
|
454
404
|
if (remoteUpdated > newest) newest = remoteUpdated;
|
|
455
405
|
const localItem = localById.get(remote.id);
|
|
456
|
-
if (
|
|
406
|
+
if (pendingRemovalIds.has(remote.id)) {
|
|
457
407
|
logger.debug(`[persistWithSync] pull:skip-pending-remove stateKey=${stateKey} id=${remote.id}`);
|
|
458
408
|
continue;
|
|
459
409
|
}
|
|
460
410
|
if (remote.deleted) {
|
|
461
411
|
if (localItem) {
|
|
462
|
-
|
|
412
|
+
nextItems = nextItems.filter((i) => i.id !== remote.id);
|
|
463
413
|
logger.debug(`[persistWithSync] pull:remove stateKey=${stateKey} id=${remote.id}`);
|
|
464
414
|
}
|
|
465
415
|
continue;
|
|
466
416
|
}
|
|
467
|
-
const pending =
|
|
468
|
-
(p) => p.stateKey === stateKey && localItem && p.localId === localItem._localId
|
|
469
|
-
);
|
|
417
|
+
const pending = localItem && pendingChanges.some((p) => p.stateKey === stateKey && p.localId === localItem._localId);
|
|
470
418
|
if (localItem && !pending) {
|
|
471
|
-
const merged = {
|
|
472
|
-
|
|
419
|
+
const merged = {
|
|
420
|
+
...localItem,
|
|
421
|
+
...remote,
|
|
422
|
+
_localId: localItem._localId
|
|
423
|
+
};
|
|
424
|
+
nextItems = nextItems.map((i) => i._localId === localItem._localId ? merged : i);
|
|
473
425
|
logger.debug(`[persistWithSync] pull:merge stateKey=${stateKey} id=${remote.id}`);
|
|
474
426
|
} else if (!localItem) {
|
|
475
|
-
|
|
427
|
+
nextItems = [...nextItems, { ...remote, _localId: nextLocalId() }];
|
|
476
428
|
logger.debug(`[persistWithSync] pull:add stateKey=${stateKey} id=${remote.id}`);
|
|
477
429
|
}
|
|
478
430
|
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
431
|
+
return {
|
|
432
|
+
[stateKey]: nextItems,
|
|
433
|
+
syncState: {
|
|
434
|
+
...state.syncState || {},
|
|
435
|
+
lastPulled: {
|
|
436
|
+
...state.syncState.lastPulled || {},
|
|
437
|
+
[stateKey]: newest.toISOString()
|
|
438
|
+
}
|
|
439
|
+
}
|
|
482
440
|
};
|
|
483
|
-
return state;
|
|
484
441
|
});
|
|
485
442
|
}
|
|
486
443
|
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);
|
|
444
|
+
logger.debug(`[persistWithSync] push:attempt action=${change.action} stateKey=${change.stateKey} localId=${change.localId}`);
|
|
445
|
+
const { action, stateKey, localId, id, version } = change;
|
|
494
446
|
switch (action) {
|
|
495
|
-
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);
|
|
496
456
|
if (!item) {
|
|
457
|
+
logger.warn(`[persistWithSync] push:${action}:no-local-item`, {
|
|
458
|
+
stateKey,
|
|
459
|
+
localId
|
|
460
|
+
});
|
|
497
461
|
removeFromPendingChanges(set, localId, stateKey);
|
|
498
462
|
return;
|
|
499
463
|
}
|
|
464
|
+
let omittedItem = omitSyncFields(item);
|
|
500
465
|
if (item.id) {
|
|
501
|
-
|
|
502
|
-
|
|
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);
|
|
503
501
|
}
|
|
504
|
-
removeFromPendingChanges(set, localId, stateKey);
|
|
505
502
|
return;
|
|
506
503
|
}
|
|
507
|
-
const
|
|
508
|
-
const result = await api.add(omitSyncFields(payload));
|
|
504
|
+
const result = await api.add(omittedItem);
|
|
509
505
|
if (result) {
|
|
510
|
-
logger.debug("[persistWithSync] push:create:success", {
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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
|
|
516
520
|
});
|
|
517
|
-
onAfterRemoteAdd?.(set, get, queueToSync, stateKey, { ...item, ...result });
|
|
518
521
|
} 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;
|
|
522
|
+
logger.warn("[persistWithSync] push:create:no-result", {
|
|
523
|
+
stateKey,
|
|
524
|
+
localId
|
|
535
525
|
});
|
|
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
|
-
}
|
|
526
|
+
if (samePendingVersion(get, stateKey, localId, version)) {
|
|
527
|
+
removeFromPendingChanges(set, localId, stateKey);
|
|
574
528
|
}
|
|
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
529
|
}
|
|
588
|
-
removeFromPendingChanges(set, localId, stateKey);
|
|
589
530
|
break;
|
|
590
|
-
}
|
|
591
531
|
}
|
|
592
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
|
+
}
|
|
593
538
|
function removeFromPendingChanges(set, localId, stateKey) {
|
|
594
539
|
set((s) => {
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
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
|
+
};
|
|
599
547
|
});
|
|
600
548
|
}
|
|
601
549
|
function omitSyncFields(item) {
|
|
@@ -607,7 +555,13 @@ function nextLocalId() {
|
|
|
607
555
|
return crypto.randomUUID();
|
|
608
556
|
}
|
|
609
557
|
function newLogger(logger, min) {
|
|
610
|
-
const order = {
|
|
558
|
+
const order = {
|
|
559
|
+
debug: 10,
|
|
560
|
+
info: 20,
|
|
561
|
+
warn: 30,
|
|
562
|
+
error: 40,
|
|
563
|
+
none: 100
|
|
564
|
+
};
|
|
611
565
|
const threshold = order[min];
|
|
612
566
|
const enabled = (lvl) => order[lvl] >= threshold;
|
|
613
567
|
return {
|
|
@@ -628,6 +582,7 @@ function findApi(stateKey, syncApi) {
|
|
|
628
582
|
0 && (module.exports = {
|
|
629
583
|
SyncAction,
|
|
630
584
|
createIndexedDBStorage,
|
|
585
|
+
createStoreWithSync,
|
|
631
586
|
nextLocalId,
|
|
632
587
|
persistWithSync
|
|
633
588
|
});
|