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/dist/index.js CHANGED
@@ -288,80 +288,107 @@ function applyQueryOpts(items, opts) {
288
288
  return result;
289
289
  }
290
290
 
291
- // src/utils/computeDiff.ts
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") return false;
297
- if (Array.isArray(v)) return false;
298
- if (v instanceof Date) return false;
299
- if (isObjectIdLike(v)) return false;
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) return true;
305
- if (a === null || b === null) return false;
306
- if (a === void 0 || b === void 0) return false;
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) return false;
314
- if (typeof a !== "object") return false;
315
- if (Array.isArray(a) !== Array.isArray(b)) return false;
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) return false;
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])) return false;
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) return false;
337
+ if (ak.length !== bk.length)
338
+ return false;
326
339
  for (const k of ak) {
327
- if (!Object.prototype.hasOwnProperty.call(b, k)) return false;
328
- if (!deepEquals(a[k], b[k])) return false;
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) return false;
348
+ if (arr.length === 0)
349
+ return false;
334
350
  for (const e of arr) {
335
- if (!e || typeof e !== "object") return false;
336
- if (e._id == null) return false;
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) return false;
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)) return false;
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") return false;
349
- if (value instanceof Date || isObjectIdLike(value)) return false;
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)) return true;
373
+ if (value.length > 0 && allElementsHaveId(value))
374
+ return true;
352
375
  for (const v of value) {
353
- if (containsIdArrayDescendant(v)) return true;
376
+ if (containsIdArrayDescendant(v))
377
+ return true;
354
378
  }
355
379
  return false;
356
380
  }
357
- if (!isPlainObject(value)) return false;
381
+ if (!isPlainObject(value))
382
+ return false;
358
383
  for (const key of Object.keys(value)) {
359
- if (containsIdArrayDescendant(value[key])) return true;
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) return;
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) existingIds.add(String(e._id));
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) updateIds.add(String(u._id));
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) existingById.set(String(e._id), e);
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)) return;
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 computeDiff(existing, update) {
494
+ function computeObjDiff(existing, update) {
469
495
  const diff = {};
470
- if (!update || typeof update !== "object") return diff;
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)) continue;
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)) continue;
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) out.push(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) return false;
564
- if (typeof value !== "object") return false;
565
- if (Array.isArray(value)) return false;
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) return 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)) return void 0;
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) return false;
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)) return false;
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) current[idx] = value[0];
592
- else current.push(value[0]);
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) return false;
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 deleteByPath(target2, path) {
610
- if (target2 === null || target2 === void 0) return false;
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
- if (parts.length === 0) return false;
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 next = navigateSegment(current, parts[i]);
616
- if (next === void 0 || next === null) return false;
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
- return deleteSegment(current, last);
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) return false;
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)) return false;
644
+ if (!Array.isArray(current))
645
+ return false;
627
646
  const idx = current.findIndex((item) => item && String(item._id) === idStr);
628
- if (idx < 0) return false;
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) return false;
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)) return false;
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 isTerminalBracketKey(path) {
646
- const tokens = tokenizePath(path);
647
- const last = tokens[tokens.length - 1];
648
- return !!(last && last.length >= 2 && last.charCodeAt(0) === 91 && last.charCodeAt(last.length - 1) === 93);
649
- }
650
- function canExpandArrayToBrackets(value) {
651
- if (!Array.isArray(value) || value.length === 0) return false;
652
- for (const el of value) {
653
- if (el === null || typeof el !== "object") return false;
654
- if (el._id == null) return false;
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
- return true;
680
+ const last = parts[parts.length - 1];
681
+ return deleteSegment(current, last);
657
682
  }
658
- function pickLayerTarget(newPath, newValue) {
659
- if (!isTerminalBracketKey(newPath)) return null;
660
- if (newValue === void 0) return null;
661
- if (Array.isArray(newValue) && newValue.length === 1 && newValue[0] && typeof newValue[0] === "object") {
662
- return newValue[0];
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
- if (newValue && typeof newValue === "object" && !Array.isArray(newValue)) {
665
- return newValue;
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 null;
705
+ return out;
668
706
  }
669
- function mergeDirtyPath(accumulated, newPath, newValue) {
670
- for (const existingKey of Object.keys(accumulated)) {
671
- if (existingKey === newPath) continue;
672
- if (isDescendantOrEqual(newPath, existingKey)) {
673
- const existingValue = accumulated[existingKey];
674
- const existingIsTerminal = isTerminalBracketKey(existingKey);
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
- const descendants = [];
704
- for (const existingKey of Object.keys(accumulated)) {
705
- if (existingKey === newPath) continue;
706
- if (isDescendantOrEqual(existingKey, newPath)) {
707
- descendants.push(existingKey);
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
- const layerInto = pickLayerTarget(newPath, newValue);
711
- if (layerInto && descendants.length > 0) {
712
- for (const desc of descendants) {
713
- const sepChar = desc[newPath.length];
714
- const relativePath = sepChar === "[" ? desc.substring(newPath.length) : desc.substring(newPath.length + 1);
715
- const descValue = accumulated[desc];
716
- if (descValue === void 0) {
717
- deleteByPath(layerInto, relativePath);
718
- } else {
719
- setByPath(layerInto, relativePath, descValue);
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 mergeDirtyChanges(accumulated, newChanges) {
727
- for (const path of Object.keys(newChanges)) {
728
- mergeDirtyPath(accumulated, path, newChanges[path]);
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(candidate);
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 { _id: item._id, update: changes };
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 = computeDiff(existing, fullChanges);
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 = _SyncedDb.applyDiffLocally(
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 `computeDiff` →
5864
- * `applyDiffLocally` → in-mem write + Dexie schedule + dirty change.
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
- const items = await this.loadCollectionToInMem(name);
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 = _SyncedDb.applyDiffLocally(
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([