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