@anfenn/zync 0.1.9 → 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 +589 -317
- 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 +326 -308
- 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;
|
|
@@ -102,7 +582,14 @@ var DEFAULT_SYNC_INTERVAL_MILLIS = 5e3;
|
|
|
102
582
|
var DEFAULT_LOGGER = console;
|
|
103
583
|
var DEFAULT_MIN_LOG_LEVEL = "debug";
|
|
104
584
|
var DEFAULT_MISSING_REMOTE_RECORD_ON_UPDATE_STRATEGY = "ignore";
|
|
105
|
-
|
|
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
|
+
}
|
|
106
593
|
function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}) {
|
|
107
594
|
const syncInterval = syncOptions.syncInterval ?? DEFAULT_SYNC_INTERVAL_MILLIS;
|
|
108
595
|
const missingStrategy = syncOptions.missingRemoteRecordDuringUpdateStrategy ?? DEFAULT_MISSING_REMOTE_RECORD_ON_UPDATE_STRATEGY;
|
|
@@ -112,13 +599,13 @@ function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}
|
|
|
112
599
|
const wrappedPersistOptions = {
|
|
113
600
|
...persistOptions,
|
|
114
601
|
onRehydrateStorage: () => {
|
|
115
|
-
logger.debug("[
|
|
602
|
+
logger.debug("[Zync] Rehydration started");
|
|
116
603
|
return (state, error) => {
|
|
117
604
|
if (error) {
|
|
118
|
-
logger.error("[
|
|
605
|
+
logger.error("[Zync] Rehydration failed", error);
|
|
119
606
|
} else {
|
|
120
607
|
baseOnRehydrate?.(state, error);
|
|
121
|
-
logger.debug("[
|
|
608
|
+
logger.debug("[Zync] Rehydration complete", state);
|
|
122
609
|
}
|
|
123
610
|
};
|
|
124
611
|
},
|
|
@@ -135,12 +622,11 @@ function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}
|
|
|
135
622
|
};
|
|
136
623
|
},
|
|
137
624
|
merge: (persisted, current) => {
|
|
625
|
+
const state = { ...current, ...persisted };
|
|
138
626
|
return {
|
|
139
|
-
...
|
|
140
|
-
...persisted,
|
|
627
|
+
...state,
|
|
141
628
|
syncState: {
|
|
142
|
-
...
|
|
143
|
-
...persisted?.syncState,
|
|
629
|
+
...state.syncState,
|
|
144
630
|
status: "idle"
|
|
145
631
|
// this confirms 'hydrating' is done
|
|
146
632
|
}
|
|
@@ -165,7 +651,7 @@ function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}
|
|
|
165
651
|
await pull(set, get, stateKey, api, logger);
|
|
166
652
|
} catch (err) {
|
|
167
653
|
syncError = syncError ?? err;
|
|
168
|
-
logger.error(`[
|
|
654
|
+
logger.error(`[Zync] Pull error for stateKey: ${stateKey}`, err);
|
|
169
655
|
}
|
|
170
656
|
}
|
|
171
657
|
const snapshot = [...get().syncState.pendingChanges || []];
|
|
@@ -186,7 +672,7 @@ function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}
|
|
|
186
672
|
);
|
|
187
673
|
} catch (err) {
|
|
188
674
|
syncError = syncError ?? err;
|
|
189
|
-
logger.error(`[
|
|
675
|
+
logger.error(`[Zync] Push error for change: ${change}`, err);
|
|
190
676
|
}
|
|
191
677
|
}
|
|
192
678
|
set((state2) => ({
|
|
@@ -200,92 +686,11 @@ function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}
|
|
|
200
686
|
await syncOnce();
|
|
201
687
|
}
|
|
202
688
|
}
|
|
203
|
-
function queueToSync(action, stateKey, ...localIds) {
|
|
204
|
-
set((state) => {
|
|
205
|
-
const queue = state.syncState.pendingChanges || [];
|
|
206
|
-
for (const localId of localIds) {
|
|
207
|
-
const item = state[stateKey].find((i) => i._localId === localId);
|
|
208
|
-
if (!item) {
|
|
209
|
-
logger.error("[persistWithSync] queueToSync:no-local-item", {
|
|
210
|
-
stateKey,
|
|
211
|
-
localId
|
|
212
|
-
});
|
|
213
|
-
continue;
|
|
214
|
-
}
|
|
215
|
-
if (action === "remove" /* Remove */ && !item.id) {
|
|
216
|
-
logger.warn("[persistWithSync] queueToSync:remove-no-id", {
|
|
217
|
-
stateKey,
|
|
218
|
-
localId
|
|
219
|
-
});
|
|
220
|
-
continue;
|
|
221
|
-
}
|
|
222
|
-
const queueItem = queue.find((p) => p.localId === localId && p.stateKey === stateKey);
|
|
223
|
-
if (queueItem) {
|
|
224
|
-
queueItem.version += 1;
|
|
225
|
-
if (queueItem.action === "createOrUpdate" /* CreateOrUpdate */ && action === "remove" /* Remove */ && item.id) {
|
|
226
|
-
queueItem.action = "remove" /* Remove */;
|
|
227
|
-
queueItem.id = item.id;
|
|
228
|
-
}
|
|
229
|
-
} else {
|
|
230
|
-
queue.push({ action, stateKey, localId, id: item.id, version: 1 });
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
return {
|
|
234
|
-
syncState: {
|
|
235
|
-
...state.syncState || {},
|
|
236
|
-
pendingChanges: queue
|
|
237
|
-
}
|
|
238
|
-
};
|
|
239
|
-
});
|
|
240
|
-
syncOnce();
|
|
241
|
-
}
|
|
242
|
-
function setAndSync(partial) {
|
|
243
|
-
if (typeof partial === "function") {
|
|
244
|
-
set((state) => ({ ...partial(state) }));
|
|
245
|
-
} else {
|
|
246
|
-
set(partial);
|
|
247
|
-
}
|
|
248
|
-
syncOnce();
|
|
249
|
-
}
|
|
250
|
-
function enable(enabled) {
|
|
251
|
-
set((state) => ({
|
|
252
|
-
syncState: {
|
|
253
|
-
...state.syncState || {},
|
|
254
|
-
enabled
|
|
255
|
-
}
|
|
256
|
-
}));
|
|
257
|
-
enableSyncTimer(enabled);
|
|
258
|
-
addVisibilityChangeListener(enabled);
|
|
259
|
-
}
|
|
260
|
-
function enableSyncTimer(enabled) {
|
|
261
|
-
clearInterval(syncIntervalId);
|
|
262
|
-
syncIntervalId = void 0;
|
|
263
|
-
if (enabled) {
|
|
264
|
-
syncIntervalId = setInterval(syncOnce, syncInterval);
|
|
265
|
-
syncOnce();
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
function addVisibilityChangeListener(add) {
|
|
269
|
-
if (add) {
|
|
270
|
-
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
271
|
-
} else {
|
|
272
|
-
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
function onVisibilityChange() {
|
|
276
|
-
if (document.visibilityState === "visible") {
|
|
277
|
-
logger.debug("[persistWithSync] Sync started now app is in foreground");
|
|
278
|
-
enableSyncTimer(true);
|
|
279
|
-
} else {
|
|
280
|
-
logger.debug("[persistWithSync] Sync paused now app is in background");
|
|
281
|
-
enableSyncTimer(false);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
689
|
async function startFirstLoad() {
|
|
285
690
|
let syncError;
|
|
286
691
|
for (const stateKey of Object.keys(syncApi)) {
|
|
287
692
|
try {
|
|
288
|
-
logger.info(`[
|
|
693
|
+
logger.info(`[Zync] firstLoad:start for stateKey: ${stateKey}`);
|
|
289
694
|
const api = findApi(stateKey, syncApi);
|
|
290
695
|
let lastId;
|
|
291
696
|
while (true) {
|
|
@@ -329,10 +734,10 @@ function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}
|
|
|
329
734
|
});
|
|
330
735
|
lastId = batch[batch.length - 1].id;
|
|
331
736
|
}
|
|
332
|
-
logger.info(`[
|
|
737
|
+
logger.info(`[Zync] firstLoad:done for stateKey: ${stateKey}`);
|
|
333
738
|
} catch (err) {
|
|
334
739
|
syncError = syncError ?? err;
|
|
335
|
-
logger.error(`[
|
|
740
|
+
logger.error(`[Zync] First load pull error for stateKey: ${stateKey}`, err);
|
|
336
741
|
}
|
|
337
742
|
}
|
|
338
743
|
set((state) => ({
|
|
@@ -343,6 +748,82 @@ function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}
|
|
|
343
748
|
}
|
|
344
749
|
}));
|
|
345
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", {
|
|
758
|
+
stateKey,
|
|
759
|
+
localId
|
|
760
|
+
});
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
763
|
+
const queueItem = pendingChanges.find((p) => p.localId === localId && p.stateKey === stateKey);
|
|
764
|
+
if (queueItem) {
|
|
765
|
+
queueItem.version += 1;
|
|
766
|
+
if (queueItem.action === "createOrUpdate" /* CreateOrUpdate */ && action === "remove" /* Remove */ && item.id) {
|
|
767
|
+
queueItem.action = "remove" /* Remove */;
|
|
768
|
+
queueItem.id = item.id;
|
|
769
|
+
}
|
|
770
|
+
logger.debug(`[Zync] queueToSync:adjusted ${queueItem.version} ${action} ${item.id} ${stateKey} ${localId}`);
|
|
771
|
+
} else {
|
|
772
|
+
pendingChanges.push({ action, stateKey, localId, id: item.id, version: 1 });
|
|
773
|
+
logger.debug(`[Zync] queueToSync:added ${action} ${item.id} ${stateKey} ${localId}`);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
return {
|
|
777
|
+
syncState: {
|
|
778
|
+
...state.syncState || {},
|
|
779
|
+
pendingChanges
|
|
780
|
+
}
|
|
781
|
+
};
|
|
782
|
+
});
|
|
783
|
+
syncOnce();
|
|
784
|
+
}
|
|
785
|
+
function setAndSync(partial) {
|
|
786
|
+
if (typeof partial === "function") {
|
|
787
|
+
set((state) => ({ ...partial(state) }));
|
|
788
|
+
} else {
|
|
789
|
+
set(partial);
|
|
790
|
+
}
|
|
791
|
+
syncOnce();
|
|
792
|
+
}
|
|
793
|
+
function enable(enabled) {
|
|
794
|
+
set((state) => ({
|
|
795
|
+
syncState: {
|
|
796
|
+
...state.syncState || {},
|
|
797
|
+
enabled
|
|
798
|
+
}
|
|
799
|
+
}));
|
|
800
|
+
enableSyncTimer(enabled);
|
|
801
|
+
addVisibilityChangeListener(enabled);
|
|
802
|
+
}
|
|
803
|
+
function enableSyncTimer(enabled) {
|
|
804
|
+
clearInterval(syncIntervalId);
|
|
805
|
+
syncIntervalId = void 0;
|
|
806
|
+
if (enabled) {
|
|
807
|
+
syncIntervalId = setInterval(syncOnce, syncInterval);
|
|
808
|
+
syncOnce();
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
function addVisibilityChangeListener(add) {
|
|
812
|
+
if (add) {
|
|
813
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
814
|
+
} else {
|
|
815
|
+
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
function onVisibilityChange() {
|
|
819
|
+
if (document.visibilityState === "visible") {
|
|
820
|
+
logger.debug("[Zync] Sync started now app is in foreground");
|
|
821
|
+
enableSyncTimer(true);
|
|
822
|
+
} else {
|
|
823
|
+
logger.debug("[Zync] Sync paused now app is in background");
|
|
824
|
+
enableSyncTimer(false);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
346
827
|
storeApi.sync = {
|
|
347
828
|
enable,
|
|
348
829
|
startFirstLoad
|
|
@@ -363,220 +844,11 @@ function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}
|
|
|
363
844
|
};
|
|
364
845
|
return (0, import_middleware.persist)(creator, wrappedPersistOptions);
|
|
365
846
|
}
|
|
366
|
-
function createStoreWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}) {
|
|
367
|
-
return (0, import_zustand.create)(persistWithSync(stateCreator, persistOptions, syncApi, syncOptions));
|
|
368
|
-
}
|
|
369
|
-
function orderFor(a) {
|
|
370
|
-
switch (a) {
|
|
371
|
-
case "createOrUpdate" /* CreateOrUpdate */:
|
|
372
|
-
return 1;
|
|
373
|
-
case "remove" /* Remove */:
|
|
374
|
-
return 2;
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
async function pull(set, get, stateKey, api, logger) {
|
|
378
|
-
const lastPulled = get().syncState.lastPulled || {};
|
|
379
|
-
const lastPulledAt = new Date(lastPulled[stateKey] || /* @__PURE__ */ new Date(0));
|
|
380
|
-
logger.debug(`[persistWithSync] pull:start stateKey=${stateKey} since=${lastPulledAt.toISOString()}`);
|
|
381
|
-
const serverData = await api.list(lastPulledAt);
|
|
382
|
-
if (!serverData?.length) return;
|
|
383
|
-
let newest = lastPulledAt;
|
|
384
|
-
set((state) => {
|
|
385
|
-
const pendingChanges = state.syncState.pendingChanges || [];
|
|
386
|
-
const localItems = state[stateKey] || [];
|
|
387
|
-
let nextItems = [...localItems];
|
|
388
|
-
const localById = new Map(localItems.filter((l) => l.id).map((l) => [l.id, l]));
|
|
389
|
-
const pendingRemovalIds = /* @__PURE__ */ new Set();
|
|
390
|
-
for (const change of pendingChanges) {
|
|
391
|
-
if (change.stateKey === stateKey && change.action === "remove" /* Remove */) {
|
|
392
|
-
const item = localItems.find((i) => i._localId === change.localId);
|
|
393
|
-
if (item && item.id) pendingRemovalIds.add(item.id);
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
for (const remote of serverData) {
|
|
397
|
-
const remoteUpdated = new Date(remote.updated_at);
|
|
398
|
-
if (remoteUpdated > newest) newest = remoteUpdated;
|
|
399
|
-
const localItem = localById.get(remote.id);
|
|
400
|
-
if (pendingRemovalIds.has(remote.id)) {
|
|
401
|
-
logger.debug(`[persistWithSync] pull:skip-pending-remove stateKey=${stateKey} id=${remote.id}`);
|
|
402
|
-
continue;
|
|
403
|
-
}
|
|
404
|
-
if (remote.deleted) {
|
|
405
|
-
if (localItem) {
|
|
406
|
-
nextItems = nextItems.filter((i) => i.id !== remote.id);
|
|
407
|
-
logger.debug(`[persistWithSync] pull:remove stateKey=${stateKey} id=${remote.id}`);
|
|
408
|
-
}
|
|
409
|
-
continue;
|
|
410
|
-
}
|
|
411
|
-
const pending = localItem && pendingChanges.some((p) => p.stateKey === stateKey && p.localId === localItem._localId);
|
|
412
|
-
if (localItem && !pending) {
|
|
413
|
-
const merged = {
|
|
414
|
-
...localItem,
|
|
415
|
-
...remote,
|
|
416
|
-
_localId: localItem._localId
|
|
417
|
-
};
|
|
418
|
-
nextItems = nextItems.map((i) => i._localId === localItem._localId ? merged : i);
|
|
419
|
-
logger.debug(`[persistWithSync] pull:merge stateKey=${stateKey} id=${remote.id}`);
|
|
420
|
-
} else if (!localItem) {
|
|
421
|
-
nextItems = [...nextItems, { ...remote, _localId: nextLocalId() }];
|
|
422
|
-
logger.debug(`[persistWithSync] pull:add stateKey=${stateKey} id=${remote.id}`);
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
return {
|
|
426
|
-
[stateKey]: nextItems,
|
|
427
|
-
syncState: {
|
|
428
|
-
...state.syncState || {},
|
|
429
|
-
lastPulled: {
|
|
430
|
-
...state.syncState.lastPulled || {},
|
|
431
|
-
[stateKey]: newest.toISOString()
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
};
|
|
435
|
-
});
|
|
436
|
-
}
|
|
437
|
-
async function pushOne(set, get, change, api, logger, queueToSync, missingStrategy, onMissingRemoteRecordDuringUpdate, onAfterRemoteAdd) {
|
|
438
|
-
logger.debug(`[persistWithSync] push:attempt action=${change.action} stateKey=${change.stateKey} localId=${change.localId}`);
|
|
439
|
-
const { action, stateKey, localId, id, version } = change;
|
|
440
|
-
switch (action) {
|
|
441
|
-
case "remove" /* Remove */:
|
|
442
|
-
await api.remove(id);
|
|
443
|
-
logger.debug(`[persistWithSync] push:remove:success ${stateKey} ${localId} ${id}`);
|
|
444
|
-
removeFromPendingChanges(set, localId, stateKey);
|
|
445
|
-
break;
|
|
446
|
-
case "createOrUpdate" /* CreateOrUpdate */:
|
|
447
|
-
const state = get();
|
|
448
|
-
const items = state[stateKey] || [];
|
|
449
|
-
const item = items.find((i) => i._localId === localId);
|
|
450
|
-
if (!item) {
|
|
451
|
-
logger.warn(`[persistWithSync] push:${action}:no-local-item`, {
|
|
452
|
-
stateKey,
|
|
453
|
-
localId
|
|
454
|
-
});
|
|
455
|
-
removeFromPendingChanges(set, localId, stateKey);
|
|
456
|
-
return;
|
|
457
|
-
}
|
|
458
|
-
let omittedItem = omitSyncFields(item);
|
|
459
|
-
if (item.id) {
|
|
460
|
-
const changed = await api.update(item.id, omittedItem);
|
|
461
|
-
if (changed) {
|
|
462
|
-
logger.debug("[persistWithSync] push:update:success", {
|
|
463
|
-
stateKey,
|
|
464
|
-
localId,
|
|
465
|
-
id: item.id
|
|
466
|
-
});
|
|
467
|
-
if (samePendingVersion(get, stateKey, localId, version)) {
|
|
468
|
-
removeFromPendingChanges(set, localId, stateKey);
|
|
469
|
-
}
|
|
470
|
-
return;
|
|
471
|
-
} else {
|
|
472
|
-
logger.warn("[persistWithSync] push:update:missingRemote", {
|
|
473
|
-
stateKey,
|
|
474
|
-
localId,
|
|
475
|
-
id: item.id
|
|
476
|
-
});
|
|
477
|
-
switch (missingStrategy) {
|
|
478
|
-
case "deleteLocalRecord":
|
|
479
|
-
set((s) => ({
|
|
480
|
-
[stateKey]: (s[stateKey] || []).filter((i) => i._localId !== localId)
|
|
481
|
-
}));
|
|
482
|
-
break;
|
|
483
|
-
case "insertNewRemoteRecord": {
|
|
484
|
-
omittedItem._localId = nextLocalId();
|
|
485
|
-
omittedItem.updated_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
486
|
-
set((s) => ({
|
|
487
|
-
[stateKey]: (s[stateKey] || []).map((i) => i._localId === localId ? omittedItem : i)
|
|
488
|
-
}));
|
|
489
|
-
queueToSync("createOrUpdate" /* CreateOrUpdate */, stateKey, omittedItem._localId);
|
|
490
|
-
break;
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
removeFromPendingChanges(set, localId, stateKey);
|
|
494
|
-
onMissingRemoteRecordDuringUpdate?.(missingStrategy, omittedItem, omittedItem._localId);
|
|
495
|
-
}
|
|
496
|
-
return;
|
|
497
|
-
}
|
|
498
|
-
const result = await api.add(omittedItem);
|
|
499
|
-
if (result) {
|
|
500
|
-
logger.debug("[persistWithSync] push:create:success", {
|
|
501
|
-
stateKey,
|
|
502
|
-
localId,
|
|
503
|
-
id: result.id
|
|
504
|
-
});
|
|
505
|
-
set((s) => ({
|
|
506
|
-
[stateKey]: (s[stateKey] || []).map((i) => i._localId === localId ? { ...i, ...result } : i)
|
|
507
|
-
}));
|
|
508
|
-
if (samePendingVersion(get, stateKey, localId, version)) {
|
|
509
|
-
removeFromPendingChanges(set, localId, stateKey);
|
|
510
|
-
}
|
|
511
|
-
onAfterRemoteAdd?.(set, get, queueToSync, stateKey, {
|
|
512
|
-
...item,
|
|
513
|
-
...result
|
|
514
|
-
});
|
|
515
|
-
} else {
|
|
516
|
-
logger.warn("[persistWithSync] push:create:no-result", {
|
|
517
|
-
stateKey,
|
|
518
|
-
localId
|
|
519
|
-
});
|
|
520
|
-
if (samePendingVersion(get, stateKey, localId, version)) {
|
|
521
|
-
removeFromPendingChanges(set, localId, stateKey);
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
break;
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
function samePendingVersion(get, stateKey, localId, version) {
|
|
528
|
-
const q = get().syncState.pendingChanges || [];
|
|
529
|
-
const curChange = q.find((p) => p.localId === localId && p.stateKey === stateKey);
|
|
530
|
-
return curChange?.version === version;
|
|
531
|
-
}
|
|
532
|
-
function removeFromPendingChanges(set, localId, stateKey) {
|
|
533
|
-
set((s) => {
|
|
534
|
-
const queue = (s.syncState.pendingChanges || []).filter((p) => !(p.localId === localId && p.stateKey === stateKey));
|
|
535
|
-
return {
|
|
536
|
-
syncState: {
|
|
537
|
-
...s.syncState || {},
|
|
538
|
-
pendingChanges: queue
|
|
539
|
-
}
|
|
540
|
-
};
|
|
541
|
-
});
|
|
542
|
-
}
|
|
543
|
-
function omitSyncFields(item) {
|
|
544
|
-
const result = { ...item };
|
|
545
|
-
for (const k of SYNC_FIELDS) delete result[k];
|
|
546
|
-
return result;
|
|
547
|
-
}
|
|
548
|
-
function nextLocalId() {
|
|
549
|
-
return crypto.randomUUID();
|
|
550
|
-
}
|
|
551
|
-
function newLogger(logger, min) {
|
|
552
|
-
const order = {
|
|
553
|
-
debug: 10,
|
|
554
|
-
info: 20,
|
|
555
|
-
warn: 30,
|
|
556
|
-
error: 40,
|
|
557
|
-
none: 100
|
|
558
|
-
};
|
|
559
|
-
const threshold = order[min];
|
|
560
|
-
const enabled = (lvl) => order[lvl] >= threshold;
|
|
561
|
-
return {
|
|
562
|
-
debug: (...a) => enabled("debug") && logger.debug?.(...a),
|
|
563
|
-
info: (...a) => enabled("info") && logger.info?.(...a),
|
|
564
|
-
warn: (...a) => enabled("warn") && logger.warn?.(...a),
|
|
565
|
-
error: (...a) => enabled("error") && logger.error?.(...a)
|
|
566
|
-
};
|
|
567
|
-
}
|
|
568
|
-
function findApi(stateKey, syncApi) {
|
|
569
|
-
const api = syncApi[stateKey];
|
|
570
|
-
if (!api || !api.add || !api.update || !api.remove || !api.list || !api.firstLoad) {
|
|
571
|
-
throw new Error(`Missing API function(s) for state key: ${stateKey}.`);
|
|
572
|
-
}
|
|
573
|
-
return api;
|
|
574
|
-
}
|
|
575
847
|
// Annotate the CommonJS export names for ESM import in node:
|
|
576
848
|
0 && (module.exports = {
|
|
577
849
|
SyncAction,
|
|
578
850
|
createIndexedDBStorage,
|
|
579
|
-
|
|
851
|
+
createWithSync,
|
|
580
852
|
nextLocalId,
|
|
581
853
|
persistWithSync
|
|
582
854
|
});
|