@anfenn/zync 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +340 -233
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +41 -9
- package/dist/index.d.ts +41 -9
- package/dist/index.js +339 -234
- package/dist/index.js.map +1 -1
- package/package.json +9 -4
package/dist/index.js
CHANGED
|
@@ -1,23 +1,87 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import "zustand";
|
|
2
|
+
import { create } from "zustand";
|
|
3
3
|
import { persist } from "zustand/middleware";
|
|
4
|
+
|
|
5
|
+
// src/indexedDBStorage.ts
|
|
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);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
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);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
await dbPromise;
|
|
31
|
+
}
|
|
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);
|
|
42
|
+
}
|
|
43
|
+
throw err;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
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
|
+
});
|
|
54
|
+
},
|
|
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
|
+
});
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/index.ts
|
|
4
71
|
var SyncAction = /* @__PURE__ */ ((SyncAction2) => {
|
|
5
|
-
SyncAction2["
|
|
6
|
-
SyncAction2["Update"] = "update";
|
|
72
|
+
SyncAction2["CreateOrUpdate"] = "createOrUpdate";
|
|
7
73
|
SyncAction2["Remove"] = "remove";
|
|
8
74
|
return SyncAction2;
|
|
9
75
|
})(SyncAction || {});
|
|
10
76
|
var DEFAULT_SYNC_INTERVAL_MILLIS = 5e3;
|
|
11
77
|
var DEFAULT_LOGGER = console;
|
|
12
78
|
var DEFAULT_MIN_LOG_LEVEL = "debug";
|
|
13
|
-
var DEFAULT_MISSING_REMOTE_RECORD_ON_UPDATE_STRATEGY = "
|
|
79
|
+
var DEFAULT_MISSING_REMOTE_RECORD_ON_UPDATE_STRATEGY = "ignore";
|
|
14
80
|
var SYNC_FIELDS = ["id", "_localId", "updated_at", "deleted"];
|
|
15
81
|
function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}) {
|
|
16
82
|
const syncInterval = syncOptions.syncInterval ?? DEFAULT_SYNC_INTERVAL_MILLIS;
|
|
17
83
|
const missingStrategy = syncOptions.missingRemoteRecordDuringUpdateStrategy ?? DEFAULT_MISSING_REMOTE_RECORD_ON_UPDATE_STRATEGY;
|
|
18
84
|
const logger = newLogger(syncOptions.logger ?? DEFAULT_LOGGER, syncOptions.minLogLevel ?? DEFAULT_MIN_LOG_LEVEL);
|
|
19
|
-
let startSync;
|
|
20
|
-
let syncIntervalId;
|
|
21
85
|
const baseOnRehydrate = persistOptions?.onRehydrateStorage;
|
|
22
86
|
const basePartialize = persistOptions?.partialize;
|
|
23
87
|
const wrappedPersistOptions = {
|
|
@@ -29,47 +93,52 @@ function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}
|
|
|
29
93
|
logger.error("[persistWithSync] Rehydration failed", error);
|
|
30
94
|
} else {
|
|
31
95
|
baseOnRehydrate?.(state, error);
|
|
96
|
+
state.syncState.status = "idle";
|
|
32
97
|
logger.debug("[persistWithSync] Rehydration complete");
|
|
33
|
-
startSync?.();
|
|
34
98
|
}
|
|
35
99
|
};
|
|
36
100
|
},
|
|
37
101
|
partialize: (s) => {
|
|
38
102
|
const base = basePartialize ? basePartialize(s) : s;
|
|
39
|
-
const { syncState
|
|
103
|
+
const { syncState, ...rest } = base || {};
|
|
40
104
|
return {
|
|
41
105
|
...rest,
|
|
42
106
|
syncState: {
|
|
43
|
-
firstLoadDone:
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
},
|
|
47
|
-
merge: (persisted, current) => {
|
|
48
|
-
const p = persisted || {};
|
|
49
|
-
const c = current || {};
|
|
50
|
-
return {
|
|
51
|
-
...c,
|
|
52
|
-
...p,
|
|
53
|
-
syncState: {
|
|
54
|
-
...c.syncState,
|
|
55
|
-
firstLoadDone: p.syncState?.firstLoadDone ?? c.syncState?.firstLoadDone ?? false,
|
|
56
|
-
status: "idle"
|
|
107
|
+
firstLoadDone: syncState.firstLoadDone,
|
|
108
|
+
pendingChanges: syncState.pendingChanges,
|
|
109
|
+
lastPulled: syncState.lastPulled
|
|
57
110
|
}
|
|
58
111
|
};
|
|
59
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
|
+
// },
|
|
60
130
|
};
|
|
61
|
-
const creator = (set, get) => {
|
|
131
|
+
const creator = (set, get, storeApi) => {
|
|
132
|
+
let syncIntervalId;
|
|
62
133
|
async function syncOnce() {
|
|
63
134
|
const state = get();
|
|
64
135
|
if (!state.syncState.enabled || state.syncState.status !== "idle") return;
|
|
65
|
-
set((state2) => {
|
|
66
|
-
|
|
67
|
-
syncState
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
};
|
|
72
|
-
});
|
|
136
|
+
set((state2) => ({
|
|
137
|
+
syncState: {
|
|
138
|
+
...state2.syncState || {},
|
|
139
|
+
status: "syncing"
|
|
140
|
+
}
|
|
141
|
+
}));
|
|
73
142
|
let syncError;
|
|
74
143
|
for (const stateKey of Object.keys(syncApi)) {
|
|
75
144
|
try {
|
|
@@ -80,7 +149,7 @@ function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}
|
|
|
80
149
|
logger.error(`[persistWithSync] Pull error for stateKey: ${stateKey}`, err);
|
|
81
150
|
}
|
|
82
151
|
}
|
|
83
|
-
const snapshot = [...get().
|
|
152
|
+
const snapshot = [...get().syncState.pendingChanges || []];
|
|
84
153
|
snapshot.sort((a, b) => orderFor(a.action) - orderFor(b.action));
|
|
85
154
|
for (const change of snapshot) {
|
|
86
155
|
try {
|
|
@@ -101,83 +170,95 @@ function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}
|
|
|
101
170
|
logger.error(`[persistWithSync] Push error for change: ${change}`, err);
|
|
102
171
|
}
|
|
103
172
|
}
|
|
104
|
-
set((state2) => {
|
|
105
|
-
|
|
106
|
-
syncState
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
};
|
|
112
|
-
});
|
|
173
|
+
set((state2) => ({
|
|
174
|
+
syncState: {
|
|
175
|
+
...state2.syncState || {},
|
|
176
|
+
status: "idle",
|
|
177
|
+
error: syncError
|
|
178
|
+
}
|
|
179
|
+
}));
|
|
113
180
|
}
|
|
114
|
-
|
|
115
|
-
clearInterval(syncIntervalId);
|
|
116
|
-
syncIntervalId = void 0;
|
|
117
|
-
await syncOnce();
|
|
118
|
-
syncIntervalId = setInterval(syncOnce, syncInterval);
|
|
119
|
-
};
|
|
120
|
-
function queueToSync(action, localId, stateKey, changes) {
|
|
181
|
+
function queueToSync(action, stateKey, ...localIds) {
|
|
121
182
|
set((state) => {
|
|
122
|
-
const queue = state.
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
queue.splice(idx, 1);
|
|
132
|
-
}
|
|
133
|
-
break;
|
|
134
|
-
case "update" /* Update */:
|
|
135
|
-
if (action === "update" /* Update */) {
|
|
136
|
-
existing.changes = { ...existing.changes, ...changes };
|
|
137
|
-
} else if (action === "remove" /* Remove */) {
|
|
138
|
-
existing.action = "remove" /* Remove */;
|
|
139
|
-
delete existing.changes;
|
|
140
|
-
}
|
|
141
|
-
break;
|
|
142
|
-
case "remove" /* Remove */:
|
|
143
|
-
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;
|
|
144
192
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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 });
|
|
149
209
|
}
|
|
150
|
-
queue.push({ stateKey, localId, action, changes });
|
|
151
210
|
}
|
|
152
|
-
state._pendingChanges = queue;
|
|
153
|
-
return state;
|
|
154
|
-
});
|
|
155
|
-
syncOnce();
|
|
156
|
-
}
|
|
157
|
-
function setAndSync(partial) {
|
|
158
|
-
set(partial);
|
|
159
|
-
syncOnce();
|
|
160
|
-
}
|
|
161
|
-
function enableSync(enabled) {
|
|
162
|
-
set((state) => {
|
|
163
211
|
return {
|
|
164
212
|
syncState: {
|
|
165
|
-
...state.syncState,
|
|
166
|
-
|
|
213
|
+
...state.syncState || {},
|
|
214
|
+
pendingChanges: queue
|
|
167
215
|
}
|
|
168
216
|
};
|
|
169
217
|
});
|
|
170
218
|
syncOnce();
|
|
171
219
|
}
|
|
172
|
-
|
|
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) {
|
|
173
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() {
|
|
174
254
|
if (document.visibilityState === "visible") {
|
|
175
|
-
logger.debug("[persistWithSync] Sync
|
|
176
|
-
|
|
255
|
+
logger.debug("[persistWithSync] Sync started now app is in foreground");
|
|
256
|
+
enableSyncTimer(true);
|
|
177
257
|
} else {
|
|
178
258
|
logger.debug("[persistWithSync] Sync paused now app is in background");
|
|
259
|
+
enableSyncTimer(false);
|
|
179
260
|
}
|
|
180
|
-
}
|
|
261
|
+
}
|
|
181
262
|
async function startFirstLoad() {
|
|
182
263
|
let syncError;
|
|
183
264
|
for (const stateKey of Object.keys(syncApi)) {
|
|
@@ -191,7 +272,7 @@ function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}
|
|
|
191
272
|
set((state) => {
|
|
192
273
|
const local = state[stateKey] || [];
|
|
193
274
|
const localById = new Map(local.filter((l) => l.id).map((l) => [l.id, l]));
|
|
194
|
-
let newest = new Date(state.
|
|
275
|
+
let newest = new Date(state.syncState.lastPulled[stateKey] || 0);
|
|
195
276
|
const next = [...local];
|
|
196
277
|
for (const remote of batch) {
|
|
197
278
|
const remoteUpdated = new Date(remote.updated_at || 0);
|
|
@@ -199,19 +280,30 @@ function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}
|
|
|
199
280
|
if (remote.deleted) continue;
|
|
200
281
|
const localItem = remote.id ? localById.get(remote.id) : void 0;
|
|
201
282
|
if (localItem) {
|
|
202
|
-
const merged = {
|
|
283
|
+
const merged = {
|
|
284
|
+
...localItem,
|
|
285
|
+
...remote,
|
|
286
|
+
_localId: localItem._localId
|
|
287
|
+
};
|
|
203
288
|
const idx = next.findIndex((i) => i._localId === localItem._localId);
|
|
204
289
|
if (idx >= 0) next[idx] = merged;
|
|
205
290
|
} else {
|
|
206
|
-
next.push({
|
|
291
|
+
next.push({
|
|
292
|
+
...remote,
|
|
293
|
+
_localId: nextLocalId()
|
|
294
|
+
});
|
|
207
295
|
}
|
|
208
296
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
297
|
+
return {
|
|
298
|
+
[stateKey]: next,
|
|
299
|
+
syncState: {
|
|
300
|
+
...state.syncState || {},
|
|
301
|
+
lastPulled: {
|
|
302
|
+
...state.syncState.lastPulled || {},
|
|
303
|
+
[stateKey]: newest.toISOString()
|
|
304
|
+
}
|
|
305
|
+
}
|
|
213
306
|
};
|
|
214
|
-
return state;
|
|
215
307
|
});
|
|
216
308
|
lastId = batch[batch.length - 1].id;
|
|
217
309
|
}
|
|
@@ -221,204 +313,209 @@ function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}
|
|
|
221
313
|
logger.error(`[persistWithSync] First load pull error for stateKey: ${stateKey}`, err);
|
|
222
314
|
}
|
|
223
315
|
}
|
|
224
|
-
set((state) => {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
316
|
+
set((state) => ({
|
|
317
|
+
syncState: {
|
|
318
|
+
...state.syncState || {},
|
|
319
|
+
firstLoadDone: true,
|
|
320
|
+
error: syncError
|
|
321
|
+
}
|
|
322
|
+
}));
|
|
230
323
|
}
|
|
324
|
+
storeApi.sync = {
|
|
325
|
+
enable,
|
|
326
|
+
startFirstLoad
|
|
327
|
+
};
|
|
231
328
|
const userState = stateCreator(setAndSync, get, queueToSync);
|
|
232
|
-
const syncState = userState.syncState || {};
|
|
233
329
|
return {
|
|
234
330
|
...userState,
|
|
235
331
|
syncState: {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
_lastPulled: userState._lastPulled ?? {}
|
|
332
|
+
// set defaults
|
|
333
|
+
status: "hydrating",
|
|
334
|
+
error: void 0,
|
|
335
|
+
enabled: false,
|
|
336
|
+
firstLoadDone: false,
|
|
337
|
+
pendingChanges: [],
|
|
338
|
+
lastPulled: {}
|
|
339
|
+
}
|
|
245
340
|
};
|
|
246
341
|
};
|
|
247
342
|
return persist(creator, wrappedPersistOptions);
|
|
248
343
|
}
|
|
344
|
+
function createStoreWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}) {
|
|
345
|
+
return create(persistWithSync(stateCreator, persistOptions, syncApi, syncOptions));
|
|
346
|
+
}
|
|
249
347
|
function orderFor(a) {
|
|
250
348
|
switch (a) {
|
|
251
|
-
case "
|
|
349
|
+
case "createOrUpdate" /* CreateOrUpdate */:
|
|
252
350
|
return 1;
|
|
253
|
-
case "update" /* Update */:
|
|
254
|
-
return 2;
|
|
255
351
|
case "remove" /* Remove */:
|
|
256
|
-
return
|
|
352
|
+
return 2;
|
|
257
353
|
}
|
|
258
354
|
}
|
|
259
355
|
async function pull(set, get, stateKey, api, logger) {
|
|
260
|
-
const lastPulled = get().
|
|
356
|
+
const lastPulled = get().syncState.lastPulled || {};
|
|
261
357
|
const lastPulledAt = new Date(lastPulled[stateKey] || /* @__PURE__ */ new Date(0));
|
|
262
358
|
logger.debug(`[persistWithSync] pull:start stateKey=${stateKey} since=${lastPulledAt.toISOString()}`);
|
|
263
359
|
const serverData = await api.list(lastPulledAt);
|
|
264
360
|
if (!serverData?.length) return;
|
|
265
361
|
let newest = lastPulledAt;
|
|
266
362
|
set((state) => {
|
|
267
|
-
const
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
+
}
|
|
275
374
|
for (const remote of serverData) {
|
|
276
375
|
const remoteUpdated = new Date(remote.updated_at);
|
|
277
376
|
if (remoteUpdated > newest) newest = remoteUpdated;
|
|
278
377
|
const localItem = localById.get(remote.id);
|
|
279
|
-
if (
|
|
378
|
+
if (pendingRemovalIds.has(remote.id)) {
|
|
280
379
|
logger.debug(`[persistWithSync] pull:skip-pending-remove stateKey=${stateKey} id=${remote.id}`);
|
|
281
380
|
continue;
|
|
282
381
|
}
|
|
283
382
|
if (remote.deleted) {
|
|
284
383
|
if (localItem) {
|
|
285
|
-
|
|
384
|
+
nextItems = nextItems.filter((i) => i.id !== remote.id);
|
|
286
385
|
logger.debug(`[persistWithSync] pull:remove stateKey=${stateKey} id=${remote.id}`);
|
|
287
386
|
}
|
|
288
387
|
continue;
|
|
289
388
|
}
|
|
290
|
-
const pending =
|
|
291
|
-
(p) => p.stateKey === stateKey && localItem && p.localId === localItem._localId
|
|
292
|
-
);
|
|
389
|
+
const pending = localItem && pendingChanges.some((p) => p.stateKey === stateKey && p.localId === localItem._localId);
|
|
293
390
|
if (localItem && !pending) {
|
|
294
|
-
const merged = {
|
|
295
|
-
|
|
391
|
+
const merged = {
|
|
392
|
+
...localItem,
|
|
393
|
+
...remote,
|
|
394
|
+
_localId: localItem._localId
|
|
395
|
+
};
|
|
396
|
+
nextItems = nextItems.map((i) => i._localId === localItem._localId ? merged : i);
|
|
296
397
|
logger.debug(`[persistWithSync] pull:merge stateKey=${stateKey} id=${remote.id}`);
|
|
297
398
|
} else if (!localItem) {
|
|
298
|
-
|
|
399
|
+
nextItems = [...nextItems, { ...remote, _localId: nextLocalId() }];
|
|
299
400
|
logger.debug(`[persistWithSync] pull:add stateKey=${stateKey} id=${remote.id}`);
|
|
300
401
|
}
|
|
301
402
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
403
|
+
return {
|
|
404
|
+
[stateKey]: nextItems,
|
|
405
|
+
syncState: {
|
|
406
|
+
...state.syncState || {},
|
|
407
|
+
lastPulled: {
|
|
408
|
+
...state.syncState.lastPulled || {},
|
|
409
|
+
[stateKey]: newest.toISOString()
|
|
410
|
+
}
|
|
411
|
+
}
|
|
305
412
|
};
|
|
306
|
-
return state;
|
|
307
413
|
});
|
|
308
414
|
}
|
|
309
415
|
async function pushOne(set, get, change, api, logger, queueToSync, missingStrategy, onMissingRemoteRecordDuringUpdate, onAfterRemoteAdd) {
|
|
310
|
-
logger.debug(
|
|
311
|
-
|
|
312
|
-
);
|
|
313
|
-
const { stateKey, localId, action, changes } = change;
|
|
314
|
-
const state = get();
|
|
315
|
-
const items = state[stateKey] || [];
|
|
316
|
-
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;
|
|
317
418
|
switch (action) {
|
|
318
|
-
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);
|
|
319
428
|
if (!item) {
|
|
429
|
+
logger.warn(`[persistWithSync] push:${action}:no-local-item`, {
|
|
430
|
+
stateKey,
|
|
431
|
+
localId
|
|
432
|
+
});
|
|
320
433
|
removeFromPendingChanges(set, localId, stateKey);
|
|
321
434
|
return;
|
|
322
435
|
}
|
|
436
|
+
let omittedItem = omitSyncFields(item);
|
|
323
437
|
if (item.id) {
|
|
324
|
-
|
|
325
|
-
|
|
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);
|
|
326
473
|
}
|
|
327
|
-
removeFromPendingChanges(set, localId, stateKey);
|
|
328
474
|
return;
|
|
329
475
|
}
|
|
330
|
-
const
|
|
331
|
-
const result = await api.add(omitSyncFields(payload));
|
|
476
|
+
const result = await api.add(omittedItem);
|
|
332
477
|
if (result) {
|
|
333
|
-
logger.debug("[persistWithSync] push:create:success", {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
|
339
492
|
});
|
|
340
|
-
onAfterRemoteAdd?.(set, get, queueToSync, stateKey, { ...item, ...result });
|
|
341
493
|
} else {
|
|
342
|
-
logger.warn("[persistWithSync] push:create:no-result", {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
break;
|
|
346
|
-
}
|
|
347
|
-
case "update" /* Update */: {
|
|
348
|
-
if (!item) {
|
|
349
|
-
removeFromPendingChanges(set, localId, stateKey);
|
|
350
|
-
return;
|
|
351
|
-
}
|
|
352
|
-
if (!item.id) {
|
|
353
|
-
set((s) => {
|
|
354
|
-
const q = s._pendingChanges || [];
|
|
355
|
-
const e = q.find((p) => p.localId === localId && p.stateKey === stateKey);
|
|
356
|
-
if (e) e.action = "create" /* Create */;
|
|
357
|
-
return s;
|
|
494
|
+
logger.warn("[persistWithSync] push:create:no-result", {
|
|
495
|
+
stateKey,
|
|
496
|
+
localId
|
|
358
497
|
});
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
const changed = await api.update(item.id, omitSyncFields({ ...changes }));
|
|
362
|
-
if (!changed) {
|
|
363
|
-
logger.warn("[persistWithSync] push:update:missingRemote", { stateKey, localId, id: item.id });
|
|
364
|
-
const oldRecord = { ...item };
|
|
365
|
-
let newLocalId;
|
|
366
|
-
switch (missingStrategy) {
|
|
367
|
-
case "deleteLocalRecord":
|
|
368
|
-
set((s) => {
|
|
369
|
-
s[stateKey] = (s[stateKey] || []).filter((i) => i._localId !== localId);
|
|
370
|
-
return s;
|
|
371
|
-
});
|
|
372
|
-
removeFromPendingChanges(set, localId, stateKey);
|
|
373
|
-
break;
|
|
374
|
-
case "insertNewRemoteRecord": {
|
|
375
|
-
const freshLocalId = nextLocalId();
|
|
376
|
-
newLocalId = freshLocalId;
|
|
377
|
-
set((s) => {
|
|
378
|
-
s[stateKey] = (s[stateKey] || []).filter((i) => i._localId !== localId);
|
|
379
|
-
s[stateKey].push({
|
|
380
|
-
...omitSyncFields(oldRecord),
|
|
381
|
-
_localId: freshLocalId,
|
|
382
|
-
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
383
|
-
});
|
|
384
|
-
const q = s._pendingChanges || [];
|
|
385
|
-
const e = q.find((p) => p.localId === localId && p.stateKey === stateKey);
|
|
386
|
-
if (e) {
|
|
387
|
-
e.localId = freshLocalId;
|
|
388
|
-
e.action = "create" /* Create */;
|
|
389
|
-
} else {
|
|
390
|
-
q.push({ stateKey, localId: freshLocalId, action: "create" /* Create */, changes });
|
|
391
|
-
}
|
|
392
|
-
s._pendingChanges = q;
|
|
393
|
-
return s;
|
|
394
|
-
});
|
|
395
|
-
break;
|
|
396
|
-
}
|
|
498
|
+
if (samePendingVersion(get, stateKey, localId, version)) {
|
|
499
|
+
removeFromPendingChanges(set, localId, stateKey);
|
|
397
500
|
}
|
|
398
|
-
onMissingRemoteRecordDuringUpdate?.(missingStrategy, oldRecord, newLocalId);
|
|
399
|
-
} else {
|
|
400
|
-
logger.debug("[persistWithSync] push:update:success", { stateKey, localId, id: item.id });
|
|
401
|
-
removeFromPendingChanges(set, localId, stateKey);
|
|
402
501
|
}
|
|
403
502
|
break;
|
|
404
|
-
}
|
|
405
|
-
case "remove" /* Remove */: {
|
|
406
|
-
const id = changes?.id;
|
|
407
|
-
if (id) {
|
|
408
|
-
await api.remove(id);
|
|
409
|
-
logger.debug("[persistWithSync] push:remove:success", { stateKey, localId, id });
|
|
410
|
-
}
|
|
411
|
-
removeFromPendingChanges(set, localId, stateKey);
|
|
412
|
-
break;
|
|
413
|
-
}
|
|
414
503
|
}
|
|
415
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
|
+
}
|
|
416
510
|
function removeFromPendingChanges(set, localId, stateKey) {
|
|
417
511
|
set((s) => {
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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
|
+
};
|
|
422
519
|
});
|
|
423
520
|
}
|
|
424
521
|
function omitSyncFields(item) {
|
|
@@ -430,7 +527,13 @@ function nextLocalId() {
|
|
|
430
527
|
return crypto.randomUUID();
|
|
431
528
|
}
|
|
432
529
|
function newLogger(logger, min) {
|
|
433
|
-
const order = {
|
|
530
|
+
const order = {
|
|
531
|
+
debug: 10,
|
|
532
|
+
info: 20,
|
|
533
|
+
warn: 30,
|
|
534
|
+
error: 40,
|
|
535
|
+
none: 100
|
|
536
|
+
};
|
|
434
537
|
const threshold = order[min];
|
|
435
538
|
const enabled = (lvl) => order[lvl] >= threshold;
|
|
436
539
|
return {
|
|
@@ -449,6 +552,8 @@ function findApi(stateKey, syncApi) {
|
|
|
449
552
|
}
|
|
450
553
|
export {
|
|
451
554
|
SyncAction,
|
|
555
|
+
createIndexedDBStorage,
|
|
556
|
+
createStoreWithSync,
|
|
452
557
|
nextLocalId,
|
|
453
558
|
persistWithSync
|
|
454
559
|
};
|