@anfenn/zync 0.1.8 → 0.1.10
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/README.md +21 -0
- package/dist/build-ELJGW24D.js +252 -0
- package/dist/build-ELJGW24D.js.map +1 -0
- package/dist/index.cjs +595 -329
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +35 -38
- package/dist/index.d.ts +35 -38
- package/dist/index.js +337 -325
- package/dist/index.js.map +1 -1
- package/package.json +12 -9
package/dist/index.cjs
CHANGED
|
@@ -3,6 +3,9 @@ var __defProp = Object.defineProperty;
|
|
|
3
3
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
5
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __esm = (fn, res) => function __init() {
|
|
7
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
8
|
+
};
|
|
6
9
|
var __export = (target, all) => {
|
|
7
10
|
for (var name in all)
|
|
8
11
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
@@ -17,12 +20,269 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
17
20
|
};
|
|
18
21
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
22
|
|
|
23
|
+
// node_modules/.pnpm/idb@8.0.3/node_modules/idb/build/index.js
|
|
24
|
+
var build_exports = {};
|
|
25
|
+
__export(build_exports, {
|
|
26
|
+
deleteDB: () => deleteDB,
|
|
27
|
+
openDB: () => openDB,
|
|
28
|
+
unwrap: () => unwrap,
|
|
29
|
+
wrap: () => wrap
|
|
30
|
+
});
|
|
31
|
+
function getIdbProxyableTypes() {
|
|
32
|
+
return idbProxyableTypes || (idbProxyableTypes = [
|
|
33
|
+
IDBDatabase,
|
|
34
|
+
IDBObjectStore,
|
|
35
|
+
IDBIndex,
|
|
36
|
+
IDBCursor,
|
|
37
|
+
IDBTransaction
|
|
38
|
+
]);
|
|
39
|
+
}
|
|
40
|
+
function getCursorAdvanceMethods() {
|
|
41
|
+
return cursorAdvanceMethods || (cursorAdvanceMethods = [
|
|
42
|
+
IDBCursor.prototype.advance,
|
|
43
|
+
IDBCursor.prototype.continue,
|
|
44
|
+
IDBCursor.prototype.continuePrimaryKey
|
|
45
|
+
]);
|
|
46
|
+
}
|
|
47
|
+
function promisifyRequest(request) {
|
|
48
|
+
const promise = new Promise((resolve, reject) => {
|
|
49
|
+
const unlisten = () => {
|
|
50
|
+
request.removeEventListener("success", success);
|
|
51
|
+
request.removeEventListener("error", error);
|
|
52
|
+
};
|
|
53
|
+
const success = () => {
|
|
54
|
+
resolve(wrap(request.result));
|
|
55
|
+
unlisten();
|
|
56
|
+
};
|
|
57
|
+
const error = () => {
|
|
58
|
+
reject(request.error);
|
|
59
|
+
unlisten();
|
|
60
|
+
};
|
|
61
|
+
request.addEventListener("success", success);
|
|
62
|
+
request.addEventListener("error", error);
|
|
63
|
+
});
|
|
64
|
+
reverseTransformCache.set(promise, request);
|
|
65
|
+
return promise;
|
|
66
|
+
}
|
|
67
|
+
function cacheDonePromiseForTransaction(tx) {
|
|
68
|
+
if (transactionDoneMap.has(tx))
|
|
69
|
+
return;
|
|
70
|
+
const done = new Promise((resolve, reject) => {
|
|
71
|
+
const unlisten = () => {
|
|
72
|
+
tx.removeEventListener("complete", complete);
|
|
73
|
+
tx.removeEventListener("error", error);
|
|
74
|
+
tx.removeEventListener("abort", error);
|
|
75
|
+
};
|
|
76
|
+
const complete = () => {
|
|
77
|
+
resolve();
|
|
78
|
+
unlisten();
|
|
79
|
+
};
|
|
80
|
+
const error = () => {
|
|
81
|
+
reject(tx.error || new DOMException("AbortError", "AbortError"));
|
|
82
|
+
unlisten();
|
|
83
|
+
};
|
|
84
|
+
tx.addEventListener("complete", complete);
|
|
85
|
+
tx.addEventListener("error", error);
|
|
86
|
+
tx.addEventListener("abort", error);
|
|
87
|
+
});
|
|
88
|
+
transactionDoneMap.set(tx, done);
|
|
89
|
+
}
|
|
90
|
+
function replaceTraps(callback) {
|
|
91
|
+
idbProxyTraps = callback(idbProxyTraps);
|
|
92
|
+
}
|
|
93
|
+
function wrapFunction(func) {
|
|
94
|
+
if (getCursorAdvanceMethods().includes(func)) {
|
|
95
|
+
return function(...args) {
|
|
96
|
+
func.apply(unwrap(this), args);
|
|
97
|
+
return wrap(this.request);
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return function(...args) {
|
|
101
|
+
return wrap(func.apply(unwrap(this), args));
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function transformCachableValue(value) {
|
|
105
|
+
if (typeof value === "function")
|
|
106
|
+
return wrapFunction(value);
|
|
107
|
+
if (value instanceof IDBTransaction)
|
|
108
|
+
cacheDonePromiseForTransaction(value);
|
|
109
|
+
if (instanceOfAny(value, getIdbProxyableTypes()))
|
|
110
|
+
return new Proxy(value, idbProxyTraps);
|
|
111
|
+
return value;
|
|
112
|
+
}
|
|
113
|
+
function wrap(value) {
|
|
114
|
+
if (value instanceof IDBRequest)
|
|
115
|
+
return promisifyRequest(value);
|
|
116
|
+
if (transformCache.has(value))
|
|
117
|
+
return transformCache.get(value);
|
|
118
|
+
const newValue = transformCachableValue(value);
|
|
119
|
+
if (newValue !== value) {
|
|
120
|
+
transformCache.set(value, newValue);
|
|
121
|
+
reverseTransformCache.set(newValue, value);
|
|
122
|
+
}
|
|
123
|
+
return newValue;
|
|
124
|
+
}
|
|
125
|
+
function openDB(name, version, { blocked, upgrade, blocking, terminated } = {}) {
|
|
126
|
+
const request = indexedDB.open(name, version);
|
|
127
|
+
const openPromise = wrap(request);
|
|
128
|
+
if (upgrade) {
|
|
129
|
+
request.addEventListener("upgradeneeded", (event) => {
|
|
130
|
+
upgrade(wrap(request.result), event.oldVersion, event.newVersion, wrap(request.transaction), event);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
if (blocked) {
|
|
134
|
+
request.addEventListener("blocked", (event) => blocked(
|
|
135
|
+
// Casting due to https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/1405
|
|
136
|
+
event.oldVersion,
|
|
137
|
+
event.newVersion,
|
|
138
|
+
event
|
|
139
|
+
));
|
|
140
|
+
}
|
|
141
|
+
openPromise.then((db) => {
|
|
142
|
+
if (terminated)
|
|
143
|
+
db.addEventListener("close", () => terminated());
|
|
144
|
+
if (blocking) {
|
|
145
|
+
db.addEventListener("versionchange", (event) => blocking(event.oldVersion, event.newVersion, event));
|
|
146
|
+
}
|
|
147
|
+
}).catch(() => {
|
|
148
|
+
});
|
|
149
|
+
return openPromise;
|
|
150
|
+
}
|
|
151
|
+
function deleteDB(name, { blocked } = {}) {
|
|
152
|
+
const request = indexedDB.deleteDatabase(name);
|
|
153
|
+
if (blocked) {
|
|
154
|
+
request.addEventListener("blocked", (event) => blocked(
|
|
155
|
+
// Casting due to https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/1405
|
|
156
|
+
event.oldVersion,
|
|
157
|
+
event
|
|
158
|
+
));
|
|
159
|
+
}
|
|
160
|
+
return wrap(request).then(() => void 0);
|
|
161
|
+
}
|
|
162
|
+
function getMethod(target, prop) {
|
|
163
|
+
if (!(target instanceof IDBDatabase && !(prop in target) && typeof prop === "string")) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (cachedMethods.get(prop))
|
|
167
|
+
return cachedMethods.get(prop);
|
|
168
|
+
const targetFuncName = prop.replace(/FromIndex$/, "");
|
|
169
|
+
const useIndex = prop !== targetFuncName;
|
|
170
|
+
const isWrite = writeMethods.includes(targetFuncName);
|
|
171
|
+
if (
|
|
172
|
+
// Bail if the target doesn't exist on the target. Eg, getAll isn't in Edge.
|
|
173
|
+
!(targetFuncName in (useIndex ? IDBIndex : IDBObjectStore).prototype) || !(isWrite || readMethods.includes(targetFuncName))
|
|
174
|
+
) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const method = async function(storeName, ...args) {
|
|
178
|
+
const tx = this.transaction(storeName, isWrite ? "readwrite" : "readonly");
|
|
179
|
+
let target2 = tx.store;
|
|
180
|
+
if (useIndex)
|
|
181
|
+
target2 = target2.index(args.shift());
|
|
182
|
+
return (await Promise.all([
|
|
183
|
+
target2[targetFuncName](...args),
|
|
184
|
+
isWrite && tx.done
|
|
185
|
+
]))[0];
|
|
186
|
+
};
|
|
187
|
+
cachedMethods.set(prop, method);
|
|
188
|
+
return method;
|
|
189
|
+
}
|
|
190
|
+
async function* iterate(...args) {
|
|
191
|
+
let cursor = this;
|
|
192
|
+
if (!(cursor instanceof IDBCursor)) {
|
|
193
|
+
cursor = await cursor.openCursor(...args);
|
|
194
|
+
}
|
|
195
|
+
if (!cursor)
|
|
196
|
+
return;
|
|
197
|
+
cursor = cursor;
|
|
198
|
+
const proxiedCursor = new Proxy(cursor, cursorIteratorTraps);
|
|
199
|
+
ittrProxiedCursorToOriginalProxy.set(proxiedCursor, cursor);
|
|
200
|
+
reverseTransformCache.set(proxiedCursor, unwrap(cursor));
|
|
201
|
+
while (cursor) {
|
|
202
|
+
yield proxiedCursor;
|
|
203
|
+
cursor = await (advanceResults.get(proxiedCursor) || cursor.continue());
|
|
204
|
+
advanceResults.delete(proxiedCursor);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function isIteratorProp(target, prop) {
|
|
208
|
+
return prop === Symbol.asyncIterator && instanceOfAny(target, [IDBIndex, IDBObjectStore, IDBCursor]) || prop === "iterate" && instanceOfAny(target, [IDBIndex, IDBObjectStore]);
|
|
209
|
+
}
|
|
210
|
+
var instanceOfAny, idbProxyableTypes, cursorAdvanceMethods, transactionDoneMap, transformCache, reverseTransformCache, idbProxyTraps, unwrap, readMethods, writeMethods, cachedMethods, advanceMethodProps, methodMap, advanceResults, ittrProxiedCursorToOriginalProxy, cursorIteratorTraps;
|
|
211
|
+
var init_build = __esm({
|
|
212
|
+
"node_modules/.pnpm/idb@8.0.3/node_modules/idb/build/index.js"() {
|
|
213
|
+
"use strict";
|
|
214
|
+
instanceOfAny = (object, constructors) => constructors.some((c) => object instanceof c);
|
|
215
|
+
transactionDoneMap = /* @__PURE__ */ new WeakMap();
|
|
216
|
+
transformCache = /* @__PURE__ */ new WeakMap();
|
|
217
|
+
reverseTransformCache = /* @__PURE__ */ new WeakMap();
|
|
218
|
+
idbProxyTraps = {
|
|
219
|
+
get(target, prop, receiver) {
|
|
220
|
+
if (target instanceof IDBTransaction) {
|
|
221
|
+
if (prop === "done")
|
|
222
|
+
return transactionDoneMap.get(target);
|
|
223
|
+
if (prop === "store") {
|
|
224
|
+
return receiver.objectStoreNames[1] ? void 0 : receiver.objectStore(receiver.objectStoreNames[0]);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return wrap(target[prop]);
|
|
228
|
+
},
|
|
229
|
+
set(target, prop, value) {
|
|
230
|
+
target[prop] = value;
|
|
231
|
+
return true;
|
|
232
|
+
},
|
|
233
|
+
has(target, prop) {
|
|
234
|
+
if (target instanceof IDBTransaction && (prop === "done" || prop === "store")) {
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
return prop in target;
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
unwrap = (value) => reverseTransformCache.get(value);
|
|
241
|
+
readMethods = ["get", "getKey", "getAll", "getAllKeys", "count"];
|
|
242
|
+
writeMethods = ["put", "add", "delete", "clear"];
|
|
243
|
+
cachedMethods = /* @__PURE__ */ new Map();
|
|
244
|
+
replaceTraps((oldTraps) => ({
|
|
245
|
+
...oldTraps,
|
|
246
|
+
get: (target, prop, receiver) => getMethod(target, prop) || oldTraps.get(target, prop, receiver),
|
|
247
|
+
has: (target, prop) => !!getMethod(target, prop) || oldTraps.has(target, prop)
|
|
248
|
+
}));
|
|
249
|
+
advanceMethodProps = ["continue", "continuePrimaryKey", "advance"];
|
|
250
|
+
methodMap = {};
|
|
251
|
+
advanceResults = /* @__PURE__ */ new WeakMap();
|
|
252
|
+
ittrProxiedCursorToOriginalProxy = /* @__PURE__ */ new WeakMap();
|
|
253
|
+
cursorIteratorTraps = {
|
|
254
|
+
get(target, prop) {
|
|
255
|
+
if (!advanceMethodProps.includes(prop))
|
|
256
|
+
return target[prop];
|
|
257
|
+
let cachedFunc = methodMap[prop];
|
|
258
|
+
if (!cachedFunc) {
|
|
259
|
+
cachedFunc = methodMap[prop] = function(...args) {
|
|
260
|
+
advanceResults.set(this, ittrProxiedCursorToOriginalProxy.get(this)[prop](...args));
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
return cachedFunc;
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
replaceTraps((oldTraps) => ({
|
|
267
|
+
...oldTraps,
|
|
268
|
+
get(target, prop, receiver) {
|
|
269
|
+
if (isIteratorProp(target, prop))
|
|
270
|
+
return iterate;
|
|
271
|
+
return oldTraps.get(target, prop, receiver);
|
|
272
|
+
},
|
|
273
|
+
has(target, prop) {
|
|
274
|
+
return isIteratorProp(target, prop) || oldTraps.has(target, prop);
|
|
275
|
+
}
|
|
276
|
+
}));
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
|
|
20
280
|
// src/index.ts
|
|
21
281
|
var index_exports = {};
|
|
22
282
|
__export(index_exports, {
|
|
23
283
|
SyncAction: () => SyncAction,
|
|
24
284
|
createIndexedDBStorage: () => createIndexedDBStorage,
|
|
25
|
-
|
|
285
|
+
createWithSync: () => createWithSync,
|
|
26
286
|
nextLocalId: () => nextLocalId,
|
|
27
287
|
persistWithSync: () => persistWithSync
|
|
28
288
|
});
|
|
@@ -30,27 +290,247 @@ module.exports = __toCommonJS(index_exports);
|
|
|
30
290
|
var import_zustand = require("zustand");
|
|
31
291
|
var import_middleware = require("zustand/middleware");
|
|
32
292
|
|
|
293
|
+
// src/logger.ts
|
|
294
|
+
function newLogger(base, min) {
|
|
295
|
+
const order = {
|
|
296
|
+
debug: 10,
|
|
297
|
+
info: 20,
|
|
298
|
+
warn: 30,
|
|
299
|
+
error: 40,
|
|
300
|
+
none: 100
|
|
301
|
+
};
|
|
302
|
+
const threshold = order[min];
|
|
303
|
+
const enabled = (lvl) => order[lvl] >= threshold;
|
|
304
|
+
return {
|
|
305
|
+
debug: (...a) => enabled("debug") && base.debug?.(...a),
|
|
306
|
+
info: (...a) => enabled("info") && base.info?.(...a),
|
|
307
|
+
warn: (...a) => enabled("warn") && base.warn?.(...a),
|
|
308
|
+
error: (...a) => enabled("error") && base.error?.(...a)
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// src/helpers.ts
|
|
313
|
+
function nextLocalId() {
|
|
314
|
+
return crypto.randomUUID();
|
|
315
|
+
}
|
|
316
|
+
function orderFor(a) {
|
|
317
|
+
switch (a) {
|
|
318
|
+
case "createOrUpdate" /* CreateOrUpdate */:
|
|
319
|
+
return 1;
|
|
320
|
+
case "remove" /* Remove */:
|
|
321
|
+
return 2;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
function omitSyncFields(item, fields) {
|
|
325
|
+
const result = { ...item };
|
|
326
|
+
for (const k of fields) delete result[k];
|
|
327
|
+
return result;
|
|
328
|
+
}
|
|
329
|
+
function samePendingVersion(get, stateKey, localId, version) {
|
|
330
|
+
const q = get().syncState.pendingChanges || [];
|
|
331
|
+
const curChange = q.find((p) => p.localId === localId && p.stateKey === stateKey);
|
|
332
|
+
return curChange?.version === version;
|
|
333
|
+
}
|
|
334
|
+
function removeFromPendingChanges(set, localId, stateKey) {
|
|
335
|
+
set((s) => {
|
|
336
|
+
const queue = (s.syncState.pendingChanges || []).filter((p) => !(p.localId === localId && p.stateKey === stateKey));
|
|
337
|
+
return {
|
|
338
|
+
syncState: {
|
|
339
|
+
...s.syncState || {},
|
|
340
|
+
pendingChanges: queue
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
function findApi(stateKey, syncApi) {
|
|
346
|
+
const api = syncApi[stateKey];
|
|
347
|
+
if (!api || !api.add || !api.update || !api.remove || !api.list || !api.firstLoad) {
|
|
348
|
+
throw new Error(`Missing API function(s) for state key: ${stateKey}.`);
|
|
349
|
+
}
|
|
350
|
+
return api;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// src/pull.ts
|
|
354
|
+
async function pull(set, get, stateKey, api, logger) {
|
|
355
|
+
const lastPulled = get().syncState.lastPulled || {};
|
|
356
|
+
const lastPulledAt = new Date(lastPulled[stateKey] || /* @__PURE__ */ new Date(0));
|
|
357
|
+
logger.debug(`[Zync] pull:start stateKey=${stateKey} since=${lastPulledAt.toISOString()}`);
|
|
358
|
+
const serverData = await api.list(lastPulledAt);
|
|
359
|
+
if (!serverData?.length) return;
|
|
360
|
+
let newest = lastPulledAt;
|
|
361
|
+
set((state) => {
|
|
362
|
+
const pendingChanges = state.syncState.pendingChanges || [];
|
|
363
|
+
const localItems = state[stateKey] || [];
|
|
364
|
+
let nextItems = [...localItems];
|
|
365
|
+
const localById = new Map(localItems.filter((l) => l.id).map((l) => [l.id, l]));
|
|
366
|
+
const pendingRemovalById = new Set(pendingChanges.filter((p) => p.stateKey === stateKey && p.action === "remove" /* Remove */).map((p) => p.id));
|
|
367
|
+
for (const remote of serverData) {
|
|
368
|
+
const remoteUpdated = new Date(remote.updated_at);
|
|
369
|
+
if (remoteUpdated > newest) newest = remoteUpdated;
|
|
370
|
+
if (pendingRemovalById.has(remote.id)) {
|
|
371
|
+
logger.debug(`[Zync] pull:skip-pending-remove stateKey=${stateKey} id=${remote.id}`);
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
const localItem = localById.get(remote.id);
|
|
375
|
+
if (remote.deleted) {
|
|
376
|
+
if (localItem) {
|
|
377
|
+
nextItems = nextItems.filter((i) => i.id !== remote.id);
|
|
378
|
+
logger.debug(`[Zync] pull:remove stateKey=${stateKey} id=${remote.id}`);
|
|
379
|
+
}
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
const pending = localItem && pendingChanges.some((p) => p.stateKey === stateKey && p.localId === localItem._localId);
|
|
383
|
+
if (localItem && !pending) {
|
|
384
|
+
const merged = {
|
|
385
|
+
...localItem,
|
|
386
|
+
...remote,
|
|
387
|
+
_localId: localItem._localId
|
|
388
|
+
};
|
|
389
|
+
nextItems = nextItems.map((i) => i._localId === localItem._localId ? merged : i);
|
|
390
|
+
logger.debug(`[Zync] pull:merge stateKey=${stateKey} id=${remote.id}`);
|
|
391
|
+
} else if (!localItem) {
|
|
392
|
+
nextItems = [...nextItems, { ...remote, _localId: nextLocalId() }];
|
|
393
|
+
logger.debug(`[Zync] pull:add stateKey=${stateKey} id=${remote.id}`);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return {
|
|
397
|
+
[stateKey]: nextItems,
|
|
398
|
+
syncState: {
|
|
399
|
+
...state.syncState || {},
|
|
400
|
+
lastPulled: {
|
|
401
|
+
...state.syncState.lastPulled || {},
|
|
402
|
+
[stateKey]: newest.toISOString()
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// src/push.ts
|
|
410
|
+
var SYNC_FIELDS = ["id", "_localId", "updated_at", "deleted"];
|
|
411
|
+
async function pushOne(set, get, change, api, logger, queueToSync, missingStrategy, onMissingRemoteRecordDuringUpdate, onAfterRemoteAdd) {
|
|
412
|
+
logger.debug(`[Zync] push:attempt action=${change.action} stateKey=${change.stateKey} localId=${change.localId}`);
|
|
413
|
+
const { action, stateKey, localId, id, version } = change;
|
|
414
|
+
switch (action) {
|
|
415
|
+
case "remove" /* Remove */:
|
|
416
|
+
await api.remove(id);
|
|
417
|
+
logger.debug(`[Zync] push:remove:success ${stateKey} ${localId} ${id}`);
|
|
418
|
+
removeFromPendingChanges(set, localId, stateKey);
|
|
419
|
+
break;
|
|
420
|
+
case "createOrUpdate" /* CreateOrUpdate */: {
|
|
421
|
+
const state = get();
|
|
422
|
+
const items = state[stateKey] || [];
|
|
423
|
+
const item = items.find((i) => i._localId === localId);
|
|
424
|
+
if (!item) {
|
|
425
|
+
logger.warn(`[Zync] push:${action}:no-local-item`, {
|
|
426
|
+
stateKey,
|
|
427
|
+
localId
|
|
428
|
+
});
|
|
429
|
+
removeFromPendingChanges(set, localId, stateKey);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
const omittedItem = omitSyncFields(item, SYNC_FIELDS);
|
|
433
|
+
if (item.id) {
|
|
434
|
+
const changed = await api.update(item.id, omittedItem);
|
|
435
|
+
if (changed) {
|
|
436
|
+
logger.debug("[Zync] push:update:success", {
|
|
437
|
+
stateKey,
|
|
438
|
+
localId,
|
|
439
|
+
id: item.id
|
|
440
|
+
});
|
|
441
|
+
if (samePendingVersion(get, stateKey, localId, version)) {
|
|
442
|
+
removeFromPendingChanges(set, localId, stateKey);
|
|
443
|
+
}
|
|
444
|
+
return;
|
|
445
|
+
} else {
|
|
446
|
+
logger.warn("[Zync] push:update:missingRemote", {
|
|
447
|
+
stateKey,
|
|
448
|
+
localId,
|
|
449
|
+
id: item.id
|
|
450
|
+
});
|
|
451
|
+
switch (missingStrategy) {
|
|
452
|
+
case "deleteLocalRecord":
|
|
453
|
+
set((s) => ({
|
|
454
|
+
[stateKey]: (s[stateKey] || []).filter((i) => i._localId !== localId)
|
|
455
|
+
}));
|
|
456
|
+
break;
|
|
457
|
+
case "insertNewRemoteRecord": {
|
|
458
|
+
omittedItem._localId = crypto.randomUUID();
|
|
459
|
+
omittedItem.updated_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
460
|
+
set((s) => ({
|
|
461
|
+
[stateKey]: (s[stateKey] || []).map((i) => i._localId === localId ? omittedItem : i)
|
|
462
|
+
}));
|
|
463
|
+
queueToSync("createOrUpdate" /* CreateOrUpdate */, stateKey, omittedItem._localId);
|
|
464
|
+
break;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
removeFromPendingChanges(set, localId, stateKey);
|
|
468
|
+
onMissingRemoteRecordDuringUpdate?.(missingStrategy, omittedItem, omittedItem._localId);
|
|
469
|
+
}
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
const result = await api.add(omittedItem);
|
|
473
|
+
if (result) {
|
|
474
|
+
logger.debug("[Zync] push:create:success", {
|
|
475
|
+
stateKey,
|
|
476
|
+
localId,
|
|
477
|
+
id: result.id
|
|
478
|
+
});
|
|
479
|
+
set((s) => ({
|
|
480
|
+
[stateKey]: (s[stateKey] || []).map((i) => i._localId === localId ? { ...i, ...result } : i)
|
|
481
|
+
}));
|
|
482
|
+
if (samePendingVersion(get, stateKey, localId, version)) {
|
|
483
|
+
removeFromPendingChanges(set, localId, stateKey);
|
|
484
|
+
}
|
|
485
|
+
onAfterRemoteAdd?.(set, get, queueToSync, stateKey, {
|
|
486
|
+
...item,
|
|
487
|
+
...result
|
|
488
|
+
});
|
|
489
|
+
} else {
|
|
490
|
+
logger.warn("[Zync] push:create:no-result", {
|
|
491
|
+
stateKey,
|
|
492
|
+
localId
|
|
493
|
+
});
|
|
494
|
+
if (samePendingVersion(get, stateKey, localId, version)) {
|
|
495
|
+
removeFromPendingChanges(set, localId, stateKey);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
break;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
33
503
|
// src/indexedDBStorage.ts
|
|
34
|
-
var import_idb = require("idb");
|
|
35
504
|
function createIndexedDBStorage(options) {
|
|
36
505
|
const dbName = options.dbName;
|
|
37
506
|
const storeName = options.storeName;
|
|
38
|
-
let dbPromise =
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
507
|
+
let dbPromise = null;
|
|
508
|
+
async function initDB() {
|
|
509
|
+
if (dbPromise) return dbPromise;
|
|
510
|
+
try {
|
|
511
|
+
const idb = await Promise.resolve().then(() => (init_build(), build_exports));
|
|
512
|
+
dbPromise = idb.openDB(dbName, 1, {
|
|
513
|
+
upgrade(db) {
|
|
514
|
+
if (!db.objectStoreNames.contains(storeName)) {
|
|
515
|
+
db.createObjectStore(storeName);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
return dbPromise;
|
|
520
|
+
} catch (_e) {
|
|
521
|
+
throw new Error('Missing optional dependency "idb". Install it to use IndexedDB storage: npm install idb');
|
|
43
522
|
}
|
|
44
|
-
}
|
|
523
|
+
}
|
|
45
524
|
async function ensureStore() {
|
|
46
|
-
const db = await
|
|
525
|
+
const db = await initDB();
|
|
47
526
|
if (db.objectStoreNames.contains(storeName)) return;
|
|
48
527
|
const nextVersion = (db.version || 0) + 1;
|
|
49
528
|
try {
|
|
50
529
|
db.close();
|
|
51
|
-
} catch (
|
|
530
|
+
} catch (_e) {
|
|
52
531
|
}
|
|
53
|
-
|
|
532
|
+
const idb = await Promise.resolve().then(() => (init_build(), build_exports));
|
|
533
|
+
dbPromise = idb.openDB(dbName, nextVersion, {
|
|
54
534
|
upgrade(upg) {
|
|
55
535
|
if (!upg.objectStoreNames.contains(storeName)) upg.createObjectStore(storeName);
|
|
56
536
|
}
|
|
@@ -59,13 +539,13 @@ function createIndexedDBStorage(options) {
|
|
|
59
539
|
}
|
|
60
540
|
async function withRetry(fn) {
|
|
61
541
|
try {
|
|
62
|
-
const db = await
|
|
542
|
+
const db = await initDB();
|
|
63
543
|
return await fn(db);
|
|
64
544
|
} catch (err) {
|
|
65
545
|
const msg = String(err && err.message ? err.message : err);
|
|
66
546
|
if (err && (err.name === "NotFoundError" || /objectStore/i.test(msg))) {
|
|
67
547
|
await ensureStore();
|
|
68
|
-
const db2 = await
|
|
548
|
+
const db2 = await initDB();
|
|
69
549
|
return await fn(db2);
|
|
70
550
|
}
|
|
71
551
|
throw err;
|
|
@@ -76,19 +556,16 @@ function createIndexedDBStorage(options) {
|
|
|
76
556
|
return withRetry(async (db) => {
|
|
77
557
|
let v = await db.get(storeName, name);
|
|
78
558
|
v = v ?? null;
|
|
79
|
-
console.log("getItem:", db.objectStoreNames, storeName, name, v);
|
|
80
559
|
return v;
|
|
81
560
|
});
|
|
82
561
|
},
|
|
83
562
|
setItem: async (name, value) => {
|
|
84
563
|
return withRetry(async (db) => {
|
|
85
|
-
console.log("setItem", name, value);
|
|
86
564
|
await db.put(storeName, value, name);
|
|
87
565
|
});
|
|
88
566
|
},
|
|
89
567
|
removeItem: async (name) => {
|
|
90
568
|
return withRetry(async (db) => {
|
|
91
|
-
console.log("removeItem", name);
|
|
92
569
|
await db.delete(storeName, name);
|
|
93
570
|
});
|
|
94
571
|
}
|
|
@@ -105,7 +582,14 @@ var DEFAULT_SYNC_INTERVAL_MILLIS = 5e3;
|
|
|
105
582
|
var DEFAULT_LOGGER = console;
|
|
106
583
|
var DEFAULT_MIN_LOG_LEVEL = "debug";
|
|
107
584
|
var DEFAULT_MISSING_REMOTE_RECORD_ON_UPDATE_STRATEGY = "ignore";
|
|
108
|
-
|
|
585
|
+
function createWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}) {
|
|
586
|
+
const store = (0, import_zustand.create)(persistWithSync(stateCreator, persistOptions, syncApi, syncOptions));
|
|
587
|
+
return new Promise((resolve) => {
|
|
588
|
+
store.persist.onFinishHydration((_state) => {
|
|
589
|
+
resolve(store);
|
|
590
|
+
});
|
|
591
|
+
});
|
|
592
|
+
}
|
|
109
593
|
function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}) {
|
|
110
594
|
const syncInterval = syncOptions.syncInterval ?? DEFAULT_SYNC_INTERVAL_MILLIS;
|
|
111
595
|
const missingStrategy = syncOptions.missingRemoteRecordDuringUpdateStrategy ?? DEFAULT_MISSING_REMOTE_RECORD_ON_UPDATE_STRATEGY;
|
|
@@ -115,14 +599,13 @@ function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}
|
|
|
115
599
|
const wrappedPersistOptions = {
|
|
116
600
|
...persistOptions,
|
|
117
601
|
onRehydrateStorage: () => {
|
|
118
|
-
logger.debug("[
|
|
602
|
+
logger.debug("[Zync] Rehydration started");
|
|
119
603
|
return (state, error) => {
|
|
120
604
|
if (error) {
|
|
121
|
-
logger.error("[
|
|
605
|
+
logger.error("[Zync] Rehydration failed", error);
|
|
122
606
|
} else {
|
|
123
607
|
baseOnRehydrate?.(state, error);
|
|
124
|
-
|
|
125
|
-
logger.debug("[persistWithSync] Rehydration complete");
|
|
608
|
+
logger.debug("[Zync] Rehydration complete", state);
|
|
126
609
|
}
|
|
127
610
|
};
|
|
128
611
|
},
|
|
@@ -137,24 +620,18 @@ function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}
|
|
|
137
620
|
lastPulled: syncState.lastPulled
|
|
138
621
|
}
|
|
139
622
|
};
|
|
623
|
+
},
|
|
624
|
+
merge: (persisted, current) => {
|
|
625
|
+
const state = { ...current, ...persisted };
|
|
626
|
+
return {
|
|
627
|
+
...state,
|
|
628
|
+
syncState: {
|
|
629
|
+
...state.syncState,
|
|
630
|
+
status: "idle"
|
|
631
|
+
// this confirms 'hydrating' is done
|
|
632
|
+
}
|
|
633
|
+
};
|
|
140
634
|
}
|
|
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
|
-
// },
|
|
158
635
|
};
|
|
159
636
|
const creator = (set, get, storeApi) => {
|
|
160
637
|
let syncIntervalId;
|
|
@@ -174,7 +651,7 @@ function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}
|
|
|
174
651
|
await pull(set, get, stateKey, api, logger);
|
|
175
652
|
} catch (err) {
|
|
176
653
|
syncError = syncError ?? err;
|
|
177
|
-
logger.error(`[
|
|
654
|
+
logger.error(`[Zync] Pull error for stateKey: ${stateKey}`, err);
|
|
178
655
|
}
|
|
179
656
|
}
|
|
180
657
|
const snapshot = [...get().syncState.pendingChanges || []];
|
|
@@ -195,7 +672,7 @@ function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}
|
|
|
195
672
|
);
|
|
196
673
|
} catch (err) {
|
|
197
674
|
syncError = syncError ?? err;
|
|
198
|
-
logger.error(`[
|
|
675
|
+
logger.error(`[Zync] Push error for change: ${change}`, err);
|
|
199
676
|
}
|
|
200
677
|
}
|
|
201
678
|
set((state2) => ({
|
|
@@ -205,41 +682,101 @@ function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}
|
|
|
205
682
|
error: syncError
|
|
206
683
|
}
|
|
207
684
|
}));
|
|
685
|
+
if (get().syncState.pendingChanges.length > 0 && !syncError) {
|
|
686
|
+
await syncOnce();
|
|
687
|
+
}
|
|
208
688
|
}
|
|
209
|
-
function
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
689
|
+
async function startFirstLoad() {
|
|
690
|
+
let syncError;
|
|
691
|
+
for (const stateKey of Object.keys(syncApi)) {
|
|
692
|
+
try {
|
|
693
|
+
logger.info(`[Zync] firstLoad:start for stateKey: ${stateKey}`);
|
|
694
|
+
const api = findApi(stateKey, syncApi);
|
|
695
|
+
let lastId;
|
|
696
|
+
while (true) {
|
|
697
|
+
const batch = await api.firstLoad(lastId);
|
|
698
|
+
if (!batch?.length) break;
|
|
699
|
+
set((state) => {
|
|
700
|
+
const local = state[stateKey] || [];
|
|
701
|
+
const localById = new Map(local.filter((l) => l.id).map((l) => [l.id, l]));
|
|
702
|
+
let newest = new Date(state.syncState.lastPulled[stateKey] || 0);
|
|
703
|
+
const next = [...local];
|
|
704
|
+
for (const remote of batch) {
|
|
705
|
+
const remoteUpdated = new Date(remote.updated_at || 0);
|
|
706
|
+
if (remoteUpdated > newest) newest = remoteUpdated;
|
|
707
|
+
if (remote.deleted) continue;
|
|
708
|
+
const localItem = remote.id ? localById.get(remote.id) : void 0;
|
|
709
|
+
if (localItem) {
|
|
710
|
+
const merged = {
|
|
711
|
+
...localItem,
|
|
712
|
+
...remote,
|
|
713
|
+
_localId: localItem._localId
|
|
714
|
+
};
|
|
715
|
+
const idx = next.findIndex((i) => i._localId === localItem._localId);
|
|
716
|
+
if (idx >= 0) next[idx] = merged;
|
|
717
|
+
} else {
|
|
718
|
+
next.push({
|
|
719
|
+
...remote,
|
|
720
|
+
_localId: nextLocalId()
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
return {
|
|
725
|
+
[stateKey]: next,
|
|
726
|
+
syncState: {
|
|
727
|
+
...state.syncState || {},
|
|
728
|
+
lastPulled: {
|
|
729
|
+
...state.syncState.lastPulled || {},
|
|
730
|
+
[stateKey]: newest.toISOString()
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
};
|
|
218
734
|
});
|
|
219
|
-
|
|
735
|
+
lastId = batch[batch.length - 1].id;
|
|
220
736
|
}
|
|
221
|
-
|
|
222
|
-
|
|
737
|
+
logger.info(`[Zync] firstLoad:done for stateKey: ${stateKey}`);
|
|
738
|
+
} catch (err) {
|
|
739
|
+
syncError = syncError ?? err;
|
|
740
|
+
logger.error(`[Zync] First load pull error for stateKey: ${stateKey}`, err);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
set((state) => ({
|
|
744
|
+
syncState: {
|
|
745
|
+
...state.syncState || {},
|
|
746
|
+
firstLoadDone: true,
|
|
747
|
+
error: syncError
|
|
748
|
+
}
|
|
749
|
+
}));
|
|
750
|
+
}
|
|
751
|
+
function queueToSync(action, stateKey, ...localIds) {
|
|
752
|
+
set((state) => {
|
|
753
|
+
const pendingChanges = state.syncState.pendingChanges || [];
|
|
754
|
+
for (const localId of localIds) {
|
|
755
|
+
const item = state[stateKey].find((i) => i._localId === localId);
|
|
756
|
+
if (!item) {
|
|
757
|
+
logger.error("[Zync] queueToSync:no-local-item", {
|
|
223
758
|
stateKey,
|
|
224
759
|
localId
|
|
225
760
|
});
|
|
226
761
|
continue;
|
|
227
762
|
}
|
|
228
|
-
const queueItem =
|
|
763
|
+
const queueItem = pendingChanges.find((p) => p.localId === localId && p.stateKey === stateKey);
|
|
229
764
|
if (queueItem) {
|
|
230
765
|
queueItem.version += 1;
|
|
231
766
|
if (queueItem.action === "createOrUpdate" /* CreateOrUpdate */ && action === "remove" /* Remove */ && item.id) {
|
|
232
767
|
queueItem.action = "remove" /* Remove */;
|
|
233
768
|
queueItem.id = item.id;
|
|
234
769
|
}
|
|
770
|
+
logger.debug(`[Zync] queueToSync:adjusted ${queueItem.version} ${action} ${item.id} ${stateKey} ${localId}`);
|
|
235
771
|
} else {
|
|
236
|
-
|
|
772
|
+
pendingChanges.push({ action, stateKey, localId, id: item.id, version: 1 });
|
|
773
|
+
logger.debug(`[Zync] queueToSync:added ${action} ${item.id} ${stateKey} ${localId}`);
|
|
237
774
|
}
|
|
238
775
|
}
|
|
239
776
|
return {
|
|
240
777
|
syncState: {
|
|
241
778
|
...state.syncState || {},
|
|
242
|
-
pendingChanges
|
|
779
|
+
pendingChanges
|
|
243
780
|
}
|
|
244
781
|
};
|
|
245
782
|
});
|
|
@@ -280,75 +817,13 @@ function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}
|
|
|
280
817
|
}
|
|
281
818
|
function onVisibilityChange() {
|
|
282
819
|
if (document.visibilityState === "visible") {
|
|
283
|
-
logger.debug("[
|
|
820
|
+
logger.debug("[Zync] Sync started now app is in foreground");
|
|
284
821
|
enableSyncTimer(true);
|
|
285
822
|
} else {
|
|
286
|
-
logger.debug("[
|
|
823
|
+
logger.debug("[Zync] Sync paused now app is in background");
|
|
287
824
|
enableSyncTimer(false);
|
|
288
825
|
}
|
|
289
826
|
}
|
|
290
|
-
async function startFirstLoad() {
|
|
291
|
-
let syncError;
|
|
292
|
-
for (const stateKey of Object.keys(syncApi)) {
|
|
293
|
-
try {
|
|
294
|
-
logger.info(`[persistWithSync] firstLoad:start for stateKey: ${stateKey}`);
|
|
295
|
-
const api = findApi(stateKey, syncApi);
|
|
296
|
-
let lastId;
|
|
297
|
-
while (true) {
|
|
298
|
-
const batch = await api.firstLoad(lastId);
|
|
299
|
-
if (!batch?.length) break;
|
|
300
|
-
set((state) => {
|
|
301
|
-
const local = state[stateKey] || [];
|
|
302
|
-
const localById = new Map(local.filter((l) => l.id).map((l) => [l.id, l]));
|
|
303
|
-
let newest = new Date(state.syncState.lastPulled[stateKey] || 0);
|
|
304
|
-
const next = [...local];
|
|
305
|
-
for (const remote of batch) {
|
|
306
|
-
const remoteUpdated = new Date(remote.updated_at || 0);
|
|
307
|
-
if (remoteUpdated > newest) newest = remoteUpdated;
|
|
308
|
-
if (remote.deleted) continue;
|
|
309
|
-
const localItem = remote.id ? localById.get(remote.id) : void 0;
|
|
310
|
-
if (localItem) {
|
|
311
|
-
const merged = {
|
|
312
|
-
...localItem,
|
|
313
|
-
...remote,
|
|
314
|
-
_localId: localItem._localId
|
|
315
|
-
};
|
|
316
|
-
const idx = next.findIndex((i) => i._localId === localItem._localId);
|
|
317
|
-
if (idx >= 0) next[idx] = merged;
|
|
318
|
-
} else {
|
|
319
|
-
next.push({
|
|
320
|
-
...remote,
|
|
321
|
-
_localId: nextLocalId()
|
|
322
|
-
});
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
return {
|
|
326
|
-
[stateKey]: next,
|
|
327
|
-
syncState: {
|
|
328
|
-
...state.syncState || {},
|
|
329
|
-
lastPulled: {
|
|
330
|
-
...state.syncState.lastPulled || {},
|
|
331
|
-
[stateKey]: newest.toISOString()
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
};
|
|
335
|
-
});
|
|
336
|
-
lastId = batch[batch.length - 1].id;
|
|
337
|
-
}
|
|
338
|
-
logger.info(`[persistWithSync] firstLoad:done for stateKey: ${stateKey}`);
|
|
339
|
-
} catch (err) {
|
|
340
|
-
syncError = syncError ?? err;
|
|
341
|
-
logger.error(`[persistWithSync] First load pull error for stateKey: ${stateKey}`, err);
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
set((state) => ({
|
|
345
|
-
syncState: {
|
|
346
|
-
...state.syncState || {},
|
|
347
|
-
firstLoadDone: true,
|
|
348
|
-
error: syncError
|
|
349
|
-
}
|
|
350
|
-
}));
|
|
351
|
-
}
|
|
352
827
|
storeApi.sync = {
|
|
353
828
|
enable,
|
|
354
829
|
startFirstLoad
|
|
@@ -369,220 +844,11 @@ function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}
|
|
|
369
844
|
};
|
|
370
845
|
return (0, import_middleware.persist)(creator, wrappedPersistOptions);
|
|
371
846
|
}
|
|
372
|
-
function createStoreWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}) {
|
|
373
|
-
return (0, import_zustand.create)(persistWithSync(stateCreator, persistOptions, syncApi, syncOptions));
|
|
374
|
-
}
|
|
375
|
-
function orderFor(a) {
|
|
376
|
-
switch (a) {
|
|
377
|
-
case "createOrUpdate" /* CreateOrUpdate */:
|
|
378
|
-
return 1;
|
|
379
|
-
case "remove" /* Remove */:
|
|
380
|
-
return 2;
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
async function pull(set, get, stateKey, api, logger) {
|
|
384
|
-
const lastPulled = get().syncState.lastPulled || {};
|
|
385
|
-
const lastPulledAt = new Date(lastPulled[stateKey] || /* @__PURE__ */ new Date(0));
|
|
386
|
-
logger.debug(`[persistWithSync] pull:start stateKey=${stateKey} since=${lastPulledAt.toISOString()}`);
|
|
387
|
-
const serverData = await api.list(lastPulledAt);
|
|
388
|
-
if (!serverData?.length) return;
|
|
389
|
-
let newest = lastPulledAt;
|
|
390
|
-
set((state) => {
|
|
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
|
-
}
|
|
402
|
-
for (const remote of serverData) {
|
|
403
|
-
const remoteUpdated = new Date(remote.updated_at);
|
|
404
|
-
if (remoteUpdated > newest) newest = remoteUpdated;
|
|
405
|
-
const localItem = localById.get(remote.id);
|
|
406
|
-
if (pendingRemovalIds.has(remote.id)) {
|
|
407
|
-
logger.debug(`[persistWithSync] pull:skip-pending-remove stateKey=${stateKey} id=${remote.id}`);
|
|
408
|
-
continue;
|
|
409
|
-
}
|
|
410
|
-
if (remote.deleted) {
|
|
411
|
-
if (localItem) {
|
|
412
|
-
nextItems = nextItems.filter((i) => i.id !== remote.id);
|
|
413
|
-
logger.debug(`[persistWithSync] pull:remove stateKey=${stateKey} id=${remote.id}`);
|
|
414
|
-
}
|
|
415
|
-
continue;
|
|
416
|
-
}
|
|
417
|
-
const pending = localItem && pendingChanges.some((p) => p.stateKey === stateKey && p.localId === localItem._localId);
|
|
418
|
-
if (localItem && !pending) {
|
|
419
|
-
const merged = {
|
|
420
|
-
...localItem,
|
|
421
|
-
...remote,
|
|
422
|
-
_localId: localItem._localId
|
|
423
|
-
};
|
|
424
|
-
nextItems = nextItems.map((i) => i._localId === localItem._localId ? merged : i);
|
|
425
|
-
logger.debug(`[persistWithSync] pull:merge stateKey=${stateKey} id=${remote.id}`);
|
|
426
|
-
} else if (!localItem) {
|
|
427
|
-
nextItems = [...nextItems, { ...remote, _localId: nextLocalId() }];
|
|
428
|
-
logger.debug(`[persistWithSync] pull:add stateKey=${stateKey} id=${remote.id}`);
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
return {
|
|
432
|
-
[stateKey]: nextItems,
|
|
433
|
-
syncState: {
|
|
434
|
-
...state.syncState || {},
|
|
435
|
-
lastPulled: {
|
|
436
|
-
...state.syncState.lastPulled || {},
|
|
437
|
-
[stateKey]: newest.toISOString()
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
};
|
|
441
|
-
});
|
|
442
|
-
}
|
|
443
|
-
async function pushOne(set, get, change, api, logger, queueToSync, missingStrategy, onMissingRemoteRecordDuringUpdate, onAfterRemoteAdd) {
|
|
444
|
-
logger.debug(`[persistWithSync] push:attempt action=${change.action} stateKey=${change.stateKey} localId=${change.localId}`);
|
|
445
|
-
const { action, stateKey, localId, id, version } = change;
|
|
446
|
-
switch (action) {
|
|
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);
|
|
456
|
-
if (!item) {
|
|
457
|
-
logger.warn(`[persistWithSync] push:${action}:no-local-item`, {
|
|
458
|
-
stateKey,
|
|
459
|
-
localId
|
|
460
|
-
});
|
|
461
|
-
removeFromPendingChanges(set, localId, stateKey);
|
|
462
|
-
return;
|
|
463
|
-
}
|
|
464
|
-
let omittedItem = omitSyncFields(item);
|
|
465
|
-
if (item.id) {
|
|
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);
|
|
501
|
-
}
|
|
502
|
-
return;
|
|
503
|
-
}
|
|
504
|
-
const result = await api.add(omittedItem);
|
|
505
|
-
if (result) {
|
|
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
|
|
520
|
-
});
|
|
521
|
-
} else {
|
|
522
|
-
logger.warn("[persistWithSync] push:create:no-result", {
|
|
523
|
-
stateKey,
|
|
524
|
-
localId
|
|
525
|
-
});
|
|
526
|
-
if (samePendingVersion(get, stateKey, localId, version)) {
|
|
527
|
-
removeFromPendingChanges(set, localId, stateKey);
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
break;
|
|
531
|
-
}
|
|
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
|
-
}
|
|
538
|
-
function removeFromPendingChanges(set, localId, stateKey) {
|
|
539
|
-
set((s) => {
|
|
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
|
-
};
|
|
547
|
-
});
|
|
548
|
-
}
|
|
549
|
-
function omitSyncFields(item) {
|
|
550
|
-
const result = { ...item };
|
|
551
|
-
for (const k of SYNC_FIELDS) delete result[k];
|
|
552
|
-
return result;
|
|
553
|
-
}
|
|
554
|
-
function nextLocalId() {
|
|
555
|
-
return crypto.randomUUID();
|
|
556
|
-
}
|
|
557
|
-
function newLogger(logger, min) {
|
|
558
|
-
const order = {
|
|
559
|
-
debug: 10,
|
|
560
|
-
info: 20,
|
|
561
|
-
warn: 30,
|
|
562
|
-
error: 40,
|
|
563
|
-
none: 100
|
|
564
|
-
};
|
|
565
|
-
const threshold = order[min];
|
|
566
|
-
const enabled = (lvl) => order[lvl] >= threshold;
|
|
567
|
-
return {
|
|
568
|
-
debug: (...a) => enabled("debug") && logger.debug?.(...a),
|
|
569
|
-
info: (...a) => enabled("info") && logger.info?.(...a),
|
|
570
|
-
warn: (...a) => enabled("warn") && logger.warn?.(...a),
|
|
571
|
-
error: (...a) => enabled("error") && logger.error?.(...a)
|
|
572
|
-
};
|
|
573
|
-
}
|
|
574
|
-
function findApi(stateKey, syncApi) {
|
|
575
|
-
const api = syncApi[stateKey];
|
|
576
|
-
if (!api || !api.add || !api.update || !api.remove || !api.list || !api.firstLoad) {
|
|
577
|
-
throw new Error(`Missing API function(s) for state key: ${stateKey}.`);
|
|
578
|
-
}
|
|
579
|
-
return api;
|
|
580
|
-
}
|
|
581
847
|
// Annotate the CommonJS export names for ESM import in node:
|
|
582
848
|
0 && (module.exports = {
|
|
583
849
|
SyncAction,
|
|
584
850
|
createIndexedDBStorage,
|
|
585
|
-
|
|
851
|
+
createWithSync,
|
|
586
852
|
nextLocalId,
|
|
587
853
|
persistWithSync
|
|
588
854
|
});
|