cry-synced-db-client 0.1.190 → 0.1.193
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/CHANGELOG.md +96 -0
- package/dist/index.js +607 -411
- package/dist/src/db/SyncedDb.d.ts +20 -68
- package/dist/src/db/sync/SyncEngine.d.ts +25 -0
- package/dist/src/types/I_RestInterface.d.ts +7 -13
- package/dist/src/types/I_SyncedDb.d.ts +61 -1
- package/dist/src/utils/computeDiff.d.ts +0 -100
- package/dist/src/utils/normalizeUndefined.d.ts +1 -1
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -288,80 +288,107 @@ function applyQueryOpts(items, opts) {
|
|
|
288
288
|
return result;
|
|
289
289
|
}
|
|
290
290
|
|
|
291
|
-
//
|
|
291
|
+
// node_modules/cry-db/dist/utils.mjs
|
|
292
292
|
function isObjectIdLike(v) {
|
|
293
293
|
return !!(v && typeof v === "object" && (v._bsontype === "ObjectId" || v._bsontype === "ObjectID") && typeof v.toString === "function");
|
|
294
294
|
}
|
|
295
295
|
function isPlainObject(v) {
|
|
296
|
-
if (v === null || typeof v !== "object")
|
|
297
|
-
|
|
298
|
-
if (v
|
|
299
|
-
|
|
296
|
+
if (v === null || typeof v !== "object")
|
|
297
|
+
return false;
|
|
298
|
+
if (Array.isArray(v))
|
|
299
|
+
return false;
|
|
300
|
+
if (v instanceof Date)
|
|
301
|
+
return false;
|
|
302
|
+
if (isObjectIdLike(v))
|
|
303
|
+
return false;
|
|
300
304
|
const proto = Object.getPrototypeOf(v);
|
|
301
305
|
return proto === Object.prototype || proto === null;
|
|
302
306
|
}
|
|
303
307
|
function deepEquals(a, b) {
|
|
304
|
-
if (a === b)
|
|
305
|
-
|
|
306
|
-
if (a ===
|
|
308
|
+
if (a === b)
|
|
309
|
+
return true;
|
|
310
|
+
if (a === null || b === null)
|
|
311
|
+
return false;
|
|
312
|
+
if (a === void 0 || b === void 0)
|
|
313
|
+
return false;
|
|
307
314
|
if (a instanceof Date && b instanceof Date) {
|
|
308
315
|
return a.getTime() === b.getTime();
|
|
309
316
|
}
|
|
310
317
|
if (isObjectIdLike(a) && isObjectIdLike(b)) {
|
|
311
318
|
return String(a) === String(b);
|
|
312
319
|
}
|
|
313
|
-
if (typeof a !== typeof b)
|
|
314
|
-
|
|
315
|
-
if (
|
|
320
|
+
if (typeof a !== typeof b)
|
|
321
|
+
return false;
|
|
322
|
+
if (typeof a !== "object")
|
|
323
|
+
return false;
|
|
324
|
+
if (Array.isArray(a) !== Array.isArray(b))
|
|
325
|
+
return false;
|
|
316
326
|
if (Array.isArray(a)) {
|
|
317
|
-
if (a.length !== b.length)
|
|
327
|
+
if (a.length !== b.length)
|
|
328
|
+
return false;
|
|
318
329
|
for (let i = 0; i < a.length; i++) {
|
|
319
|
-
if (!deepEquals(a[i], b[i]))
|
|
330
|
+
if (!deepEquals(a[i], b[i]))
|
|
331
|
+
return false;
|
|
320
332
|
}
|
|
321
333
|
return true;
|
|
322
334
|
}
|
|
323
335
|
const ak = Object.keys(a);
|
|
324
336
|
const bk = Object.keys(b);
|
|
325
|
-
if (ak.length !== bk.length)
|
|
337
|
+
if (ak.length !== bk.length)
|
|
338
|
+
return false;
|
|
326
339
|
for (const k of ak) {
|
|
327
|
-
if (!Object.prototype.hasOwnProperty.call(b, k))
|
|
328
|
-
|
|
340
|
+
if (!Object.prototype.hasOwnProperty.call(b, k))
|
|
341
|
+
return false;
|
|
342
|
+
if (!deepEquals(a[k], b[k]))
|
|
343
|
+
return false;
|
|
329
344
|
}
|
|
330
345
|
return true;
|
|
331
346
|
}
|
|
332
347
|
function allElementsHaveId(arr) {
|
|
333
|
-
if (arr.length === 0)
|
|
348
|
+
if (arr.length === 0)
|
|
349
|
+
return false;
|
|
334
350
|
for (const e of arr) {
|
|
335
|
-
if (!e || typeof e !== "object")
|
|
336
|
-
|
|
351
|
+
if (!e || typeof e !== "object")
|
|
352
|
+
return false;
|
|
353
|
+
if (e._id == null)
|
|
354
|
+
return false;
|
|
337
355
|
}
|
|
338
356
|
return true;
|
|
339
357
|
}
|
|
340
358
|
function sameIdSequence(a, b) {
|
|
341
|
-
if (a.length !== b.length)
|
|
359
|
+
if (a.length !== b.length)
|
|
360
|
+
return false;
|
|
342
361
|
for (let i = 0; i < a.length; i++) {
|
|
343
|
-
if (String(a[i]._id) !== String(b[i]._id))
|
|
362
|
+
if (String(a[i]._id) !== String(b[i]._id))
|
|
363
|
+
return false;
|
|
344
364
|
}
|
|
345
365
|
return true;
|
|
346
366
|
}
|
|
347
367
|
function containsIdArrayDescendant(value) {
|
|
348
|
-
if (value === null || typeof value !== "object")
|
|
349
|
-
|
|
368
|
+
if (value === null || typeof value !== "object")
|
|
369
|
+
return false;
|
|
370
|
+
if (value instanceof Date || isObjectIdLike(value))
|
|
371
|
+
return false;
|
|
350
372
|
if (Array.isArray(value)) {
|
|
351
|
-
if (value.length > 0 && allElementsHaveId(value))
|
|
373
|
+
if (value.length > 0 && allElementsHaveId(value))
|
|
374
|
+
return true;
|
|
352
375
|
for (const v of value) {
|
|
353
|
-
if (containsIdArrayDescendant(v))
|
|
376
|
+
if (containsIdArrayDescendant(v))
|
|
377
|
+
return true;
|
|
354
378
|
}
|
|
355
379
|
return false;
|
|
356
380
|
}
|
|
357
|
-
if (!isPlainObject(value))
|
|
381
|
+
if (!isPlainObject(value))
|
|
382
|
+
return false;
|
|
358
383
|
for (const key of Object.keys(value)) {
|
|
359
|
-
if (containsIdArrayDescendant(value[key]))
|
|
384
|
+
if (containsIdArrayDescendant(value[key]))
|
|
385
|
+
return true;
|
|
360
386
|
}
|
|
361
387
|
return false;
|
|
362
388
|
}
|
|
363
389
|
function computeArrayDiff(existingArr, updateArr, basePath, diff) {
|
|
364
|
-
if (existingArr.length === 0 && updateArr.length === 0)
|
|
390
|
+
if (existingArr.length === 0 && updateArr.length === 0)
|
|
391
|
+
return;
|
|
365
392
|
if (!allElementsHaveId(updateArr) || existingArr.length > 0 && !allElementsHaveId(existingArr)) {
|
|
366
393
|
if (!deepEquals(existingArr, updateArr)) {
|
|
367
394
|
diff[basePath] = updateArr;
|
|
@@ -369,9 +396,11 @@ function computeArrayDiff(existingArr, updateArr, basePath, diff) {
|
|
|
369
396
|
return;
|
|
370
397
|
}
|
|
371
398
|
const existingIds = /* @__PURE__ */ new Set();
|
|
372
|
-
for (const e of existingArr)
|
|
399
|
+
for (const e of existingArr)
|
|
400
|
+
existingIds.add(String(e._id));
|
|
373
401
|
const updateIds = /* @__PURE__ */ new Set();
|
|
374
|
-
for (const u of updateArr)
|
|
402
|
+
for (const u of updateArr)
|
|
403
|
+
updateIds.add(String(u._id));
|
|
375
404
|
let sameSet = existingIds.size === updateIds.size;
|
|
376
405
|
if (sameSet) {
|
|
377
406
|
for (const id of existingIds) {
|
|
@@ -412,7 +441,8 @@ function computeArrayDiff(existingArr, updateArr, basePath, diff) {
|
|
|
412
441
|
}
|
|
413
442
|
if (existingArr.length > 0) {
|
|
414
443
|
const existingById = /* @__PURE__ */ new Map();
|
|
415
|
-
for (const e of existingArr)
|
|
444
|
+
for (const e of existingArr)
|
|
445
|
+
existingById.set(String(e._id), e);
|
|
416
446
|
for (const updateEl of updateArr) {
|
|
417
447
|
const id = String(updateEl._id);
|
|
418
448
|
if (existingIds.has(id)) {
|
|
@@ -422,19 +452,15 @@ function computeArrayDiff(existingArr, updateArr, basePath, diff) {
|
|
|
422
452
|
diff[elementPath] = updateEl;
|
|
423
453
|
}
|
|
424
454
|
} else {
|
|
425
|
-
computeDiffInto(
|
|
426
|
-
existingById.get(id),
|
|
427
|
-
updateEl,
|
|
428
|
-
elementPath,
|
|
429
|
-
diff
|
|
430
|
-
);
|
|
455
|
+
computeDiffInto(existingById.get(id), updateEl, elementPath, diff);
|
|
431
456
|
}
|
|
432
457
|
}
|
|
433
458
|
}
|
|
434
459
|
}
|
|
435
460
|
}
|
|
436
461
|
function computeDiffInto(existing, update, basePath, diff) {
|
|
437
|
-
if (deepEquals(existing, update))
|
|
462
|
+
if (deepEquals(existing, update))
|
|
463
|
+
return;
|
|
438
464
|
if (update === null || update === void 0 || typeof update !== "object" || update instanceof Date || isObjectIdLike(update)) {
|
|
439
465
|
diff[basePath] = update;
|
|
440
466
|
return;
|
|
@@ -465,32 +491,26 @@ function computeDiffInto(existing, update, basePath, diff) {
|
|
|
465
491
|
}
|
|
466
492
|
}
|
|
467
493
|
var SERVER_MANAGED_METADATA_KEYS = /* @__PURE__ */ new Set(["_ts", "_rev", "_csq"]);
|
|
468
|
-
function
|
|
494
|
+
function computeObjDiff(existing, update) {
|
|
469
495
|
const diff = {};
|
|
470
|
-
if (!update || typeof update !== "object")
|
|
496
|
+
if (!update || typeof update !== "object")
|
|
497
|
+
return diff;
|
|
471
498
|
if (existing === null || existing === void 0) {
|
|
472
499
|
const cleaned = {};
|
|
473
500
|
for (const k of Object.keys(update)) {
|
|
474
|
-
if (SERVER_MANAGED_METADATA_KEYS.has(k))
|
|
501
|
+
if (SERVER_MANAGED_METADATA_KEYS.has(k))
|
|
502
|
+
continue;
|
|
475
503
|
cleaned[k] = update[k];
|
|
476
504
|
}
|
|
477
505
|
return cleaned;
|
|
478
506
|
}
|
|
479
507
|
for (const key of Object.keys(update)) {
|
|
480
|
-
if (SERVER_MANAGED_METADATA_KEYS.has(key))
|
|
508
|
+
if (SERVER_MANAGED_METADATA_KEYS.has(key))
|
|
509
|
+
continue;
|
|
481
510
|
computeDiffInto(existing[key], update[key], key, diff);
|
|
482
511
|
}
|
|
483
512
|
return diff;
|
|
484
513
|
}
|
|
485
|
-
function isDescendantOrEqual(path, candidate) {
|
|
486
|
-
if (path === candidate) return true;
|
|
487
|
-
if (!path.startsWith(candidate)) return false;
|
|
488
|
-
const next = path[candidate.length];
|
|
489
|
-
return next === "." || next === "[";
|
|
490
|
-
}
|
|
491
|
-
function pathsOverlap(a, b) {
|
|
492
|
-
return isDescendantOrEqual(a, b) || isDescendantOrEqual(b, a);
|
|
493
|
-
}
|
|
494
514
|
function tokenizePath(path) {
|
|
495
515
|
const out = [];
|
|
496
516
|
let buf = "";
|
|
@@ -517,60 +537,27 @@ function tokenizePath(path) {
|
|
|
517
537
|
buf += ch;
|
|
518
538
|
}
|
|
519
539
|
}
|
|
520
|
-
if (buf)
|
|
540
|
+
if (buf)
|
|
541
|
+
out.push(buf);
|
|
521
542
|
return out;
|
|
522
543
|
}
|
|
523
|
-
function setByPath(target2, path, value, opts) {
|
|
524
|
-
if (target2 === null || target2 === void 0) return false;
|
|
525
|
-
const autoCreate = (opts == null ? void 0 : opts.autoCreate) !== false;
|
|
526
|
-
const parts = tokenizePath(path);
|
|
527
|
-
const pathHasArrayShape = parts.some(
|
|
528
|
-
(p) => p.startsWith("[") && p.endsWith("]") || /^\d+$/.test(p)
|
|
529
|
-
);
|
|
530
|
-
const canAutoCreate = autoCreate && !pathHasArrayShape;
|
|
531
|
-
let current = target2;
|
|
532
|
-
const created = [];
|
|
533
|
-
for (let i = 0; i < parts.length - 1; i++) {
|
|
534
|
-
const part = parts[i];
|
|
535
|
-
let next = navigateSegment(current, part);
|
|
536
|
-
const isMissing = next === void 0 || next === null;
|
|
537
|
-
if (isMissing && canAutoCreate) {
|
|
538
|
-
if (isPlainObjectContainer(current)) {
|
|
539
|
-
current[part] = {};
|
|
540
|
-
created.push({ container: current, key: part });
|
|
541
|
-
next = current[part];
|
|
542
|
-
} else {
|
|
543
|
-
return false;
|
|
544
|
-
}
|
|
545
|
-
} else if (isMissing) {
|
|
546
|
-
return false;
|
|
547
|
-
} else if (autoCreate && !pathHasArrayShape && !isPlainObjectContainer(next) && !Array.isArray(next)) {
|
|
548
|
-
return false;
|
|
549
|
-
}
|
|
550
|
-
current = next;
|
|
551
|
-
}
|
|
552
|
-
const last = parts[parts.length - 1];
|
|
553
|
-
const ok = setSegment(current, last, value);
|
|
554
|
-
if (!ok && created.length > 0) {
|
|
555
|
-
for (let i = created.length - 1; i >= 0; i--) {
|
|
556
|
-
const { container, key } = created[i];
|
|
557
|
-
delete container[key];
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
return ok;
|
|
561
|
-
}
|
|
562
544
|
function isPlainObjectContainer(value) {
|
|
563
|
-
if (value === null || value === void 0)
|
|
564
|
-
|
|
565
|
-
if (
|
|
545
|
+
if (value === null || value === void 0)
|
|
546
|
+
return false;
|
|
547
|
+
if (typeof value !== "object")
|
|
548
|
+
return false;
|
|
549
|
+
if (Array.isArray(value))
|
|
550
|
+
return false;
|
|
566
551
|
const proto = Object.getPrototypeOf(value);
|
|
567
552
|
return proto === Object.prototype || proto === null;
|
|
568
553
|
}
|
|
569
554
|
function navigateSegment(current, part) {
|
|
570
|
-
if (current === null || current === void 0)
|
|
555
|
+
if (current === null || current === void 0)
|
|
556
|
+
return void 0;
|
|
571
557
|
if (part.startsWith("[") && part.endsWith("]")) {
|
|
572
558
|
const idStr = part.slice(1, -1);
|
|
573
|
-
if (!Array.isArray(current))
|
|
559
|
+
if (!Array.isArray(current))
|
|
560
|
+
return void 0;
|
|
574
561
|
return current.find((item) => item && String(item._id) === idStr);
|
|
575
562
|
}
|
|
576
563
|
if (/^\d+$/.test(part) && Array.isArray(current)) {
|
|
@@ -582,17 +569,22 @@ function navigateSegment(current, part) {
|
|
|
582
569
|
return void 0;
|
|
583
570
|
}
|
|
584
571
|
function setSegment(current, part, value) {
|
|
585
|
-
if (current === null || current === void 0)
|
|
572
|
+
if (current === null || current === void 0)
|
|
573
|
+
return false;
|
|
586
574
|
if (part.startsWith("[") && part.endsWith("]")) {
|
|
587
575
|
const idStr = part.slice(1, -1);
|
|
588
|
-
if (!Array.isArray(current))
|
|
576
|
+
if (!Array.isArray(current))
|
|
577
|
+
return false;
|
|
589
578
|
const idx = current.findIndex((item) => item && String(item._id) === idStr);
|
|
590
579
|
if (Array.isArray(value) && value.length === 1 && value[0] && typeof value[0] === "object" && String(value[0]._id) === idStr) {
|
|
591
|
-
if (idx >= 0)
|
|
592
|
-
|
|
580
|
+
if (idx >= 0)
|
|
581
|
+
current[idx] = value[0];
|
|
582
|
+
else
|
|
583
|
+
current.push(value[0]);
|
|
593
584
|
return true;
|
|
594
585
|
}
|
|
595
|
-
if (idx < 0)
|
|
586
|
+
if (idx < 0)
|
|
587
|
+
return false;
|
|
596
588
|
current[idx] = value;
|
|
597
589
|
return true;
|
|
598
590
|
}
|
|
@@ -606,127 +598,243 @@ function setSegment(current, part, value) {
|
|
|
606
598
|
}
|
|
607
599
|
return false;
|
|
608
600
|
}
|
|
609
|
-
function
|
|
610
|
-
if (target2 === null || target2 === void 0)
|
|
601
|
+
function setByPath(target2, path, value, opts) {
|
|
602
|
+
if (target2 === null || target2 === void 0)
|
|
603
|
+
return false;
|
|
604
|
+
const autoCreate = (opts === null || opts === void 0 ? void 0 : opts.autoCreate) !== false;
|
|
611
605
|
const parts = tokenizePath(path);
|
|
612
|
-
|
|
606
|
+
const pathHasArrayShape = parts.some((p) => p.startsWith("[") && p.endsWith("]") || /^\d+$/.test(p));
|
|
607
|
+
const canAutoCreate = autoCreate && !pathHasArrayShape;
|
|
613
608
|
let current = target2;
|
|
609
|
+
const created = [];
|
|
614
610
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
615
|
-
const
|
|
616
|
-
|
|
611
|
+
const part = parts[i];
|
|
612
|
+
let next = navigateSegment(current, part);
|
|
613
|
+
const isMissing = next === void 0 || next === null;
|
|
614
|
+
if (isMissing && canAutoCreate) {
|
|
615
|
+
if (isPlainObjectContainer(current)) {
|
|
616
|
+
current[part] = {};
|
|
617
|
+
created.push({ container: current, key: part });
|
|
618
|
+
next = current[part];
|
|
619
|
+
} else {
|
|
620
|
+
return false;
|
|
621
|
+
}
|
|
622
|
+
} else if (isMissing) {
|
|
623
|
+
return false;
|
|
624
|
+
} else if (autoCreate && !pathHasArrayShape && !isPlainObjectContainer(next) && !Array.isArray(next)) {
|
|
625
|
+
return false;
|
|
626
|
+
}
|
|
617
627
|
current = next;
|
|
618
628
|
}
|
|
619
629
|
const last = parts[parts.length - 1];
|
|
620
|
-
|
|
630
|
+
const ok = setSegment(current, last, value);
|
|
631
|
+
if (!ok && created.length > 0) {
|
|
632
|
+
for (let i = created.length - 1; i >= 0; i--) {
|
|
633
|
+
const { container, key } = created[i];
|
|
634
|
+
delete container[key];
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
return ok;
|
|
621
638
|
}
|
|
622
639
|
function deleteSegment(current, part) {
|
|
623
|
-
if (current === null || current === void 0)
|
|
640
|
+
if (current === null || current === void 0)
|
|
641
|
+
return false;
|
|
624
642
|
if (part.startsWith("[") && part.endsWith("]")) {
|
|
625
643
|
const idStr = part.slice(1, -1);
|
|
626
|
-
if (!Array.isArray(current))
|
|
644
|
+
if (!Array.isArray(current))
|
|
645
|
+
return false;
|
|
627
646
|
const idx = current.findIndex((item) => item && String(item._id) === idStr);
|
|
628
|
-
if (idx < 0)
|
|
647
|
+
if (idx < 0)
|
|
648
|
+
return false;
|
|
629
649
|
current.splice(idx, 1);
|
|
630
650
|
return true;
|
|
631
651
|
}
|
|
632
652
|
if (/^\d+$/.test(part) && Array.isArray(current)) {
|
|
633
653
|
const idx = Number(part);
|
|
634
|
-
if (idx < 0 || idx >= current.length)
|
|
654
|
+
if (idx < 0 || idx >= current.length)
|
|
655
|
+
return false;
|
|
635
656
|
current.splice(idx, 1);
|
|
636
657
|
return true;
|
|
637
658
|
}
|
|
638
659
|
if (typeof current === "object") {
|
|
639
|
-
if (!Object.prototype.hasOwnProperty.call(current, part))
|
|
660
|
+
if (!Object.prototype.hasOwnProperty.call(current, part))
|
|
661
|
+
return false;
|
|
640
662
|
delete current[part];
|
|
641
663
|
return true;
|
|
642
664
|
}
|
|
643
665
|
return false;
|
|
644
666
|
}
|
|
645
|
-
function
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
for (
|
|
653
|
-
|
|
654
|
-
if (
|
|
667
|
+
function deleteByPath(target2, path) {
|
|
668
|
+
if (target2 === null || target2 === void 0)
|
|
669
|
+
return false;
|
|
670
|
+
const parts = tokenizePath(path);
|
|
671
|
+
if (parts.length === 0)
|
|
672
|
+
return false;
|
|
673
|
+
let current = target2;
|
|
674
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
675
|
+
const next = navigateSegment(current, parts[i]);
|
|
676
|
+
if (next === void 0 || next === null)
|
|
677
|
+
return false;
|
|
678
|
+
current = next;
|
|
655
679
|
}
|
|
656
|
-
|
|
680
|
+
const last = parts[parts.length - 1];
|
|
681
|
+
return deleteSegment(current, last);
|
|
657
682
|
}
|
|
658
|
-
function
|
|
659
|
-
if (
|
|
660
|
-
|
|
661
|
-
if (
|
|
662
|
-
return
|
|
683
|
+
function safeDeepClone(value) {
|
|
684
|
+
if (value === null || value === void 0)
|
|
685
|
+
return value;
|
|
686
|
+
if (typeof value !== "object")
|
|
687
|
+
return value;
|
|
688
|
+
if (value instanceof Date)
|
|
689
|
+
return new Date(value.getTime());
|
|
690
|
+
if (isObjectIdLike(value))
|
|
691
|
+
return value;
|
|
692
|
+
if (Array.isArray(value)) {
|
|
693
|
+
const out2 = new Array(value.length);
|
|
694
|
+
for (let i = 0; i < value.length; i++)
|
|
695
|
+
out2[i] = safeDeepClone(value[i]);
|
|
696
|
+
return out2;
|
|
663
697
|
}
|
|
664
|
-
|
|
665
|
-
|
|
698
|
+
const proto = Object.getPrototypeOf(value);
|
|
699
|
+
if (proto !== Object.prototype && proto !== null)
|
|
700
|
+
return value;
|
|
701
|
+
const out = {};
|
|
702
|
+
for (const key of Object.keys(value)) {
|
|
703
|
+
out[key] = safeDeepClone(value[key]);
|
|
666
704
|
}
|
|
667
|
-
return
|
|
705
|
+
return out;
|
|
668
706
|
}
|
|
669
|
-
function
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
if (existingIsTerminal && existingValue === void 0) {
|
|
676
|
-
return;
|
|
677
|
-
}
|
|
678
|
-
let mutationTarget = existingValue;
|
|
679
|
-
if (existingIsTerminal && Array.isArray(existingValue) && existingValue.length === 1) {
|
|
680
|
-
mutationTarget = existingValue[0];
|
|
681
|
-
}
|
|
682
|
-
if (!existingIsTerminal && (existingValue === null || existingValue === void 0)) {
|
|
683
|
-
accumulated[existingKey] = {};
|
|
684
|
-
mutationTarget = accumulated[existingKey];
|
|
685
|
-
}
|
|
686
|
-
if (!existingIsTerminal && newPath[existingKey.length] === "[" && canExpandArrayToBrackets(existingValue)) {
|
|
687
|
-
delete accumulated[existingKey];
|
|
688
|
-
for (const el of existingValue) {
|
|
689
|
-
accumulated[`${existingKey}[${String(el._id)}]`] = [el];
|
|
690
|
-
}
|
|
691
|
-
mergeDirtyPath(accumulated, newPath, newValue);
|
|
692
|
-
return;
|
|
693
|
-
}
|
|
694
|
-
const sepChar = newPath[existingKey.length];
|
|
695
|
-
const relativePath = sepChar === "[" ? newPath.substring(existingKey.length) : newPath.substring(existingKey.length + 1);
|
|
696
|
-
const ok = setByPath(mutationTarget, relativePath, newValue);
|
|
697
|
-
if (ok) return;
|
|
698
|
-
delete accumulated[existingKey];
|
|
699
|
-
accumulated[newPath] = newValue;
|
|
707
|
+
function materializeBracketPath(seed, path, value, collection, id) {
|
|
708
|
+
const tokens = tokenizePath(path);
|
|
709
|
+
const firstToken = tokens[0];
|
|
710
|
+
const dropSilently = typeof firstToken === "string" && firstToken.startsWith("_");
|
|
711
|
+
const drop = (reason) => {
|
|
712
|
+
if (dropSilently)
|
|
700
713
|
return;
|
|
714
|
+
console.error(`[cry-db] applyObjDiff: dropping bracket-path diff entry (${reason})`, { collection, _id: String(id), path, value });
|
|
715
|
+
};
|
|
716
|
+
if (tokens.length < 2) {
|
|
717
|
+
drop(`unsupported token count ${tokens.length}`);
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
if (firstToken === void 0 || firstToken.startsWith("[")) {
|
|
721
|
+
drop("first segment is not a plain field");
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
let bracketIdx = -1;
|
|
725
|
+
for (let i = 1; i < tokens.length; i++) {
|
|
726
|
+
if (tokens[i].startsWith("[")) {
|
|
727
|
+
bracketIdx = i;
|
|
728
|
+
break;
|
|
701
729
|
}
|
|
702
730
|
}
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
731
|
+
if (bracketIdx < 0) {
|
|
732
|
+
drop("no bracket segment found");
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
for (let i = bracketIdx + 1; i < tokens.length; i++) {
|
|
736
|
+
if (tokens[i].startsWith("[")) {
|
|
737
|
+
drop("nested bracket path");
|
|
738
|
+
return;
|
|
708
739
|
}
|
|
709
740
|
}
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
741
|
+
if (dropSilently && bracketIdx > 1) {
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
const prefixTokens = tokens.slice(0, bracketIdx);
|
|
745
|
+
const bracketToken = tokens[bracketIdx];
|
|
746
|
+
const suffixTokens = tokens.slice(bracketIdx + 1);
|
|
747
|
+
const bracketId = bracketToken.slice(1, -1);
|
|
748
|
+
if (bracketId.length === 0) {
|
|
749
|
+
drop("empty bracket id");
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
let parent = seed;
|
|
753
|
+
for (let i = 0; i < prefixTokens.length - 1; i++) {
|
|
754
|
+
const seg = prefixTokens[i];
|
|
755
|
+
const cur = parent[seg];
|
|
756
|
+
if (cur === void 0 || cur === null) {
|
|
757
|
+
parent[seg] = {};
|
|
758
|
+
} else if (typeof cur !== "object" || Array.isArray(cur) || cur instanceof Date) {
|
|
759
|
+
drop(`existing intermediate "${prefixTokens.slice(0, i + 1).join(".")}" is not a plain object`);
|
|
760
|
+
return;
|
|
721
761
|
}
|
|
762
|
+
parent = parent[seg];
|
|
763
|
+
}
|
|
764
|
+
const lastPrefixSeg = prefixTokens[prefixTokens.length - 1];
|
|
765
|
+
let arr = parent[lastPrefixSeg];
|
|
766
|
+
if (arr != null && !Array.isArray(arr)) {
|
|
767
|
+
drop(`existing "${prefixTokens.join(".")}" is not an array`);
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
if (arr == null) {
|
|
771
|
+
arr = [];
|
|
772
|
+
parent[lastPrefixSeg] = arr;
|
|
773
|
+
}
|
|
774
|
+
if (suffixTokens.length === 0) {
|
|
775
|
+
let element = value;
|
|
776
|
+
if (Array.isArray(value) && value.length === 1 && value[0] != null && typeof value[0] === "object") {
|
|
777
|
+
element = value[0];
|
|
778
|
+
}
|
|
779
|
+
if (element == null || typeof element !== "object" || Array.isArray(element)) {
|
|
780
|
+
drop("value is not a single element or wire-form wrapper");
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
if (element._id == null) {
|
|
784
|
+
element._id = bracketId;
|
|
785
|
+
}
|
|
786
|
+
const replaceIdx = arr.findIndex((it) => it != null && typeof it === "object" && String(it._id) === bracketId);
|
|
787
|
+
if (replaceIdx >= 0) {
|
|
788
|
+
arr[replaceIdx] = element;
|
|
789
|
+
} else {
|
|
790
|
+
arr.push(element);
|
|
791
|
+
}
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
const buildNestedSubTree = (segs, leaf) => {
|
|
795
|
+
let acc = leaf;
|
|
796
|
+
for (let i = segs.length - 1; i >= 0; i--) {
|
|
797
|
+
acc = { [segs[i]]: acc };
|
|
798
|
+
}
|
|
799
|
+
return acc;
|
|
800
|
+
};
|
|
801
|
+
const existingIdx = arr.findIndex((it) => it != null && typeof it === "object" && String(it._id) === bracketId);
|
|
802
|
+
if (existingIdx >= 0) {
|
|
803
|
+
let cur = arr[existingIdx];
|
|
804
|
+
for (let i = 0; i < suffixTokens.length - 1; i++) {
|
|
805
|
+
const seg = suffixTokens[i];
|
|
806
|
+
const next = cur[seg];
|
|
807
|
+
if (next == null || typeof next !== "object" || Array.isArray(next)) {
|
|
808
|
+
cur[seg] = {};
|
|
809
|
+
}
|
|
810
|
+
cur = cur[seg];
|
|
811
|
+
}
|
|
812
|
+
cur[suffixTokens[suffixTokens.length - 1]] = value;
|
|
813
|
+
} else {
|
|
814
|
+
const subTree = buildNestedSubTree(suffixTokens, value);
|
|
815
|
+
arr.push(__spreadValues({ _id: bracketId }, subTree));
|
|
722
816
|
}
|
|
723
|
-
for (const k of descendants) delete accumulated[k];
|
|
724
|
-
accumulated[newPath] = newValue;
|
|
725
817
|
}
|
|
726
|
-
function
|
|
727
|
-
|
|
728
|
-
|
|
818
|
+
function applyObjDiff(base, diff, fallbackId, collection) {
|
|
819
|
+
const seed = base ? safeDeepClone(base) : { _id: fallbackId };
|
|
820
|
+
if (seed._id == null)
|
|
821
|
+
seed._id = fallbackId;
|
|
822
|
+
for (const path of Object.keys(diff)) {
|
|
823
|
+
const value = diff[path];
|
|
824
|
+
if (value === void 0) {
|
|
825
|
+
deleteByPath(seed, path);
|
|
826
|
+
continue;
|
|
827
|
+
}
|
|
828
|
+
if (!path.includes(".") && !path.includes("[")) {
|
|
829
|
+
seed[path] = value;
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
const ok = setByPath(seed, path, value);
|
|
833
|
+
if (ok)
|
|
834
|
+
continue;
|
|
835
|
+
materializeBracketPath(seed, path, value, collection, fallbackId);
|
|
729
836
|
}
|
|
837
|
+
return seed;
|
|
730
838
|
}
|
|
731
839
|
|
|
732
840
|
// src/utils/normalizeUndefined.ts
|
|
@@ -3303,6 +3411,99 @@ var _SyncEngine = class _SyncEngine {
|
|
|
3303
3411
|
throw err;
|
|
3304
3412
|
}
|
|
3305
3413
|
}
|
|
3414
|
+
/**
|
|
3415
|
+
* Adopt cry-db `mustRefresh` records (≥ 2.5.0) into Dexie + in-mem.
|
|
3416
|
+
*
|
|
3417
|
+
* The server returns the full, sanitized post-write record when it resolved a
|
|
3418
|
+
* placeholder (`SEQ_*`/`__hashed__*`) or a concurrent write advanced `_rev`
|
|
3419
|
+
* past the client's base. Per the cry-db contract we REPLACE the local record
|
|
3420
|
+
* with it, THEN re-apply any still-unsynced local edits to OTHER fields on top
|
|
3421
|
+
* ("apply mustRefresh before the merge so they're reconciled, not clobbered").
|
|
3422
|
+
*
|
|
3423
|
+
* "Other fields" = dirty paths NOT in the just-uploaded snapshot (authored
|
|
3424
|
+
* during the round-trip). Uploaded paths — including a resolved SEQ/hash
|
|
3425
|
+
* placeholder — are owned by the server record and must NOT be re-applied,
|
|
3426
|
+
* else the placeholder would clobber the resolved value. Re-applied paths are
|
|
3427
|
+
* kept dirty (rebased on the server's new `_rev`) so they upload next sync.
|
|
3428
|
+
*
|
|
3429
|
+
* @param uploadedById path→value the client sent per id (the upload snapshot)
|
|
3430
|
+
* @param dirtyBefore dirty entries captured BEFORE the success-clear
|
|
3431
|
+
*/
|
|
3432
|
+
async adoptMustRefresh(collection, mustRefresh, uploadedById, dirtyBefore) {
|
|
3433
|
+
var _a;
|
|
3434
|
+
if ((_a = this.collections.get(collection)) == null ? void 0 : _a.writeOnly) return;
|
|
3435
|
+
const dexieSave = [];
|
|
3436
|
+
const dexieDeleteIds = [];
|
|
3437
|
+
const memUpsert = [];
|
|
3438
|
+
const memDelete = [];
|
|
3439
|
+
const reAddDirty = [];
|
|
3440
|
+
for (const record of mustRefresh) {
|
|
3441
|
+
if (record._deleted || record._archived) {
|
|
3442
|
+
dexieDeleteIds.push(record._id);
|
|
3443
|
+
memDelete.push({ _id: record._id });
|
|
3444
|
+
continue;
|
|
3445
|
+
}
|
|
3446
|
+
const uploaded = uploadedById.get(String(record._id));
|
|
3447
|
+
const dirtyEntry = dirtyBefore.get(String(record._id));
|
|
3448
|
+
const remaining = {};
|
|
3449
|
+
if (dirtyEntry) {
|
|
3450
|
+
for (const path of Object.keys(dirtyEntry.changes)) {
|
|
3451
|
+
if (!uploaded || !(path in uploaded)) {
|
|
3452
|
+
remaining[path] = dirtyEntry.changes[path];
|
|
3453
|
+
}
|
|
3454
|
+
}
|
|
3455
|
+
}
|
|
3456
|
+
const hasRemaining = Object.keys(remaining).length > 0;
|
|
3457
|
+
const merged = hasRemaining ? applyObjDiff(
|
|
3458
|
+
record,
|
|
3459
|
+
remaining,
|
|
3460
|
+
record._id,
|
|
3461
|
+
collection
|
|
3462
|
+
) : record;
|
|
3463
|
+
dexieSave.push(merged);
|
|
3464
|
+
memUpsert.push(merged);
|
|
3465
|
+
if (hasRemaining) {
|
|
3466
|
+
reAddDirty.push({
|
|
3467
|
+
id: record._id,
|
|
3468
|
+
changes: remaining,
|
|
3469
|
+
baseMeta: {
|
|
3470
|
+
_ts: record._ts,
|
|
3471
|
+
_rev: record._rev
|
|
3472
|
+
}
|
|
3473
|
+
});
|
|
3474
|
+
}
|
|
3475
|
+
}
|
|
3476
|
+
if (dexieSave.length > 0) await this.dexieDb.saveMany(collection, dexieSave);
|
|
3477
|
+
if (dexieDeleteIds.length > 0) {
|
|
3478
|
+
await this.dexieDb.deleteMany(collection, dexieDeleteIds);
|
|
3479
|
+
}
|
|
3480
|
+
if (memUpsert.length > 0) {
|
|
3481
|
+
this.deps.writeToInMemBatch(collection, memUpsert, "upsert", { source: "incremental" });
|
|
3482
|
+
}
|
|
3483
|
+
if (memDelete.length > 0) {
|
|
3484
|
+
this.deps.writeToInMemBatch(collection, memDelete, "delete", { source: "incremental" });
|
|
3485
|
+
}
|
|
3486
|
+
if (reAddDirty.length > 0) {
|
|
3487
|
+
await this.dexieDb.addDirtyChangesBatch(collection, reAddDirty);
|
|
3488
|
+
}
|
|
3489
|
+
}
|
|
3490
|
+
/**
|
|
3491
|
+
* Build the per-id upload snapshot (`_id` → sent `update`) for one collection
|
|
3492
|
+
* from the request batches — used by `adoptMustRefresh` to tell apart
|
|
3493
|
+
* server-reconciled paths from local edits made during the round-trip.
|
|
3494
|
+
*/
|
|
3495
|
+
uploadedSnapshotFor(collectionBatches, collection) {
|
|
3496
|
+
const out = /* @__PURE__ */ new Map();
|
|
3497
|
+
for (const batch of collectionBatches) {
|
|
3498
|
+
for (const b of batch) {
|
|
3499
|
+
if (b.collection !== collection) continue;
|
|
3500
|
+
for (const u of b.batch.updates) {
|
|
3501
|
+
out.set(String(u._id), u.update);
|
|
3502
|
+
}
|
|
3503
|
+
}
|
|
3504
|
+
}
|
|
3505
|
+
return out;
|
|
3506
|
+
}
|
|
3306
3507
|
/**
|
|
3307
3508
|
* Upload dirty items for all collections.
|
|
3308
3509
|
*/
|
|
@@ -3417,7 +3618,11 @@ var _SyncEngine = class _SyncEngine {
|
|
|
3417
3618
|
continue;
|
|
3418
3619
|
}
|
|
3419
3620
|
}
|
|
3420
|
-
mappedUpdates.push(
|
|
3621
|
+
mappedUpdates.push({
|
|
3622
|
+
_id: candidate._id,
|
|
3623
|
+
_rev: dirtyBaseRev != null ? dirtyBaseRev : 0,
|
|
3624
|
+
update: candidate.update
|
|
3625
|
+
});
|
|
3421
3626
|
}
|
|
3422
3627
|
if (mappedUpdates.length === 0) continue;
|
|
3423
3628
|
collectionBatches.push([{
|
|
@@ -3466,8 +3671,12 @@ var _SyncEngine = class _SyncEngine {
|
|
|
3466
3671
|
for (const result of results) {
|
|
3467
3672
|
const {
|
|
3468
3673
|
collection,
|
|
3469
|
-
results: { inserted, updated, deleted, errors: errors2, warnings }
|
|
3674
|
+
results: { inserted, updated, deleted, errors: errors2, warnings, mustRefresh }
|
|
3470
3675
|
} = result;
|
|
3676
|
+
const mustRefreshIds = /* @__PURE__ */ new Set();
|
|
3677
|
+
if (mustRefresh && mustRefresh.length > 0) {
|
|
3678
|
+
for (const r of mustRefresh) mustRefreshIds.add(String(r._id));
|
|
3679
|
+
}
|
|
3471
3680
|
const erroredIds = /* @__PURE__ */ new Set();
|
|
3472
3681
|
if (errors2 && errors2.length > 0) {
|
|
3473
3682
|
for (const e of errors2) erroredIds.add(String(e._id));
|
|
@@ -3503,6 +3712,8 @@ var _SyncEngine = class _SyncEngine {
|
|
|
3503
3712
|
`[SyncEngine] Sync upload [${collection}]: ${ambiguous.length} id(s) appeared in BOTH inserted/updated/deleted AND errors[] \u2014 keeping dirty for safety. _ids: ${ambiguous.join(", ")}`
|
|
3504
3713
|
);
|
|
3505
3714
|
}
|
|
3715
|
+
const dirtyBeforeRefresh = mustRefreshIds.size > 0 ? await this.dexieDb.getDirtyChangesBatch(collection, [...mustRefreshIds]) : /* @__PURE__ */ new Map();
|
|
3716
|
+
const uploadedByIdRefresh = mustRefreshIds.size > 0 ? this.uploadedSnapshotFor(collectionBatches, collection) : /* @__PURE__ */ new Map();
|
|
3506
3717
|
if (allSuccessIds.length > 0) {
|
|
3507
3718
|
await this.dexieDb.clearDirtyChangesBatch(collection, allSuccessIds);
|
|
3508
3719
|
}
|
|
@@ -3523,6 +3734,7 @@ var _SyncEngine = class _SyncEngine {
|
|
|
3523
3734
|
for (let i = 0; i < insertedAndUpdated.length; i++) {
|
|
3524
3735
|
const entity = insertedAndUpdated[i];
|
|
3525
3736
|
const dexieItem = dexieItems[i];
|
|
3737
|
+
if (mustRefreshIds.has(String(entity._id))) continue;
|
|
3526
3738
|
if (dexieItem) {
|
|
3527
3739
|
dexieItem._rev = entity._rev;
|
|
3528
3740
|
dexieItem._ts = entity._ts;
|
|
@@ -3570,6 +3782,14 @@ var _SyncEngine = class _SyncEngine {
|
|
|
3570
3782
|
sentCount += deleted.length;
|
|
3571
3783
|
collectionSentCount += deleted.length;
|
|
3572
3784
|
}
|
|
3785
|
+
if (mustRefresh && mustRefresh.length > 0) {
|
|
3786
|
+
await this.adoptMustRefresh(
|
|
3787
|
+
collection,
|
|
3788
|
+
mustRefresh,
|
|
3789
|
+
uploadedByIdRefresh,
|
|
3790
|
+
dirtyBeforeRefresh
|
|
3791
|
+
);
|
|
3792
|
+
}
|
|
3573
3793
|
if (collectionSentCount > 0) {
|
|
3574
3794
|
collectionSentCounts[collection] = collectionSentCount;
|
|
3575
3795
|
}
|
|
@@ -3664,7 +3884,11 @@ var _SyncEngine = class _SyncEngine {
|
|
|
3664
3884
|
batch: {
|
|
3665
3885
|
updates: updates.map((item) => {
|
|
3666
3886
|
const _a = item.delta, { _ts, _rev } = _a, changes = __objRest(_a, ["_ts", "_rev"]);
|
|
3667
|
-
return {
|
|
3887
|
+
return {
|
|
3888
|
+
_id: item._id,
|
|
3889
|
+
_rev: typeof _rev === "number" ? _rev : 0,
|
|
3890
|
+
update: changes
|
|
3891
|
+
};
|
|
3668
3892
|
}),
|
|
3669
3893
|
deletes: []
|
|
3670
3894
|
}
|
|
@@ -3676,7 +3900,7 @@ var _SyncEngine = class _SyncEngine {
|
|
|
3676
3900
|
let sentCount = 0;
|
|
3677
3901
|
for (const result of results) {
|
|
3678
3902
|
const {
|
|
3679
|
-
results: { inserted, updated, deleted, errors: errors2, warnings }
|
|
3903
|
+
results: { inserted, updated, deleted, errors: errors2, warnings, mustRefresh }
|
|
3680
3904
|
} = result;
|
|
3681
3905
|
const erroredIds = /* @__PURE__ */ new Set();
|
|
3682
3906
|
if (errors2 && errors2.length > 0) {
|
|
@@ -3725,9 +3949,20 @@ var _SyncEngine = class _SyncEngine {
|
|
|
3725
3949
|
`[SyncEngine] Sync upload [${collection}]: ${ambiguous.length} id(s) appeared in BOTH inserted/updated/deleted AND errors[] \u2014 keeping dirty for safety. _ids: ${ambiguous.join(", ")}`
|
|
3726
3950
|
);
|
|
3727
3951
|
}
|
|
3952
|
+
const refreshIds = (mustRefresh != null ? mustRefresh : []).map((r) => String(r._id));
|
|
3953
|
+
const dirtyBeforeRefresh = refreshIds.length > 0 ? await this.dexieDb.getDirtyChangesBatch(collection, refreshIds) : /* @__PURE__ */ new Map();
|
|
3954
|
+
const uploadedByIdRefresh = refreshIds.length > 0 ? this.uploadedSnapshotFor(collectionBatches, collection) : /* @__PURE__ */ new Map();
|
|
3728
3955
|
if (allSuccessIds.length > 0) {
|
|
3729
3956
|
await this.dexieDb.clearDirtyChangesBatch(collection, allSuccessIds);
|
|
3730
3957
|
}
|
|
3958
|
+
if (mustRefresh && mustRefresh.length > 0) {
|
|
3959
|
+
await this.adoptMustRefresh(
|
|
3960
|
+
collection,
|
|
3961
|
+
mustRefresh,
|
|
3962
|
+
uploadedByIdRefresh,
|
|
3963
|
+
dirtyBeforeRefresh
|
|
3964
|
+
);
|
|
3965
|
+
}
|
|
3731
3966
|
sentCount += allSuccessIds.length;
|
|
3732
3967
|
}
|
|
3733
3968
|
return { sentCount };
|
|
@@ -4456,6 +4691,8 @@ var _SyncedDb = class _SyncedDb {
|
|
|
4456
4691
|
this.syncOnlyCollections = null;
|
|
4457
4692
|
// Sync metadata cache
|
|
4458
4693
|
this.syncMetaCache = /* @__PURE__ */ new Map();
|
|
4694
|
+
// Per-collection hydration status (powers getPreloadStatus / onPreloadStatusChange)
|
|
4695
|
+
this.preloadStatusMap = /* @__PURE__ */ new Map();
|
|
4459
4696
|
this._pendingFullResync = false;
|
|
4460
4697
|
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p;
|
|
4461
4698
|
this.tenant = config.tenant;
|
|
@@ -4475,6 +4712,7 @@ var _SyncedDb = class _SyncedDb {
|
|
|
4475
4712
|
this.onDexieSyncStart = config.onDexieSyncStart;
|
|
4476
4713
|
this.onDexieSyncEnd = config.onDexieSyncEnd;
|
|
4477
4714
|
this.onSyncProgress = config.onSyncProgress;
|
|
4715
|
+
this.onPreloadStatusChange = config.onPreloadStatusChange;
|
|
4478
4716
|
this.onServerSyncStart = config.onServerSyncStart;
|
|
4479
4717
|
this.onServerSyncEnd = config.onServerSyncEnd;
|
|
4480
4718
|
this.onConflictResolved = config.onConflictResolved;
|
|
@@ -4494,6 +4732,7 @@ var _SyncedDb = class _SyncedDb {
|
|
|
4494
4732
|
this.evictOnWake = (_g = config.evictOnWake) != null ? _g : false;
|
|
4495
4733
|
for (const col of config.collections) {
|
|
4496
4734
|
this.collections.set(col.name, col);
|
|
4735
|
+
if (!col.writeOnly) this.preloadStatusMap.set(col.name, { state: "pending", itemCount: 0 });
|
|
4497
4736
|
}
|
|
4498
4737
|
this.inMemManager = new InMemManager({
|
|
4499
4738
|
inMemDb: this.inMemDb,
|
|
@@ -4794,6 +5033,9 @@ var _SyncedDb = class _SyncedDb {
|
|
|
4794
5033
|
const existing = this.collections.get(spec.name);
|
|
4795
5034
|
if (existing && !existing.temporaryConfig) continue;
|
|
4796
5035
|
this.collections.set(spec.name, spec);
|
|
5036
|
+
if (!spec.writeOnly && !this.preloadStatusMap.has(spec.name)) {
|
|
5037
|
+
this.preloadStatusMap.set(spec.name, { state: "pending", itemCount: 0 });
|
|
5038
|
+
}
|
|
4797
5039
|
if (this.syncOnlyCollections) {
|
|
4798
5040
|
this.syncOnlyCollections.add(spec.name);
|
|
4799
5041
|
}
|
|
@@ -4914,8 +5156,10 @@ var _SyncedDb = class _SyncedDb {
|
|
|
4914
5156
|
for (const [name] of this.collections) {
|
|
4915
5157
|
if (this.isSyncAllowed(name) && !prevAllowed.has(name)) {
|
|
4916
5158
|
newlyAllowed.push(name);
|
|
5159
|
+
this.preloadStatusMap.set(name, { state: "pending", itemCount: 0 });
|
|
4917
5160
|
}
|
|
4918
5161
|
}
|
|
5162
|
+
this.emitPreloadStatusChange();
|
|
4919
5163
|
if (newlyAllowed.length > 0) {
|
|
4920
5164
|
await this.loadCollectionsToInMem(newlyAllowed, "setSyncOnlyTheseCollections");
|
|
4921
5165
|
}
|
|
@@ -5573,7 +5817,7 @@ var _SyncedDb = class _SyncedDb {
|
|
|
5573
5817
|
);
|
|
5574
5818
|
}
|
|
5575
5819
|
const fullChanges = __spreadProps(__spreadValues({}, update), { _lastUpdaterId: this.updaterId });
|
|
5576
|
-
const diff =
|
|
5820
|
+
const diff = computeObjDiff(existing, fullChanges);
|
|
5577
5821
|
await this.dexieDb.addDirtyChange(
|
|
5578
5822
|
collection,
|
|
5579
5823
|
id,
|
|
@@ -5582,7 +5826,7 @@ var _SyncedDb = class _SyncedDb {
|
|
|
5582
5826
|
);
|
|
5583
5827
|
const isWriteOnly = (_b = this.collections.get(collection)) == null ? void 0 : _b.writeOnly;
|
|
5584
5828
|
const currentMem = isWriteOnly ? null : this.inMemDb.getById(collection, id);
|
|
5585
|
-
const merged =
|
|
5829
|
+
const merged = applyObjDiff(
|
|
5586
5830
|
currentMem != null ? currentMem : existing,
|
|
5587
5831
|
diff,
|
|
5588
5832
|
id,
|
|
@@ -5844,6 +6088,7 @@ var _SyncedDb = class _SyncedDb {
|
|
|
5844
6088
|
}
|
|
5845
6089
|
} finally {
|
|
5846
6090
|
this.syncLock = false;
|
|
6091
|
+
this.emitPreloadStatusChange();
|
|
5847
6092
|
}
|
|
5848
6093
|
}
|
|
5849
6094
|
async processQueuedWsUpdates() {
|
|
@@ -5860,8 +6105,8 @@ var _SyncedDb = class _SyncedDb {
|
|
|
5860
6105
|
// ==================== Batch Operations ====================
|
|
5861
6106
|
/**
|
|
5862
6107
|
* Run each batch entry through the standard `upsert()` pipeline so every
|
|
5863
|
-
* write — single or batched — goes through `
|
|
5864
|
-
* `
|
|
6108
|
+
* write — single or batched — goes through `computeObjDiff` →
|
|
6109
|
+
* `applyObjDiff` → in-mem write + Dexie schedule + dirty change.
|
|
5865
6110
|
*
|
|
5866
6111
|
* Server upload remains batched: dirty entries accumulated by the per-item
|
|
5867
6112
|
* calls coalesce into one `updateCollections` request at the next sync
|
|
@@ -6660,7 +6905,11 @@ var _SyncedDb = class _SyncedDb {
|
|
|
6660
6905
|
async () => {
|
|
6661
6906
|
while (queue.length > 0) {
|
|
6662
6907
|
const name = queue.shift();
|
|
6663
|
-
|
|
6908
|
+
let items = 0;
|
|
6909
|
+
try {
|
|
6910
|
+
items = await this.loadCollectionToInMem(name);
|
|
6911
|
+
} catch (e) {
|
|
6912
|
+
}
|
|
6664
6913
|
totalItems += items;
|
|
6665
6914
|
loaded++;
|
|
6666
6915
|
this.safeCallback(this.onSyncProgress, {
|
|
@@ -6682,7 +6931,73 @@ var _SyncedDb = class _SyncedDb {
|
|
|
6682
6931
|
});
|
|
6683
6932
|
return totalItems;
|
|
6684
6933
|
}
|
|
6934
|
+
/**
|
|
6935
|
+
* Hydrate a single collection from Dexie, recording the outcome in
|
|
6936
|
+
* {@link preloadStatusMap} (hydrated / failed) for {@link getPreloadStatus}.
|
|
6937
|
+
* Rethrows on failure so existing callers (e.g. the `Promise.allSettled` in
|
|
6938
|
+
* addCollectionsToSync, the per-collection try/catch in loadCollectionsToInMem)
|
|
6939
|
+
* keep their current behavior.
|
|
6940
|
+
*/
|
|
6685
6941
|
async loadCollectionToInMem(name) {
|
|
6942
|
+
var _a;
|
|
6943
|
+
try {
|
|
6944
|
+
const count = await this._hydrateCollectionFromDexie(name);
|
|
6945
|
+
this.preloadStatusMap.set(name, { state: "hydrated", itemCount: count, hydratedAt: /* @__PURE__ */ new Date() });
|
|
6946
|
+
this.emitPreloadStatusChange();
|
|
6947
|
+
return count;
|
|
6948
|
+
} catch (err) {
|
|
6949
|
+
const prev = this.preloadStatusMap.get(name);
|
|
6950
|
+
this.preloadStatusMap.set(name, {
|
|
6951
|
+
state: "failed",
|
|
6952
|
+
itemCount: (_a = prev == null ? void 0 : prev.itemCount) != null ? _a : 0,
|
|
6953
|
+
hydratedAt: prev == null ? void 0 : prev.hydratedAt,
|
|
6954
|
+
lastError: err instanceof Error ? err.message : String(err)
|
|
6955
|
+
});
|
|
6956
|
+
this.emitPreloadStatusChange();
|
|
6957
|
+
throw err;
|
|
6958
|
+
}
|
|
6959
|
+
}
|
|
6960
|
+
/**
|
|
6961
|
+
* Per-collection hydration status + rolled-up aggregate, over readable
|
|
6962
|
+
* (non-`writeOnly`), in-scope collections. See {@link I_SyncedDb.getPreloadStatus}.
|
|
6963
|
+
*/
|
|
6964
|
+
getPreloadStatus() {
|
|
6965
|
+
var _a, _b;
|
|
6966
|
+
const collections = [];
|
|
6967
|
+
let readyCount = 0;
|
|
6968
|
+
let failedCount = 0;
|
|
6969
|
+
let pendingCount = 0;
|
|
6970
|
+
for (const name of this.collections.keys()) {
|
|
6971
|
+
if (!this.isSyncAllowed(name)) continue;
|
|
6972
|
+
const rec = (_a = this.preloadStatusMap.get(name)) != null ? _a : { state: "pending", itemCount: 0 };
|
|
6973
|
+
const everDownloaded = !!((_b = this.syncMetaCache.get(name)) == null ? void 0 : _b.lastSyncTs);
|
|
6974
|
+
const ready = rec.state === "hydrated" && (rec.itemCount > 0 || everDownloaded);
|
|
6975
|
+
if (rec.state === "failed") failedCount++;
|
|
6976
|
+
else if (ready) readyCount++;
|
|
6977
|
+
else pendingCount++;
|
|
6978
|
+
collections.push({
|
|
6979
|
+
name,
|
|
6980
|
+
state: rec.state,
|
|
6981
|
+
itemCount: rec.itemCount,
|
|
6982
|
+
ready,
|
|
6983
|
+
hydratedAt: rec.hydratedAt,
|
|
6984
|
+
lastError: rec.lastError,
|
|
6985
|
+
everDownloaded
|
|
6986
|
+
});
|
|
6987
|
+
}
|
|
6988
|
+
const expectedCount = collections.length;
|
|
6989
|
+
let aggregate;
|
|
6990
|
+
if (expectedCount === 0) aggregate = "idle";
|
|
6991
|
+
else if (readyCount === expectedCount) aggregate = "full";
|
|
6992
|
+
else if (failedCount === expectedCount) aggregate = "failed";
|
|
6993
|
+
else aggregate = "partial";
|
|
6994
|
+
return { aggregate, collections, expectedCount, readyCount, failedCount, pendingCount };
|
|
6995
|
+
}
|
|
6996
|
+
/** Emit onPreloadStatusChange with a fresh snapshot (skips computation when no listener). */
|
|
6997
|
+
emitPreloadStatusChange() {
|
|
6998
|
+
if (this.onPreloadStatusChange) this.safeCallback(this.onPreloadStatusChange, this.getPreloadStatus());
|
|
6999
|
+
}
|
|
7000
|
+
async _hydrateCollectionFromDexie(name) {
|
|
6686
7001
|
const allItems = [];
|
|
6687
7002
|
await this.dexieDb.forEachBatch(name, 2e3, async (chunk) => {
|
|
6688
7003
|
for (let i = 0; i < chunk.length; i++) {
|
|
@@ -6709,7 +7024,7 @@ var _SyncedDb = class _SyncedDb {
|
|
|
6709
7024
|
if (key === "_id" || key === "_ts" || key === "_rev") continue;
|
|
6710
7025
|
diff[key] = dirtyItem[key];
|
|
6711
7026
|
}
|
|
6712
|
-
const merged =
|
|
7027
|
+
const merged = applyObjDiff(
|
|
6713
7028
|
existing != null ? existing : null,
|
|
6714
7029
|
diff,
|
|
6715
7030
|
id,
|
|
@@ -6900,224 +7215,6 @@ var _SyncedDb = class _SyncedDb {
|
|
|
6900
7215
|
static isObjectIdLike(v) {
|
|
6901
7216
|
return !!(v && typeof v === "object" && (v._bsontype === "ObjectId" || typeof v.toHexString === "function"));
|
|
6902
7217
|
}
|
|
6903
|
-
/**
|
|
6904
|
-
* Mongo-symmetric local apply: starting from `base` (a safe deep clone of
|
|
6905
|
-
* `currentMem` or `existing`), walk each `(path, value)` entry of `diff`:
|
|
6906
|
-
*
|
|
6907
|
-
* - `value === undefined` → `deleteByPath` (mongo `$unset` symmetric)
|
|
6908
|
-
* - otherwise → `setByPath` (mongo `$set` symmetric)
|
|
6909
|
-
*
|
|
6910
|
-
* The result is what an equivalent server-side `$set` + `$unset` would
|
|
6911
|
-
* have produced. Replaces the previous shallow `{ ...currentMem, ...update }`
|
|
6912
|
-
* merge which dropped nested fields the caller's `update` didn't mention.
|
|
6913
|
-
*
|
|
6914
|
-
* Returns a new object (the cloned-and-mutated `base`); never mutates
|
|
6915
|
-
* the input `base` reference.
|
|
6916
|
-
*/
|
|
6917
|
-
static applyDiffLocally(base, diff, fallbackId, collection) {
|
|
6918
|
-
const seed = base ? _SyncedDb.safeDeepClone(base) : { _id: fallbackId };
|
|
6919
|
-
if (seed._id == null) seed._id = fallbackId;
|
|
6920
|
-
for (const path of Object.keys(diff)) {
|
|
6921
|
-
const value = diff[path];
|
|
6922
|
-
if (value === void 0) {
|
|
6923
|
-
deleteByPath(seed, path);
|
|
6924
|
-
continue;
|
|
6925
|
-
}
|
|
6926
|
-
if (!path.includes(".") && !path.includes("[")) {
|
|
6927
|
-
seed[path] = value;
|
|
6928
|
-
continue;
|
|
6929
|
-
}
|
|
6930
|
-
const ok = setByPath(seed, path, value);
|
|
6931
|
-
if (ok) continue;
|
|
6932
|
-
_SyncedDb.materializeBracketPath(seed, path, value, collection, fallbackId);
|
|
6933
|
-
}
|
|
6934
|
-
return seed;
|
|
6935
|
-
}
|
|
6936
|
-
/**
|
|
6937
|
-
* Fallback for `setByPath` failures inside `applyDiffLocally`. Materializes
|
|
6938
|
-
* missing array containers AND missing intermediate plain objects so the
|
|
6939
|
-
* diff entry can land locally instead of being dropped.
|
|
6940
|
-
*
|
|
6941
|
-
* Supported path shape (tokenizes to `[…plain prefix, [<id>], …plain suffix]`):
|
|
6942
|
-
*
|
|
6943
|
-
* • Single-segment prefix:
|
|
6944
|
-
* - `polje[<id>] = <obj>` → seed.polje = [<obj>]
|
|
6945
|
-
* - `polje[<id>].field = <v>` → seed.polje = [{_id, field: <v>}]
|
|
6946
|
-
* - `polje[<id>].a.b.c = <v>` → seed.polje = [{_id, a: {b: {c: <v>}}}]
|
|
6947
|
-
*
|
|
6948
|
-
* • Multi-segment plain prefix:
|
|
6949
|
-
* - `outer.polje[<id>] = <obj>` → seed.outer = {polje: [<obj>]}
|
|
6950
|
-
* - `outer.inner.polje[<id>].field = <v>` → seed.outer = {inner: {polje: [{_id, field: <v>}]}}
|
|
6951
|
-
*
|
|
6952
|
-
* • Existing matching `_id` on the target array: walk into the element
|
|
6953
|
-
* and create missing intermediates on the way down; set the leaf
|
|
6954
|
-
* without pushing a duplicate element.
|
|
6955
|
-
*
|
|
6956
|
-
* Dropped:
|
|
6957
|
-
* • Multi-bracket paths (e.g. `polje[a].sub[b]`, `outer[a].sub.inner[b]`)
|
|
6958
|
-
* — require shape knowledge the fallback can't reconstruct. Server
|
|
6959
|
-
* applies the path; next sync hydrates the canonical state locally.
|
|
6960
|
-
* • Existing intermediate that is a non-plain value (Date, primitive,
|
|
6961
|
-
* array where an object is expected).
|
|
6962
|
-
*
|
|
6963
|
-
* `_`-prefixed first segment (e.g. `_redundanca…`): for shapes that
|
|
6964
|
-
* extend BEYOND the originally-supported single-segment prefix, the
|
|
6965
|
-
* drop is **silent** and unconditional — these fields are server-
|
|
6966
|
-
* mirrored and local materialization could create state that diverges
|
|
6967
|
-
* from the canonical server form. Simple single-segment shapes
|
|
6968
|
-
* (`_field[<id>]`, `_field[<id>].sub`, `_field[<id>].sub.deep…`) still
|
|
6969
|
-
* materialize as before.
|
|
6970
|
-
*
|
|
6971
|
-
* Replaces the pre-fix blind `seed[path] = value` fallback that stamped
|
|
6972
|
-
* literal bracket-keyed top-level properties (e.g. `"postavke[<id>]": [<el>]`)
|
|
6973
|
-
* onto Dexie rows and in-mem state, persisting forever through subsequent
|
|
6974
|
-
* `safeDeepClone`-based save cycles.
|
|
6975
|
-
*/
|
|
6976
|
-
static materializeBracketPath(seed, path, value, collection, id) {
|
|
6977
|
-
const tokens = tokenizePath(path);
|
|
6978
|
-
const firstToken = tokens[0];
|
|
6979
|
-
const dropSilently = typeof firstToken === "string" && firstToken.startsWith("_");
|
|
6980
|
-
const drop = (reason) => {
|
|
6981
|
-
if (dropSilently) return;
|
|
6982
|
-
console.error(
|
|
6983
|
-
`[SyncedDb] applyDiffLocally: dropping bracket-path diff entry (${reason})`,
|
|
6984
|
-
{ collection, _id: String(id), path, value }
|
|
6985
|
-
);
|
|
6986
|
-
};
|
|
6987
|
-
if (tokens.length < 2) {
|
|
6988
|
-
drop(`unsupported token count ${tokens.length}`);
|
|
6989
|
-
return;
|
|
6990
|
-
}
|
|
6991
|
-
if (firstToken === void 0 || firstToken.startsWith("[")) {
|
|
6992
|
-
drop("first segment is not a plain field");
|
|
6993
|
-
return;
|
|
6994
|
-
}
|
|
6995
|
-
let bracketIdx = -1;
|
|
6996
|
-
for (let i = 1; i < tokens.length; i++) {
|
|
6997
|
-
if (tokens[i].startsWith("[")) {
|
|
6998
|
-
bracketIdx = i;
|
|
6999
|
-
break;
|
|
7000
|
-
}
|
|
7001
|
-
}
|
|
7002
|
-
if (bracketIdx < 0) {
|
|
7003
|
-
drop("no bracket segment found");
|
|
7004
|
-
return;
|
|
7005
|
-
}
|
|
7006
|
-
for (let i = bracketIdx + 1; i < tokens.length; i++) {
|
|
7007
|
-
if (tokens[i].startsWith("[")) {
|
|
7008
|
-
drop("nested bracket path");
|
|
7009
|
-
return;
|
|
7010
|
-
}
|
|
7011
|
-
}
|
|
7012
|
-
if (dropSilently && bracketIdx > 1) {
|
|
7013
|
-
return;
|
|
7014
|
-
}
|
|
7015
|
-
const prefixTokens = tokens.slice(0, bracketIdx);
|
|
7016
|
-
const bracketToken = tokens[bracketIdx];
|
|
7017
|
-
const suffixTokens = tokens.slice(bracketIdx + 1);
|
|
7018
|
-
const bracketId = bracketToken.slice(1, -1);
|
|
7019
|
-
if (bracketId.length === 0) {
|
|
7020
|
-
drop("empty bracket id");
|
|
7021
|
-
return;
|
|
7022
|
-
}
|
|
7023
|
-
let parent = seed;
|
|
7024
|
-
for (let i = 0; i < prefixTokens.length - 1; i++) {
|
|
7025
|
-
const seg = prefixTokens[i];
|
|
7026
|
-
const cur = parent[seg];
|
|
7027
|
-
if (cur === void 0 || cur === null) {
|
|
7028
|
-
parent[seg] = {};
|
|
7029
|
-
} else if (typeof cur !== "object" || Array.isArray(cur) || cur instanceof Date) {
|
|
7030
|
-
drop(
|
|
7031
|
-
`existing intermediate "${prefixTokens.slice(0, i + 1).join(".")}" is not a plain object`
|
|
7032
|
-
);
|
|
7033
|
-
return;
|
|
7034
|
-
}
|
|
7035
|
-
parent = parent[seg];
|
|
7036
|
-
}
|
|
7037
|
-
const lastPrefixSeg = prefixTokens[prefixTokens.length - 1];
|
|
7038
|
-
let arr = parent[lastPrefixSeg];
|
|
7039
|
-
if (arr != null && !Array.isArray(arr)) {
|
|
7040
|
-
drop(`existing "${prefixTokens.join(".")}" is not an array`);
|
|
7041
|
-
return;
|
|
7042
|
-
}
|
|
7043
|
-
if (arr == null) {
|
|
7044
|
-
arr = [];
|
|
7045
|
-
parent[lastPrefixSeg] = arr;
|
|
7046
|
-
}
|
|
7047
|
-
if (suffixTokens.length === 0) {
|
|
7048
|
-
let element = value;
|
|
7049
|
-
if (Array.isArray(value) && value.length === 1 && value[0] != null && typeof value[0] === "object") {
|
|
7050
|
-
element = value[0];
|
|
7051
|
-
}
|
|
7052
|
-
if (element == null || typeof element !== "object" || Array.isArray(element)) {
|
|
7053
|
-
drop("value is not a single element or wire-form wrapper");
|
|
7054
|
-
return;
|
|
7055
|
-
}
|
|
7056
|
-
if (element._id == null) {
|
|
7057
|
-
element._id = bracketId;
|
|
7058
|
-
}
|
|
7059
|
-
const replaceIdx = arr.findIndex(
|
|
7060
|
-
(it) => it != null && typeof it === "object" && String(it._id) === bracketId
|
|
7061
|
-
);
|
|
7062
|
-
if (replaceIdx >= 0) {
|
|
7063
|
-
arr[replaceIdx] = element;
|
|
7064
|
-
} else {
|
|
7065
|
-
arr.push(element);
|
|
7066
|
-
}
|
|
7067
|
-
return;
|
|
7068
|
-
}
|
|
7069
|
-
const buildNestedSubTree = (segs, leaf) => {
|
|
7070
|
-
let acc = leaf;
|
|
7071
|
-
for (let i = segs.length - 1; i >= 0; i--) {
|
|
7072
|
-
acc = { [segs[i]]: acc };
|
|
7073
|
-
}
|
|
7074
|
-
return acc;
|
|
7075
|
-
};
|
|
7076
|
-
const existingIdx = arr.findIndex(
|
|
7077
|
-
(it) => it != null && typeof it === "object" && String(it._id) === bracketId
|
|
7078
|
-
);
|
|
7079
|
-
if (existingIdx >= 0) {
|
|
7080
|
-
let cur = arr[existingIdx];
|
|
7081
|
-
for (let i = 0; i < suffixTokens.length - 1; i++) {
|
|
7082
|
-
const seg = suffixTokens[i];
|
|
7083
|
-
const next = cur[seg];
|
|
7084
|
-
if (next == null || typeof next !== "object" || Array.isArray(next)) {
|
|
7085
|
-
cur[seg] = {};
|
|
7086
|
-
}
|
|
7087
|
-
cur = cur[seg];
|
|
7088
|
-
}
|
|
7089
|
-
cur[suffixTokens[suffixTokens.length - 1]] = value;
|
|
7090
|
-
} else {
|
|
7091
|
-
const subTree = buildNestedSubTree(suffixTokens, value);
|
|
7092
|
-
arr.push(__spreadValues({ _id: bracketId }, subTree));
|
|
7093
|
-
}
|
|
7094
|
-
}
|
|
7095
|
-
/**
|
|
7096
|
-
* Deep clone for `applyDiffLocally`. Recurses into plain objects and
|
|
7097
|
-
* arrays; preserves `Date` (cloned to avoid shared reference) and
|
|
7098
|
-
* `ObjectId`-like values by reference (their internal Buffer state is
|
|
7099
|
-
* immutable from our perspective). Other class instances pass through
|
|
7100
|
-
* by reference. Avoids `structuredClone` because it throws on class
|
|
7101
|
-
* instances like bson `ObjectId`.
|
|
7102
|
-
*/
|
|
7103
|
-
static safeDeepClone(value) {
|
|
7104
|
-
if (value === null || value === void 0) return value;
|
|
7105
|
-
if (typeof value !== "object") return value;
|
|
7106
|
-
if (value instanceof Date) return new Date(value.getTime());
|
|
7107
|
-
if (_SyncedDb.isObjectIdLike(value)) return value;
|
|
7108
|
-
if (Array.isArray(value)) {
|
|
7109
|
-
const out2 = new Array(value.length);
|
|
7110
|
-
for (let i = 0; i < value.length; i++) out2[i] = _SyncedDb.safeDeepClone(value[i]);
|
|
7111
|
-
return out2;
|
|
7112
|
-
}
|
|
7113
|
-
const proto = Object.getPrototypeOf(value);
|
|
7114
|
-
if (proto !== Object.prototype && proto !== null) return value;
|
|
7115
|
-
const out = {};
|
|
7116
|
-
for (const key of Object.keys(value)) {
|
|
7117
|
-
out[key] = _SyncedDb.safeDeepClone(value[key]);
|
|
7118
|
-
}
|
|
7119
|
-
return out;
|
|
7120
|
-
}
|
|
7121
7218
|
/**
|
|
7122
7219
|
* Asserts write-only collection has online connectivity for reads.
|
|
7123
7220
|
* @throws Error if offline
|
|
@@ -7173,6 +7270,105 @@ var SyncedDb = _SyncedDb;
|
|
|
7173
7270
|
|
|
7174
7271
|
// src/db/DexieDb.ts
|
|
7175
7272
|
import Dexie from "dexie";
|
|
7273
|
+
|
|
7274
|
+
// src/utils/computeDiff.ts
|
|
7275
|
+
function isDescendantOrEqual(path, candidate) {
|
|
7276
|
+
if (path === candidate) return true;
|
|
7277
|
+
if (!path.startsWith(candidate)) return false;
|
|
7278
|
+
const next = path[candidate.length];
|
|
7279
|
+
return next === "." || next === "[";
|
|
7280
|
+
}
|
|
7281
|
+
function pathsOverlap(a, b) {
|
|
7282
|
+
return isDescendantOrEqual(a, b) || isDescendantOrEqual(b, a);
|
|
7283
|
+
}
|
|
7284
|
+
function isTerminalBracketKey(path) {
|
|
7285
|
+
const tokens = tokenizePath(path);
|
|
7286
|
+
const last = tokens[tokens.length - 1];
|
|
7287
|
+
return !!(last && last.length >= 2 && last.charCodeAt(0) === 91 && last.charCodeAt(last.length - 1) === 93);
|
|
7288
|
+
}
|
|
7289
|
+
function canExpandArrayToBrackets(value) {
|
|
7290
|
+
if (!Array.isArray(value) || value.length === 0) return false;
|
|
7291
|
+
for (const el of value) {
|
|
7292
|
+
if (el === null || typeof el !== "object") return false;
|
|
7293
|
+
if (el._id == null) return false;
|
|
7294
|
+
}
|
|
7295
|
+
return true;
|
|
7296
|
+
}
|
|
7297
|
+
function pickLayerTarget(newPath, newValue) {
|
|
7298
|
+
if (!isTerminalBracketKey(newPath)) return null;
|
|
7299
|
+
if (newValue === void 0) return null;
|
|
7300
|
+
if (Array.isArray(newValue) && newValue.length === 1 && newValue[0] && typeof newValue[0] === "object") {
|
|
7301
|
+
return newValue[0];
|
|
7302
|
+
}
|
|
7303
|
+
if (newValue && typeof newValue === "object" && !Array.isArray(newValue)) {
|
|
7304
|
+
return newValue;
|
|
7305
|
+
}
|
|
7306
|
+
return null;
|
|
7307
|
+
}
|
|
7308
|
+
function mergeDirtyPath(accumulated, newPath, newValue) {
|
|
7309
|
+
for (const existingKey of Object.keys(accumulated)) {
|
|
7310
|
+
if (existingKey === newPath) continue;
|
|
7311
|
+
if (isDescendantOrEqual(newPath, existingKey)) {
|
|
7312
|
+
const existingValue = accumulated[existingKey];
|
|
7313
|
+
const existingIsTerminal = isTerminalBracketKey(existingKey);
|
|
7314
|
+
if (existingIsTerminal && existingValue === void 0) {
|
|
7315
|
+
return;
|
|
7316
|
+
}
|
|
7317
|
+
let mutationTarget = existingValue;
|
|
7318
|
+
if (existingIsTerminal && Array.isArray(existingValue) && existingValue.length === 1) {
|
|
7319
|
+
mutationTarget = existingValue[0];
|
|
7320
|
+
}
|
|
7321
|
+
if (!existingIsTerminal && (existingValue === null || existingValue === void 0)) {
|
|
7322
|
+
accumulated[existingKey] = {};
|
|
7323
|
+
mutationTarget = accumulated[existingKey];
|
|
7324
|
+
}
|
|
7325
|
+
if (!existingIsTerminal && newPath[existingKey.length] === "[" && canExpandArrayToBrackets(existingValue)) {
|
|
7326
|
+
delete accumulated[existingKey];
|
|
7327
|
+
for (const el of existingValue) {
|
|
7328
|
+
accumulated[`${existingKey}[${String(el._id)}]`] = [el];
|
|
7329
|
+
}
|
|
7330
|
+
mergeDirtyPath(accumulated, newPath, newValue);
|
|
7331
|
+
return;
|
|
7332
|
+
}
|
|
7333
|
+
const sepChar = newPath[existingKey.length];
|
|
7334
|
+
const relativePath = sepChar === "[" ? newPath.substring(existingKey.length) : newPath.substring(existingKey.length + 1);
|
|
7335
|
+
const ok = setByPath(mutationTarget, relativePath, newValue);
|
|
7336
|
+
if (ok) return;
|
|
7337
|
+
delete accumulated[existingKey];
|
|
7338
|
+
accumulated[newPath] = newValue;
|
|
7339
|
+
return;
|
|
7340
|
+
}
|
|
7341
|
+
}
|
|
7342
|
+
const descendants = [];
|
|
7343
|
+
for (const existingKey of Object.keys(accumulated)) {
|
|
7344
|
+
if (existingKey === newPath) continue;
|
|
7345
|
+
if (isDescendantOrEqual(existingKey, newPath)) {
|
|
7346
|
+
descendants.push(existingKey);
|
|
7347
|
+
}
|
|
7348
|
+
}
|
|
7349
|
+
const layerInto = pickLayerTarget(newPath, newValue);
|
|
7350
|
+
if (layerInto && descendants.length > 0) {
|
|
7351
|
+
for (const desc of descendants) {
|
|
7352
|
+
const sepChar = desc[newPath.length];
|
|
7353
|
+
const relativePath = sepChar === "[" ? desc.substring(newPath.length) : desc.substring(newPath.length + 1);
|
|
7354
|
+
const descValue = accumulated[desc];
|
|
7355
|
+
if (descValue === void 0) {
|
|
7356
|
+
deleteByPath(layerInto, relativePath);
|
|
7357
|
+
} else {
|
|
7358
|
+
setByPath(layerInto, relativePath, descValue);
|
|
7359
|
+
}
|
|
7360
|
+
}
|
|
7361
|
+
}
|
|
7362
|
+
for (const k of descendants) delete accumulated[k];
|
|
7363
|
+
accumulated[newPath] = newValue;
|
|
7364
|
+
}
|
|
7365
|
+
function mergeDirtyChanges(accumulated, newChanges) {
|
|
7366
|
+
for (const path of Object.keys(newChanges)) {
|
|
7367
|
+
mergeDirtyPath(accumulated, path, newChanges[path]);
|
|
7368
|
+
}
|
|
7369
|
+
}
|
|
7370
|
+
|
|
7371
|
+
// src/db/DexieDb.ts
|
|
7176
7372
|
var SYNC_META_TABLE = "_sync_meta";
|
|
7177
7373
|
var DIRTY_CHANGES_TABLE = "_dirty_changes";
|
|
7178
7374
|
var META_ONLY_DIRTY_KEYS = /* @__PURE__ */ new Set([
|