@korajs/core 1.0.0-beta.12 → 1.0.0-beta.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -13
- package/dist/bindings/index.d.cts +26 -6
- package/dist/bindings/index.d.ts +26 -6
- package/dist/{build-scope-map-CItP1gsz.d.cts → build-scope-map-C9zBTJ1R.d.cts} +1 -1
- package/dist/{build-scope-map-Cv5HfCQP.d.ts → build-scope-map-CZaMyZLV.d.ts} +1 -1
- package/dist/{chunk-5IICSH6H.js → chunk-QUD3DMSD.js} +668 -24
- package/dist/chunk-QUD3DMSD.js.map +1 -0
- package/dist/events-Da29Dt_3.d.cts +1737 -0
- package/dist/events-Da29Dt_3.d.ts +1737 -0
- package/dist/index.cjs +2548 -207
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +810 -220
- package/dist/index.d.ts +810 -220
- package/dist/index.js +2028 -334
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs +857 -85
- package/dist/internal.cjs.map +1 -1
- package/dist/internal.d.cts +251 -21
- package/dist/internal.d.ts +251 -21
- package/dist/internal.js +447 -1
- package/dist/internal.js.map +1 -1
- package/dist/sql-gen-BOeuiyqu.d.cts +622 -0
- package/dist/sql-gen-CXTA6w-w.d.ts +622 -0
- package/package.json +2 -1
- package/dist/chunk-5IICSH6H.js.map +0 -1
- package/dist/events-D7JCtQSD.d.cts +0 -1010
- package/dist/events-D7JCtQSD.d.ts +0 -1010
- package/dist/operation-BX4ok7U4.d.ts +0 -164
- package/dist/operation-BXvJ6RiA.d.cts +0 -164
package/dist/index.js
CHANGED
|
@@ -1,23 +1,51 @@
|
|
|
1
1
|
import {
|
|
2
2
|
AppNotReadyError,
|
|
3
3
|
ClockDriftError,
|
|
4
|
+
DEFAULT_OPERATION_HASH_VERSION,
|
|
4
5
|
HybridLogicalClock,
|
|
5
6
|
InvalidTimestampError,
|
|
6
7
|
KORA_ERROR_FIX_SUGGESTIONS,
|
|
7
8
|
KoraError,
|
|
8
9
|
MAX_LOGICAL,
|
|
9
10
|
MergeConflictError,
|
|
11
|
+
NonCanonicalValueError,
|
|
10
12
|
OperationError,
|
|
13
|
+
OperationTooLargeError,
|
|
11
14
|
RemoteClockDriftError,
|
|
12
15
|
SchemaValidationError,
|
|
13
16
|
StorageError,
|
|
14
17
|
SyncError,
|
|
18
|
+
base64ToBytes,
|
|
19
|
+
bytesToBase64,
|
|
20
|
+
canonicalValue,
|
|
21
|
+
canonicalize,
|
|
22
|
+
canonicalizeLegacyOperation,
|
|
23
|
+
canonicalizeOperationBody,
|
|
24
|
+
canonicalizeProvenLegacyClear,
|
|
25
|
+
collectionIndexName,
|
|
26
|
+
compareStamps,
|
|
27
|
+
computeOperationId,
|
|
15
28
|
createOperation,
|
|
29
|
+
decodeBytesFromOpData,
|
|
30
|
+
encodeBytesForOpData,
|
|
31
|
+
generateFullDDL,
|
|
32
|
+
generateSQL,
|
|
16
33
|
getKoraErrorFix,
|
|
34
|
+
isAfter,
|
|
35
|
+
isKoraBytesValue,
|
|
36
|
+
isLegacyNumericByteObject,
|
|
17
37
|
isValidOperation,
|
|
38
|
+
maxStamp,
|
|
39
|
+
minStamp,
|
|
40
|
+
planField,
|
|
41
|
+
quoteIdent,
|
|
42
|
+
sqlDefaultLiteral,
|
|
43
|
+
stampOf,
|
|
44
|
+
stampTimestamp,
|
|
18
45
|
topologicalSort,
|
|
46
|
+
verifyOperationId,
|
|
19
47
|
verifyOperationIntegrity
|
|
20
|
-
} from "./chunk-
|
|
48
|
+
} from "./chunk-QUD3DMSD.js";
|
|
21
49
|
|
|
22
50
|
// src/types.ts
|
|
23
51
|
var MERGE_STRATEGIES = [
|
|
@@ -463,6 +491,44 @@ function toAtomicOp(sentinel) {
|
|
|
463
491
|
}
|
|
464
492
|
|
|
465
493
|
// src/operations/replay-record.ts
|
|
494
|
+
function mergeArraySet(local, remote, base) {
|
|
495
|
+
const serialize = (v) => JSON.stringify(v);
|
|
496
|
+
const baseSet = new Set(base.map(serialize));
|
|
497
|
+
const localSet = new Set(local.map(serialize));
|
|
498
|
+
const remoteSet = new Set(remote.map(serialize));
|
|
499
|
+
const keep = (s) => localSet.has(s) && remoteSet.has(s) || !baseSet.has(s) && (localSet.has(s) || remoteSet.has(s));
|
|
500
|
+
const seen = /* @__PURE__ */ new Set();
|
|
501
|
+
const result = [];
|
|
502
|
+
const addIfKept = (serialized, value) => {
|
|
503
|
+
if (!seen.has(serialized) && keep(serialized)) {
|
|
504
|
+
seen.add(serialized);
|
|
505
|
+
result.push(value);
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
for (const item of base) {
|
|
509
|
+
addIfKept(serialize(item), item);
|
|
510
|
+
}
|
|
511
|
+
const others = /* @__PURE__ */ new Map();
|
|
512
|
+
for (const item of [...local, ...remote]) {
|
|
513
|
+
const s = serialize(item);
|
|
514
|
+
if (!baseSet.has(s) && !others.has(s)) {
|
|
515
|
+
others.set(s, item);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
for (const s of [...others.keys()].sort()) {
|
|
519
|
+
addIfKept(s, others.get(s));
|
|
520
|
+
}
|
|
521
|
+
return result;
|
|
522
|
+
}
|
|
523
|
+
function resolveArrayWrite(running, value, base) {
|
|
524
|
+
if (!Array.isArray(running) || !Array.isArray(value) || !Array.isArray(base)) {
|
|
525
|
+
return value;
|
|
526
|
+
}
|
|
527
|
+
if (canonicalize(running) === canonicalize(base)) {
|
|
528
|
+
return value;
|
|
529
|
+
}
|
|
530
|
+
return mergeArraySet(running, value, base);
|
|
531
|
+
}
|
|
466
532
|
function replayOperationsForRecord(ops) {
|
|
467
533
|
let record = null;
|
|
468
534
|
let deleted = false;
|
|
@@ -485,6 +551,8 @@ function replayOperationsForRecord(ops) {
|
|
|
485
551
|
const atomicOp = op2.atomicOps?.[field];
|
|
486
552
|
if (atomicOp && lastWriterKind[field] === `atomic:${atomicOp.type}`) {
|
|
487
553
|
next[field] = applyAtomicOp(next[field], atomicOp);
|
|
554
|
+
} else if (!atomicOp && op2.previousData && field in op2.previousData) {
|
|
555
|
+
next[field] = resolveArrayWrite(next[field], value, op2.previousData[field]);
|
|
488
556
|
} else {
|
|
489
557
|
next[field] = value;
|
|
490
558
|
}
|
|
@@ -501,105 +569,1565 @@ function replayOperationsForRecord(ops) {
|
|
|
501
569
|
}
|
|
502
570
|
return deleted ? null : record;
|
|
503
571
|
}
|
|
572
|
+
function replayFieldVersionsForRecord(ops) {
|
|
573
|
+
let fields = null;
|
|
574
|
+
let deleted = false;
|
|
575
|
+
const first = ops[0];
|
|
576
|
+
const last = ops[ops.length - 1];
|
|
577
|
+
if (!first || !last) return null;
|
|
578
|
+
for (const op2 of ops) {
|
|
579
|
+
switch (op2.type) {
|
|
580
|
+
case "insert":
|
|
581
|
+
if (op2.data) {
|
|
582
|
+
fields = {};
|
|
583
|
+
for (const field of Object.keys(op2.data)) fields[field] = { ...op2.timestamp };
|
|
584
|
+
deleted = false;
|
|
585
|
+
}
|
|
586
|
+
break;
|
|
587
|
+
case "update":
|
|
588
|
+
if (op2.data) {
|
|
589
|
+
const next = { ...fields ?? {} };
|
|
590
|
+
for (const field of Object.keys(op2.data)) next[field] = { ...op2.timestamp };
|
|
591
|
+
fields = next;
|
|
592
|
+
deleted = false;
|
|
593
|
+
}
|
|
594
|
+
break;
|
|
595
|
+
case "delete":
|
|
596
|
+
deleted = true;
|
|
597
|
+
break;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
if (deleted || fields === null) return null;
|
|
601
|
+
return { fields, created: { ...first.timestamp }, latest: { ...last.timestamp } };
|
|
602
|
+
}
|
|
603
|
+
function expandFieldVersionedOperations(ops) {
|
|
604
|
+
const expanded = [];
|
|
605
|
+
for (const op2 of ops) {
|
|
606
|
+
if (op2.type !== "insert" || !op2.fieldVersions || !op2.data) {
|
|
607
|
+
expanded.push(op2);
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
const { fieldVersions, ...base } = op2;
|
|
611
|
+
expanded.push(base);
|
|
612
|
+
const groups = /* @__PURE__ */ new Map();
|
|
613
|
+
let newest = op2.timestamp;
|
|
614
|
+
for (const v of Object.values(fieldVersions)) {
|
|
615
|
+
if (HybridLogicalClock.compare(v, newest) > 0) newest = v;
|
|
616
|
+
}
|
|
617
|
+
for (const [field, value] of Object.entries(op2.data)) {
|
|
618
|
+
const version = fieldVersions[field] ?? newest;
|
|
619
|
+
if (HybridLogicalClock.compare(version, op2.timestamp) <= 0) continue;
|
|
620
|
+
const key = `${version.wallTime}:${version.logical}:${version.nodeId}`;
|
|
621
|
+
const group = groups.get(key) ?? { timestamp: version, data: {} };
|
|
622
|
+
group.data[field] = value;
|
|
623
|
+
groups.set(key, group);
|
|
624
|
+
}
|
|
625
|
+
let index = 0;
|
|
626
|
+
for (const group of groups.values()) {
|
|
627
|
+
expanded.push({
|
|
628
|
+
...base,
|
|
629
|
+
id: `${op2.id}#field-version-${index++}`,
|
|
630
|
+
type: "update",
|
|
631
|
+
data: group.data,
|
|
632
|
+
previousData: null,
|
|
633
|
+
timestamp: { ...group.timestamp }
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
return expanded.sort((a, b) => HybridLogicalClock.compare(a.timestamp, b.timestamp));
|
|
638
|
+
}
|
|
504
639
|
|
|
505
|
-
// src/
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
640
|
+
// src/migration/apply-operation-transforms.ts
|
|
641
|
+
var MAX_TRANSFORM_STEPS = 32;
|
|
642
|
+
function applyOperationTransforms(operation, targetSchemaVersion, transforms) {
|
|
643
|
+
let current = operation;
|
|
644
|
+
for (let step = 0; step < MAX_TRANSFORM_STEPS; step++) {
|
|
645
|
+
if (current.schemaVersion === targetSchemaVersion) {
|
|
646
|
+
return current;
|
|
647
|
+
}
|
|
648
|
+
const transform = transforms.find(
|
|
649
|
+
(candidate) => candidate.fromVersion === current.schemaVersion
|
|
650
|
+
);
|
|
651
|
+
if (!transform) {
|
|
652
|
+
return null;
|
|
653
|
+
}
|
|
654
|
+
const next = transform.transform(current);
|
|
655
|
+
if (next === null) {
|
|
656
|
+
return null;
|
|
657
|
+
}
|
|
658
|
+
current = next;
|
|
509
659
|
}
|
|
510
|
-
|
|
511
|
-
return Object.keys(record).length === 1 && typeof record.$koraBytes === "string";
|
|
660
|
+
return null;
|
|
512
661
|
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
662
|
+
|
|
663
|
+
// src/migration/operation-view.ts
|
|
664
|
+
var OperationTransformError = class extends KoraError {
|
|
665
|
+
constructor(message, context) {
|
|
666
|
+
super(message, "INVALID_OPERATION_TRANSFORM", {
|
|
667
|
+
...context,
|
|
668
|
+
fix: "An OperationTransform may rewrite data, previousData, atomicOps and schemaVersion only, deterministically, with JSON values."
|
|
669
|
+
});
|
|
670
|
+
this.name = "OperationTransformError";
|
|
516
671
|
}
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
if (
|
|
520
|
-
|
|
672
|
+
};
|
|
673
|
+
function operationSchemaView(op2, targetSchemaVersion, transforms) {
|
|
674
|
+
if (op2.schemaVersion === targetSchemaVersion) return op2;
|
|
675
|
+
if (transforms === void 0 || transforms.length === 0) return op2;
|
|
676
|
+
if (op2.encrypted !== void 0) return op2;
|
|
677
|
+
let view;
|
|
678
|
+
try {
|
|
679
|
+
view = applyOperationTransforms(op2, targetSchemaVersion, transforms);
|
|
680
|
+
} catch (error) {
|
|
681
|
+
throw new OperationTransformError(
|
|
682
|
+
`The schema transform of operation ${op2.id} (schema v${op2.schemaVersion} -> v${targetSchemaVersion}) threw: ${error instanceof Error ? error.message : String(error)}`,
|
|
683
|
+
{ operationId: op2.id, fromVersion: op2.schemaVersion, toVersion: targetSchemaVersion }
|
|
684
|
+
);
|
|
521
685
|
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
if (
|
|
525
|
-
|
|
686
|
+
if (view === null || view === op2) return view;
|
|
687
|
+
for (const key of ["id", "nodeId", "type", "collection", "recordId"]) {
|
|
688
|
+
if (view[key] !== op2[key]) {
|
|
689
|
+
throw new OperationTransformError(
|
|
690
|
+
`The schema transform of operation ${op2.id} changed its ${key} ("${String(op2[key])}" -> "${String(view[key])}"). A transform may rewrite data, previousData, atomicOps and schemaVersion only.`,
|
|
691
|
+
{ operationId: op2.id, field: key }
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
const a = op2.timestamp;
|
|
696
|
+
const b = view.timestamp;
|
|
697
|
+
if (a.wallTime !== b.wallTime || a.logical !== b.logical || a.nodeId !== b.nodeId) {
|
|
698
|
+
throw new OperationTransformError(
|
|
699
|
+
`The schema transform of operation ${op2.id} changed its timestamp. A transform may rewrite data, previousData, atomicOps and schemaVersion only.`,
|
|
700
|
+
{ operationId: op2.id, field: "timestamp" }
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
try {
|
|
704
|
+
return canonicalizeOperationBody(view);
|
|
705
|
+
} catch (error) {
|
|
706
|
+
throw new OperationTransformError(
|
|
707
|
+
`The schema transform of operation ${op2.id} produced a value with no JSON form: ${error instanceof Error ? error.message : String(error)}`,
|
|
708
|
+
{ operationId: op2.id }
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
function hashText(text) {
|
|
713
|
+
let hash = 2166136261;
|
|
714
|
+
for (let i = 0; i < text.length; i++) {
|
|
715
|
+
hash ^= text.charCodeAt(i);
|
|
716
|
+
hash = Math.imul(hash, 16777619) >>> 0;
|
|
717
|
+
}
|
|
718
|
+
return hash.toString(16).padStart(8, "0");
|
|
719
|
+
}
|
|
720
|
+
function operationTransformsFingerprint(targetSchemaVersion, transforms) {
|
|
721
|
+
if (transforms === void 0 || transforms.length === 0) return "";
|
|
722
|
+
const parts = transforms.map((t2) => `${t2.fromVersion}>${t2.toVersion}:${hashText(String(t2.transform))}`).sort();
|
|
723
|
+
return `xf@v${targetSchemaVersion}(${parts.join(",")})`;
|
|
724
|
+
}
|
|
725
|
+
var OperationTransformCoverageError = class extends KoraError {
|
|
726
|
+
constructor(message, versions, targetSchemaVersion, context = {}) {
|
|
727
|
+
super(message, "OPERATION_TRANSFORM_MISSING", {
|
|
728
|
+
...context,
|
|
729
|
+
versions: [...versions],
|
|
730
|
+
targetSchemaVersion,
|
|
731
|
+
fix: `Keep a transform path registered from every schema version in the operation log to v${targetSchemaVersion} (a transform must stay registered as long as operations of its source version exist, and the log is append-only), or register no transforms at all (operations then fold as written).`
|
|
732
|
+
});
|
|
733
|
+
this.versions = versions;
|
|
734
|
+
this.targetSchemaVersion = targetSchemaVersion;
|
|
735
|
+
this.name = "OperationTransformCoverageError";
|
|
736
|
+
}
|
|
737
|
+
versions;
|
|
738
|
+
targetSchemaVersion;
|
|
739
|
+
};
|
|
740
|
+
function missingTransformPaths(storedVersions, targetSchemaVersion, transforms) {
|
|
741
|
+
if (transforms === void 0 || transforms.length === 0) return [];
|
|
742
|
+
const missing = [];
|
|
743
|
+
for (const version of new Set(storedVersions)) {
|
|
744
|
+
if (version === targetSchemaVersion) continue;
|
|
745
|
+
let current = version;
|
|
746
|
+
let reached = false;
|
|
747
|
+
for (let step = 0; step < 32; step++) {
|
|
748
|
+
if (current === targetSchemaVersion) {
|
|
749
|
+
reached = true;
|
|
750
|
+
break;
|
|
751
|
+
}
|
|
752
|
+
const next = transforms.find((candidate) => candidate.fromVersion === current);
|
|
753
|
+
if (!next) break;
|
|
754
|
+
current = next.toVersion;
|
|
755
|
+
}
|
|
756
|
+
if (!reached) missing.push({ version, missingFrom: current });
|
|
757
|
+
}
|
|
758
|
+
return missing.sort((a, b) => a.version - b.version);
|
|
759
|
+
}
|
|
760
|
+
function assertOperationTransformCoverage(storedVersions, targetSchemaVersion, transforms, replica) {
|
|
761
|
+
const missing = missingTransformPaths(storedVersions, targetSchemaVersion, transforms);
|
|
762
|
+
if (missing.length === 0) return;
|
|
763
|
+
const versions = missing.map((entry) => entry.version);
|
|
764
|
+
const steps = [...new Set(missing.map((entry) => entry.missingFrom))];
|
|
765
|
+
const plural = versions.length > 1;
|
|
766
|
+
throw new OperationTransformCoverageError(
|
|
767
|
+
`The ${replica} holds operations of schema version${plural ? "s" : ""} ${versions.map((v) => `v${v}`).join(", ")}, but the registered operationTransforms have no path from ${plural ? "them" : "it"} to the current schema v${targetSchemaVersion} (no transform from ${steps.map((v) => `v${v}`).join(", ")}). Starting would silently drop those operations from their records, so it is refused. Keep the retired transform(s) registered: a transform may be removed only after every operation of its source version has been folded into snapshots by an explicit compaction.`,
|
|
768
|
+
versions,
|
|
769
|
+
targetSchemaVersion,
|
|
770
|
+
{ replica, missingFrom: steps }
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// src/fold/authority.ts
|
|
775
|
+
var RESERVED_NODE_ID_PREFIX = "kora:";
|
|
776
|
+
var SERVER_NODE_ID_PREFIX = "kora:server:";
|
|
777
|
+
function isReservedNodeId(nodeId) {
|
|
778
|
+
return nodeId.startsWith(RESERVED_NODE_ID_PREFIX);
|
|
779
|
+
}
|
|
780
|
+
function isServerNodeId(nodeId) {
|
|
781
|
+
return nodeId.startsWith(SERVER_NODE_ID_PREFIX) && nodeId.length > SERVER_NODE_ID_PREFIX.length;
|
|
782
|
+
}
|
|
783
|
+
function isAuthoritativeNodeId(nodeId, explicit) {
|
|
784
|
+
return isServerNodeId(nodeId) || (explicit?.has(nodeId) ?? false);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// src/fold/errors.ts
|
|
788
|
+
var FoldStateError = class extends KoraError {
|
|
789
|
+
constructor(message, context) {
|
|
790
|
+
super(message, "FOLD_STATE_INVALID", {
|
|
791
|
+
fix: "Rebuild the record state from its operation log with foldRecord(ops, schema).",
|
|
792
|
+
...context
|
|
793
|
+
});
|
|
794
|
+
this.name = "FoldStateError";
|
|
795
|
+
}
|
|
796
|
+
};
|
|
797
|
+
var FoldConfigurationError = class extends KoraError {
|
|
798
|
+
constructor(message, context) {
|
|
799
|
+
super(message, "FOLD_CONFIGURATION", context);
|
|
800
|
+
this.name = "FoldConfigurationError";
|
|
801
|
+
}
|
|
802
|
+
};
|
|
803
|
+
|
|
804
|
+
// src/fold/values.ts
|
|
805
|
+
function isPlainObject(value) {
|
|
806
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
807
|
+
const proto = Object.getPrototypeOf(value);
|
|
808
|
+
return proto === null || proto === Object.prototype;
|
|
809
|
+
}
|
|
810
|
+
function normalizeValue(value) {
|
|
811
|
+
if (value instanceof Uint8Array) return { $koraBytes: bytesToBase64(value) };
|
|
812
|
+
if (value instanceof ArrayBuffer) return { $koraBytes: bytesToBase64(new Uint8Array(value)) };
|
|
813
|
+
if (Array.isArray(value)) return value.map(normalizeValue);
|
|
814
|
+
if (isPlainObject(value)) {
|
|
815
|
+
const out = {};
|
|
816
|
+
for (const [key, member] of Object.entries(value)) {
|
|
817
|
+
if (member !== void 0) out[key] = normalizeValue(member);
|
|
818
|
+
}
|
|
819
|
+
return out;
|
|
820
|
+
}
|
|
821
|
+
return value;
|
|
822
|
+
}
|
|
823
|
+
function canonicalKey(value) {
|
|
824
|
+
return canonicalize(normalizeValue(value));
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// src/fold/field-states.ts
|
|
828
|
+
function emptyField(plan) {
|
|
829
|
+
switch (plan.kind) {
|
|
830
|
+
case "reg":
|
|
831
|
+
return { k: "reg", e: [], val: void 0 };
|
|
832
|
+
case "res":
|
|
833
|
+
return { k: "res", e: [], val: void 0 };
|
|
834
|
+
case "set":
|
|
835
|
+
return { k: "set", ao: plan.appendOnly, sh: null, clr: null, el: {} };
|
|
836
|
+
case "map":
|
|
837
|
+
return { k: "map", sh: null, clr: null, keys: {} };
|
|
838
|
+
case "ctr":
|
|
839
|
+
return { k: "ctr", base: null, d: [], val: void 0 };
|
|
840
|
+
case "max":
|
|
841
|
+
case "min":
|
|
842
|
+
return { k: plan.kind, best: null, reg: null };
|
|
843
|
+
case "rt":
|
|
844
|
+
return { k: "rt", reset: null, u: {} };
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
function entryKind(entry) {
|
|
848
|
+
return entry.a ? entry.a.type : "set";
|
|
849
|
+
}
|
|
850
|
+
function chainFold(entries) {
|
|
851
|
+
let value;
|
|
852
|
+
let last = null;
|
|
853
|
+
for (const entry of entries) {
|
|
854
|
+
value = entry.a && last === entry.a.type ? applyAtomicOp(value, entry.a) : entry.v;
|
|
855
|
+
last = entryKind(entry);
|
|
856
|
+
}
|
|
857
|
+
return value;
|
|
858
|
+
}
|
|
859
|
+
function insertionIndex(entries, stamp) {
|
|
860
|
+
let lo = 0;
|
|
861
|
+
let hi = entries.length;
|
|
862
|
+
while (lo < hi) {
|
|
863
|
+
const mid = lo + hi >>> 1;
|
|
864
|
+
const entry = entries[mid];
|
|
865
|
+
const cmp = compareStamps(entry.s, stamp);
|
|
866
|
+
if (cmp === 0) return -1;
|
|
867
|
+
if (cmp < 0) lo = mid + 1;
|
|
868
|
+
else hi = mid;
|
|
869
|
+
}
|
|
870
|
+
return lo;
|
|
871
|
+
}
|
|
872
|
+
function pruneChain(entries) {
|
|
873
|
+
for (let i = entries.length - 1; i > 0; i--) {
|
|
874
|
+
if (!entries[i].a) return entries.slice(i);
|
|
875
|
+
}
|
|
876
|
+
return entries;
|
|
877
|
+
}
|
|
878
|
+
function toEntry(write) {
|
|
879
|
+
return write.a ? { s: write.s, v: write.v, a: write.a } : { s: write.s, v: write.v };
|
|
880
|
+
}
|
|
881
|
+
function applyRegister(state, write) {
|
|
882
|
+
const entries = state.e;
|
|
883
|
+
const index = insertionIndex(entries, write.s);
|
|
884
|
+
if (index === -1) return { state, changed: false };
|
|
885
|
+
const first = entries[0];
|
|
886
|
+
if (index === 0 && first !== void 0 && !first.a) return { state, changed: false };
|
|
887
|
+
const entry = toEntry(write);
|
|
888
|
+
if (index === entries.length) {
|
|
889
|
+
if (!entry.a) return { state: { k: "reg", e: [entry], val: entry.v }, changed: true };
|
|
890
|
+
const last = entries[entries.length - 1];
|
|
891
|
+
const val = last !== void 0 && entryKind(last) === entry.a.type ? applyAtomicOp(state.val, entry.a) : entry.v;
|
|
892
|
+
return { state: { k: "reg", e: [...entries, entry], val }, changed: true };
|
|
893
|
+
}
|
|
894
|
+
const next = pruneChain([...entries.slice(0, index), entry, ...entries.slice(index)]);
|
|
895
|
+
return { state: { k: "reg", e: next, val: chainFold(next) }, changed: true };
|
|
896
|
+
}
|
|
897
|
+
function mergeLogs(a, b) {
|
|
898
|
+
const out = [];
|
|
899
|
+
let i = 0;
|
|
900
|
+
let j = 0;
|
|
901
|
+
while (i < a.length || j < b.length) {
|
|
902
|
+
const x = a[i];
|
|
903
|
+
const y = b[j];
|
|
904
|
+
if (y === void 0 || x !== void 0 && compareStamps(x.s, y.s) < 0) {
|
|
905
|
+
out.push(x);
|
|
906
|
+
i++;
|
|
907
|
+
} else if (x === void 0 || compareStamps(x.s, y.s) > 0) {
|
|
908
|
+
out.push(y);
|
|
909
|
+
j++;
|
|
910
|
+
} else {
|
|
911
|
+
out.push(x);
|
|
912
|
+
i++;
|
|
913
|
+
j++;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
return out;
|
|
917
|
+
}
|
|
918
|
+
function resolveStep(plan, current, entry) {
|
|
919
|
+
try {
|
|
920
|
+
return { value: canonicalValue(plan.resolver?.(current, entry.v, entry.b), "resolver") };
|
|
921
|
+
} catch (error) {
|
|
922
|
+
return { value: entry.v, error: error instanceof Error ? error.message : String(error) };
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
function resolverFold(plan, entries) {
|
|
926
|
+
let value;
|
|
927
|
+
let error;
|
|
928
|
+
entries.forEach((entry, index) => {
|
|
929
|
+
if (index === 0) {
|
|
930
|
+
value = entry.v;
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
const step = resolveStep(plan, value, entry);
|
|
934
|
+
value = step.value;
|
|
935
|
+
if (step.error !== void 0) error = step.error;
|
|
936
|
+
});
|
|
937
|
+
return error === void 0 ? { value } : { value, error };
|
|
938
|
+
}
|
|
939
|
+
function pruneResolverLog(entries) {
|
|
940
|
+
for (let i = entries.length - 1; i > 0; i--) {
|
|
941
|
+
if (entries[i].z === 1) return entries.slice(i);
|
|
942
|
+
}
|
|
943
|
+
return entries;
|
|
944
|
+
}
|
|
945
|
+
function resolverState(entries, result) {
|
|
946
|
+
return result.error === void 0 ? { k: "res", e: entries, val: result.value } : { k: "res", e: entries, val: result.value, err: result.error };
|
|
947
|
+
}
|
|
948
|
+
function applyResolver(state, write, plan) {
|
|
949
|
+
const index = insertionIndex(state.e, write.s);
|
|
950
|
+
if (index === -1) return { state, changed: false };
|
|
951
|
+
if (index === 0 && state.e[0]?.z === 1) return { state, changed: false };
|
|
952
|
+
const entry = { s: write.s, v: write.v };
|
|
953
|
+
if (write.insert) entry.b = null;
|
|
954
|
+
else if (write.hasPrev) entry.b = write.prev;
|
|
955
|
+
if (index === state.e.length) {
|
|
956
|
+
const entries2 = [...state.e, entry];
|
|
957
|
+
if (state.e.length === 0)
|
|
958
|
+
return { state: resolverState(entries2, { value: entry.v }), changed: true };
|
|
959
|
+
const step = resolveStep(plan, state.val, entry);
|
|
960
|
+
const next = step.error !== void 0 ? { k: "res", e: entries2, val: step.value, err: step.error } : state.err !== void 0 ? { k: "res", e: entries2, val: step.value, err: state.err } : { k: "res", e: entries2, val: step.value };
|
|
961
|
+
return {
|
|
962
|
+
state: next,
|
|
963
|
+
changed: true,
|
|
964
|
+
...step.error !== void 0 ? { error: step.error } : {}
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
const entries = [...state.e.slice(0, index), entry, ...state.e.slice(index)];
|
|
968
|
+
const result = resolverFold(plan, entries);
|
|
969
|
+
return { state: resolverState(entries, result), changed: true };
|
|
970
|
+
}
|
|
971
|
+
function laterShape(current, next) {
|
|
972
|
+
return current === null || compareStamps(next.s, current.s) > 0 ? next : null;
|
|
973
|
+
}
|
|
974
|
+
function firstAddBefore(a, b) {
|
|
975
|
+
if (a === null) return true;
|
|
976
|
+
const cmp = compareStamps(b.s, a.s);
|
|
977
|
+
return cmp < 0 || cmp === 0 && b.i < a.i;
|
|
978
|
+
}
|
|
979
|
+
function elementKey(canonicalValue2, occurrence) {
|
|
980
|
+
return `${canonicalValue2}#${occurrence}`;
|
|
981
|
+
}
|
|
982
|
+
function elementWriter(base) {
|
|
983
|
+
let el = base;
|
|
984
|
+
let changed = false;
|
|
985
|
+
const read = (value, occurrence) => {
|
|
986
|
+
const canonical = canonicalKey(value);
|
|
987
|
+
const key = elementKey(canonical, occurrence);
|
|
988
|
+
const existing = el[key];
|
|
989
|
+
return [
|
|
990
|
+
key,
|
|
991
|
+
existing ? { ...existing } : { v: JSON.parse(canonical), n: occurrence, a: null, f: null, r: null }
|
|
992
|
+
];
|
|
993
|
+
};
|
|
994
|
+
const store = (key, element) => {
|
|
995
|
+
if (el === base) el = { ...base };
|
|
996
|
+
el[key] = element;
|
|
997
|
+
changed = true;
|
|
998
|
+
};
|
|
999
|
+
return {
|
|
1000
|
+
add(value, occurrence, stamp, index) {
|
|
1001
|
+
const [key, element] = read(value, occurrence);
|
|
1002
|
+
let touched = false;
|
|
1003
|
+
if (isAfter(stamp, element.a)) {
|
|
1004
|
+
element.a = stamp;
|
|
1005
|
+
touched = true;
|
|
1006
|
+
}
|
|
1007
|
+
const first = { s: stamp, i: index };
|
|
1008
|
+
if (firstAddBefore(element.f, first)) {
|
|
1009
|
+
element.f = first;
|
|
1010
|
+
touched = true;
|
|
1011
|
+
}
|
|
1012
|
+
if (touched) store(key, element);
|
|
1013
|
+
},
|
|
1014
|
+
remove(value, occurrence, stamp) {
|
|
1015
|
+
const [key, element] = read(value, occurrence);
|
|
1016
|
+
if (isAfter(stamp, element.r)) {
|
|
1017
|
+
element.r = stamp;
|
|
1018
|
+
store(key, element);
|
|
1019
|
+
}
|
|
1020
|
+
},
|
|
1021
|
+
result: () => ({ el, changed })
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
function countOccurrences(items) {
|
|
1025
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1026
|
+
const occurrences = items.map((item) => {
|
|
1027
|
+
const canonical = canonicalKey(item);
|
|
1028
|
+
const k = counts.get(canonical) ?? 0;
|
|
1029
|
+
counts.set(canonical, k + 1);
|
|
1030
|
+
return { item, canonical, k };
|
|
1031
|
+
});
|
|
1032
|
+
return { counts, occurrences };
|
|
1033
|
+
}
|
|
1034
|
+
function applyElementSet(state, write) {
|
|
1035
|
+
const writer = elementWriter(state.el);
|
|
1036
|
+
let sh = state.sh;
|
|
1037
|
+
let clr = state.clr;
|
|
1038
|
+
let changed = false;
|
|
1039
|
+
const setShape = (arr, value) => {
|
|
1040
|
+
const next = arr ? { s: write.s, arr } : { s: write.s, arr, v: value };
|
|
1041
|
+
const later = laterShape(sh, next);
|
|
1042
|
+
if (later) {
|
|
1043
|
+
sh = later;
|
|
1044
|
+
changed = true;
|
|
1045
|
+
}
|
|
1046
|
+
};
|
|
1047
|
+
const atomic = write.a;
|
|
1048
|
+
const prev = !write.insert && Array.isArray(write.prev) ? write.prev : null;
|
|
1049
|
+
const atomicElement = atomic !== void 0 && (atomic.type === "append" || atomic.type === "remove");
|
|
1050
|
+
if (atomicElement && (prev === null || !Array.isArray(write.v))) {
|
|
1051
|
+
setShape(true);
|
|
1052
|
+
if (atomic.type === "append") writer.add(atomic.value, 0, write.s, 0);
|
|
1053
|
+
else if (!state.ao) writer.remove(atomic.value, 0, write.s);
|
|
1054
|
+
} else if (!Array.isArray(write.v)) {
|
|
1055
|
+
setShape(false, write.v);
|
|
1056
|
+
const nextClr = maxStamp(clr, write.s);
|
|
1057
|
+
if (nextClr !== clr) {
|
|
1058
|
+
clr = nextClr;
|
|
1059
|
+
changed = true;
|
|
1060
|
+
}
|
|
1061
|
+
} else {
|
|
1062
|
+
setShape(true);
|
|
1063
|
+
const next = countOccurrences(write.v);
|
|
1064
|
+
const before = prev ? countOccurrences(prev) : null;
|
|
1065
|
+
next.occurrences.forEach(({ item, canonical, k }, index) => {
|
|
1066
|
+
if (k >= (before?.counts.get(canonical) ?? 0)) writer.add(item, k, write.s, index);
|
|
1067
|
+
});
|
|
1068
|
+
if (before && !state.ao) {
|
|
1069
|
+
for (const { item, canonical, k } of before.occurrences) {
|
|
1070
|
+
if (k >= (next.counts.get(canonical) ?? 0)) writer.remove(item, k, write.s);
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
const elements = writer.result();
|
|
1075
|
+
if (!changed && !elements.changed) return { state, changed: false };
|
|
1076
|
+
return { state: { k: "set", ao: state.ao, sh, clr, el: elements.el }, changed: true };
|
|
1077
|
+
}
|
|
1078
|
+
function isElementPresent(element, state) {
|
|
1079
|
+
if (element.a === null || element.f === null) return false;
|
|
1080
|
+
if (state.ao) return true;
|
|
1081
|
+
return isAfter(element.a, element.r) && isAfter(element.a, state.clr);
|
|
1082
|
+
}
|
|
1083
|
+
function materializeElementSet(state) {
|
|
1084
|
+
if (state.sh === null) return void 0;
|
|
1085
|
+
if (!state.sh.arr) return state.sh.v;
|
|
1086
|
+
const present = Object.entries(state.el).filter(([, element]) => isElementPresent(element, state));
|
|
1087
|
+
present.sort(([ka, a], [kb, b]) => {
|
|
1088
|
+
const fa = a.f;
|
|
1089
|
+
const fb = b.f;
|
|
1090
|
+
return compareStamps(fa.s, fb.s) || fa.i - fb.i || (ka < kb ? -1 : ka > kb ? 1 : 0);
|
|
1091
|
+
});
|
|
1092
|
+
return present.map(([, element]) => element.v);
|
|
1093
|
+
}
|
|
1094
|
+
function applyKeyMap(state, write) {
|
|
1095
|
+
let sh = state.sh;
|
|
1096
|
+
let clr = state.clr;
|
|
1097
|
+
let keys = state.keys;
|
|
1098
|
+
let changed = false;
|
|
1099
|
+
const setKey = (key, next) => {
|
|
1100
|
+
const current = keys[key];
|
|
1101
|
+
if (current === void 0 || compareStamps(next.s, current.s) > 0) {
|
|
1102
|
+
if (keys === state.keys) keys = { ...state.keys };
|
|
1103
|
+
keys[key] = next;
|
|
1104
|
+
changed = true;
|
|
1105
|
+
}
|
|
1106
|
+
};
|
|
1107
|
+
if (!isPlainObject(write.v)) {
|
|
1108
|
+
const later = laterShape(sh, { s: write.s, obj: false, v: write.v });
|
|
1109
|
+
if (later) {
|
|
1110
|
+
sh = later;
|
|
1111
|
+
changed = true;
|
|
1112
|
+
}
|
|
1113
|
+
const nextClr = maxStamp(clr, write.s);
|
|
1114
|
+
if (nextClr !== clr) {
|
|
1115
|
+
clr = nextClr;
|
|
1116
|
+
changed = true;
|
|
1117
|
+
}
|
|
1118
|
+
} else {
|
|
1119
|
+
const later = laterShape(sh, { s: write.s, obj: true });
|
|
1120
|
+
if (later) {
|
|
1121
|
+
sh = later;
|
|
1122
|
+
changed = true;
|
|
1123
|
+
}
|
|
1124
|
+
const prev = !write.insert && isPlainObject(write.prev) ? write.prev : null;
|
|
1125
|
+
for (const [key, value] of Object.entries(write.v)) {
|
|
1126
|
+
if (prev && key in prev && canonicalKey(prev[key]) === canonicalKey(value)) continue;
|
|
1127
|
+
setKey(key, { s: write.s, del: false, v: value });
|
|
1128
|
+
}
|
|
1129
|
+
if (prev) {
|
|
1130
|
+
for (const key of Object.keys(prev)) {
|
|
1131
|
+
if (!(key in write.v)) setKey(key, { s: write.s, del: true });
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
if (!changed) return { state, changed: false };
|
|
1136
|
+
return { state: { k: "map", sh, clr, keys }, changed: true };
|
|
1137
|
+
}
|
|
1138
|
+
function materializeKeyMap(state) {
|
|
1139
|
+
if (state.sh === null) return void 0;
|
|
1140
|
+
if (!state.sh.obj) return state.sh.v;
|
|
1141
|
+
const out = {};
|
|
1142
|
+
for (const key of Object.keys(state.keys).sort()) {
|
|
1143
|
+
const entry = state.keys[key];
|
|
1144
|
+
if (entry.del || !isAfter(entry.s, state.clr)) continue;
|
|
1145
|
+
out[key] = entry.v;
|
|
1146
|
+
}
|
|
1147
|
+
return out;
|
|
1148
|
+
}
|
|
1149
|
+
function counterDelta(write) {
|
|
1150
|
+
if (write.a?.type === "increment" && typeof write.a.value === "number") return write.a.value;
|
|
1151
|
+
if (!write.insert && !write.a && typeof write.v === "number" && typeof write.prev === "number") {
|
|
1152
|
+
return write.v - write.prev;
|
|
1153
|
+
}
|
|
1154
|
+
return null;
|
|
1155
|
+
}
|
|
1156
|
+
function deltaIndex(deltas, stamp) {
|
|
1157
|
+
let lo = 0;
|
|
1158
|
+
let hi = deltas.length;
|
|
1159
|
+
while (lo < hi) {
|
|
1160
|
+
const mid = lo + hi >>> 1;
|
|
1161
|
+
const cmp = compareStamps(deltas[mid].s, stamp);
|
|
1162
|
+
if (cmp === 0) return -1;
|
|
1163
|
+
if (cmp < 0) lo = mid + 1;
|
|
1164
|
+
else hi = mid;
|
|
1165
|
+
}
|
|
1166
|
+
return lo;
|
|
1167
|
+
}
|
|
1168
|
+
function counterValue(base, deltas) {
|
|
1169
|
+
if (deltas.length === 0) return base?.v;
|
|
1170
|
+
let sum = typeof base?.v === "number" ? base.v : 0;
|
|
1171
|
+
for (const delta of deltas) sum += delta.n;
|
|
1172
|
+
return sum;
|
|
1173
|
+
}
|
|
1174
|
+
function counterState(base, d) {
|
|
1175
|
+
return { k: "ctr", base, d, val: counterValue(base, d) };
|
|
1176
|
+
}
|
|
1177
|
+
function pruneDeltas(d, base) {
|
|
1178
|
+
if (base === null) return [...d];
|
|
1179
|
+
return d.filter((delta) => compareStamps(delta.s, base.s) > 0);
|
|
1180
|
+
}
|
|
1181
|
+
function applyCounter(state, write) {
|
|
1182
|
+
const n = counterDelta(write);
|
|
1183
|
+
if (n !== null) {
|
|
1184
|
+
if (state.base !== null && compareStamps(write.s, state.base.s) < 0) {
|
|
1185
|
+
return { state, changed: false };
|
|
1186
|
+
}
|
|
1187
|
+
const index = deltaIndex(state.d, write.s);
|
|
1188
|
+
if (index === -1) return { state, changed: false };
|
|
1189
|
+
const delta = { s: write.s, n };
|
|
1190
|
+
if (index === state.d.length) {
|
|
1191
|
+
const val = state.d.length === 0 ? counterValue(state.base, [delta]) : state.val + n;
|
|
1192
|
+
return { state: { k: "ctr", base: state.base, d: [...state.d, delta], val }, changed: true };
|
|
1193
|
+
}
|
|
1194
|
+
const d = [...state.d.slice(0, index), delta, ...state.d.slice(index)];
|
|
1195
|
+
return { state: counterState(state.base, d), changed: true };
|
|
1196
|
+
}
|
|
1197
|
+
if (state.base !== null && compareStamps(write.s, state.base.s) <= 0) {
|
|
1198
|
+
return { state, changed: false };
|
|
1199
|
+
}
|
|
1200
|
+
const base = { s: write.s, v: write.v };
|
|
1201
|
+
return { state: counterState(base, pruneDeltas(state.d, base)), changed: true };
|
|
1202
|
+
}
|
|
1203
|
+
function mergeDeltas(a, b) {
|
|
1204
|
+
const out = [];
|
|
1205
|
+
let i = 0;
|
|
1206
|
+
let j = 0;
|
|
1207
|
+
while (i < a.length || j < b.length) {
|
|
1208
|
+
const x = a[i];
|
|
1209
|
+
const y = b[j];
|
|
1210
|
+
if (y === void 0 || x !== void 0 && compareStamps(x.s, y.s) < 0) {
|
|
1211
|
+
out.push(x);
|
|
1212
|
+
i++;
|
|
1213
|
+
} else if (x === void 0 || compareStamps(x.s, y.s) > 0) {
|
|
1214
|
+
out.push(y);
|
|
1215
|
+
j++;
|
|
1216
|
+
} else {
|
|
1217
|
+
out.push(x);
|
|
1218
|
+
i++;
|
|
1219
|
+
j++;
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
return out;
|
|
1223
|
+
}
|
|
1224
|
+
function betterExtremum(kind, current, next) {
|
|
1225
|
+
if (current === null) return true;
|
|
1226
|
+
if (next.v !== current.v) return kind === "max" ? next.v > current.v : next.v < current.v;
|
|
1227
|
+
return compareStamps(next.s, current.s) > 0;
|
|
1228
|
+
}
|
|
1229
|
+
function applyExtremum(state, write) {
|
|
1230
|
+
if (typeof write.v === "number" && !Number.isNaN(write.v)) {
|
|
1231
|
+
const next = { s: write.s, v: write.v };
|
|
1232
|
+
if (!betterExtremum(state.k, state.best, next)) return { state, changed: false };
|
|
1233
|
+
return { state: { k: state.k, best: next, reg: state.reg }, changed: true };
|
|
1234
|
+
}
|
|
1235
|
+
if (state.reg !== null && compareStamps(write.s, state.reg.s) <= 0) {
|
|
1236
|
+
return { state, changed: false };
|
|
1237
|
+
}
|
|
1238
|
+
return { state: { k: state.k, best: state.best, reg: { s: write.s, v: write.v } }, changed: true };
|
|
1239
|
+
}
|
|
1240
|
+
function decodeRichtext(value) {
|
|
1241
|
+
if (value === null || value === void 0) return { reset: null };
|
|
1242
|
+
try {
|
|
1243
|
+
const decoded = decodeBytesFromOpData(value);
|
|
1244
|
+
if (typeof decoded === "string") return { reset: decoded };
|
|
1245
|
+
return { update: bytesToBase64(decoded) };
|
|
1246
|
+
} catch {
|
|
1247
|
+
return { reset: value };
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
function applyRichtext(state, write, subsumes) {
|
|
1251
|
+
const decoded = decodeRichtext(write.v);
|
|
1252
|
+
if ("reset" in decoded) {
|
|
1253
|
+
if (state.reset !== null && compareStamps(write.s, state.reset.s) <= 0) {
|
|
1254
|
+
return { state, changed: false };
|
|
1255
|
+
}
|
|
1256
|
+
return {
|
|
1257
|
+
state: { k: "rt", reset: { s: write.s, v: decoded.reset }, u: state.u },
|
|
1258
|
+
changed: true
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
const current = state.u[decoded.update];
|
|
1262
|
+
if (current !== void 0 && compareStamps(write.s, current) <= 0) {
|
|
1263
|
+
return { state, changed: false };
|
|
1264
|
+
}
|
|
1265
|
+
if (subsumes === void 0) {
|
|
1266
|
+
return {
|
|
1267
|
+
state: { k: "rt", reset: state.reset, u: { ...state.u, [decoded.update]: write.s } },
|
|
1268
|
+
changed: true
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
const incoming = base64ToBytes(decoded.update);
|
|
1272
|
+
const u = {};
|
|
1273
|
+
for (const [update, stamp] of Object.entries(state.u)) {
|
|
1274
|
+
if (update === decoded.update) continue;
|
|
1275
|
+
const bytes = base64ToBytes(update);
|
|
1276
|
+
if (compareStamps(write.s, stamp) <= 0 && subsumes(incoming, bytes)) {
|
|
1277
|
+
return { state, changed: false };
|
|
1278
|
+
}
|
|
1279
|
+
if (compareStamps(stamp, write.s) <= 0 && subsumes(bytes, incoming)) continue;
|
|
1280
|
+
u[update] = stamp;
|
|
1281
|
+
}
|
|
1282
|
+
u[decoded.update] = write.s;
|
|
1283
|
+
return { state: { k: "rt", reset: state.reset, u }, changed: true };
|
|
1284
|
+
}
|
|
1285
|
+
function materializeRichtext(state, merger, field) {
|
|
1286
|
+
const live = Object.entries(state.u).filter(([, stamp]) => isAfter(stamp, state.reset?.s ?? null)).map(([update]) => update).sort();
|
|
1287
|
+
if (live.length === 0) return state.reset ? state.reset.v : void 0;
|
|
1288
|
+
if (live.length === 1) return { $koraBytes: live[0] };
|
|
1289
|
+
if (!merger) {
|
|
1290
|
+
throw new FoldConfigurationError(
|
|
1291
|
+
`Richtext field "${field}" has ${live.length} concurrent Yjs updates and no richtext merger was given.`,
|
|
1292
|
+
{
|
|
1293
|
+
field,
|
|
1294
|
+
updates: live.length,
|
|
1295
|
+
fix: "Pass { richtext: mergeYjsUpdates } (from @korajs/store) in the fold options."
|
|
1296
|
+
}
|
|
1297
|
+
);
|
|
1298
|
+
}
|
|
1299
|
+
return { $koraBytes: bytesToBase64(merger(live.map((update) => base64ToBytes(update)))) };
|
|
1300
|
+
}
|
|
1301
|
+
function applyFieldWrite(state, write, plan, subsumes) {
|
|
1302
|
+
switch (state.k) {
|
|
1303
|
+
case "reg":
|
|
1304
|
+
return applyRegister(state, write);
|
|
1305
|
+
case "res":
|
|
1306
|
+
return applyResolver(state, write, plan);
|
|
1307
|
+
case "set":
|
|
1308
|
+
return applyElementSet(state, write);
|
|
1309
|
+
case "map":
|
|
1310
|
+
return applyKeyMap(state, write);
|
|
1311
|
+
case "ctr":
|
|
1312
|
+
return applyCounter(state, write);
|
|
1313
|
+
case "max":
|
|
1314
|
+
case "min":
|
|
1315
|
+
return applyExtremum(state, write);
|
|
1316
|
+
case "rt":
|
|
1317
|
+
return applyRichtext(state, write, subsumes);
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
function materializeField(state, field, merger) {
|
|
1321
|
+
switch (state.k) {
|
|
1322
|
+
case "reg":
|
|
1323
|
+
case "res":
|
|
1324
|
+
return state.e.length === 0 ? void 0 : state.val;
|
|
1325
|
+
case "set":
|
|
1326
|
+
return materializeElementSet(state);
|
|
1327
|
+
case "map":
|
|
1328
|
+
return materializeKeyMap(state);
|
|
1329
|
+
case "ctr":
|
|
1330
|
+
return state.val;
|
|
1331
|
+
case "max":
|
|
1332
|
+
case "min":
|
|
1333
|
+
return state.best !== null ? state.best.v : state.reg?.v;
|
|
1334
|
+
case "rt":
|
|
1335
|
+
return materializeRichtext(state, merger, field);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
function fieldStamp(state) {
|
|
1339
|
+
switch (state.k) {
|
|
1340
|
+
case "reg":
|
|
1341
|
+
case "res":
|
|
1342
|
+
return state.e[state.e.length - 1]?.s ?? null;
|
|
1343
|
+
case "set": {
|
|
1344
|
+
let newest = maxStamp(state.sh?.s ?? null, state.clr);
|
|
1345
|
+
for (const element of Object.values(state.el)) {
|
|
1346
|
+
newest = maxStamp(maxStamp(newest, element.a), element.r);
|
|
1347
|
+
}
|
|
1348
|
+
return newest;
|
|
1349
|
+
}
|
|
1350
|
+
case "map": {
|
|
1351
|
+
let newest = maxStamp(state.sh?.s ?? null, state.clr);
|
|
1352
|
+
for (const entry of Object.values(state.keys)) newest = maxStamp(newest, entry.s);
|
|
1353
|
+
return newest;
|
|
1354
|
+
}
|
|
1355
|
+
case "ctr": {
|
|
1356
|
+
return maxStamp(state.base?.s ?? null, state.d[state.d.length - 1]?.s ?? null);
|
|
1357
|
+
}
|
|
1358
|
+
case "max":
|
|
1359
|
+
case "min":
|
|
1360
|
+
return maxStamp(state.best?.s ?? null, state.reg?.s ?? null);
|
|
1361
|
+
case "rt": {
|
|
1362
|
+
let newest = state.reset?.s ?? null;
|
|
1363
|
+
for (const stamp of Object.values(state.u)) newest = maxStamp(newest, stamp);
|
|
1364
|
+
return newest;
|
|
526
1365
|
}
|
|
527
1366
|
}
|
|
528
|
-
return true;
|
|
529
1367
|
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
);
|
|
534
|
-
|
|
1368
|
+
function laterOf(a, b) {
|
|
1369
|
+
if (a === null) return b;
|
|
1370
|
+
if (b === null) return a;
|
|
1371
|
+
return compareStamps(a.s, b.s) >= 0 ? a : b;
|
|
1372
|
+
}
|
|
1373
|
+
function joinElements(a, b) {
|
|
1374
|
+
const out = { ...a };
|
|
1375
|
+
for (const [key, y] of Object.entries(b)) {
|
|
1376
|
+
const x = out[key];
|
|
1377
|
+
if (x === void 0) {
|
|
1378
|
+
out[key] = y;
|
|
1379
|
+
continue;
|
|
1380
|
+
}
|
|
1381
|
+
out[key] = {
|
|
1382
|
+
v: x.v,
|
|
1383
|
+
n: x.n,
|
|
1384
|
+
a: maxStamp(x.a, y.a),
|
|
1385
|
+
f: y.f !== null && firstAddBefore(x.f, y.f) ? y.f : x.f,
|
|
1386
|
+
r: maxStamp(x.r, y.r)
|
|
1387
|
+
};
|
|
1388
|
+
}
|
|
1389
|
+
return out;
|
|
1390
|
+
}
|
|
1391
|
+
function joinFieldStates(a, b, plan) {
|
|
1392
|
+
if (a.k !== b.k) return a;
|
|
1393
|
+
switch (a.k) {
|
|
1394
|
+
case "reg": {
|
|
1395
|
+
const entries = pruneChain(mergeLogs(a.e, b.e));
|
|
1396
|
+
return { k: "reg", e: entries, val: chainFold(entries) };
|
|
1397
|
+
}
|
|
1398
|
+
case "res": {
|
|
1399
|
+
const entries = pruneResolverLog(mergeLogs(a.e, b.e));
|
|
1400
|
+
return resolverState(entries, resolverFold(plan, entries));
|
|
1401
|
+
}
|
|
1402
|
+
case "set": {
|
|
1403
|
+
const other = b;
|
|
1404
|
+
return {
|
|
1405
|
+
k: "set",
|
|
1406
|
+
ao: a.ao,
|
|
1407
|
+
sh: laterOf(a.sh, other.sh),
|
|
1408
|
+
clr: maxStamp(a.clr, other.clr),
|
|
1409
|
+
el: joinElements(a.el, other.el)
|
|
1410
|
+
};
|
|
1411
|
+
}
|
|
1412
|
+
case "map": {
|
|
1413
|
+
const other = b;
|
|
1414
|
+
const keys = { ...a.keys };
|
|
1415
|
+
for (const [key, entry] of Object.entries(other.keys)) {
|
|
1416
|
+
const current = keys[key];
|
|
1417
|
+
if (current === void 0 || compareStamps(entry.s, current.s) > 0) keys[key] = entry;
|
|
1418
|
+
}
|
|
1419
|
+
return { k: "map", sh: laterOf(a.sh, other.sh), clr: maxStamp(a.clr, other.clr), keys };
|
|
1420
|
+
}
|
|
1421
|
+
case "ctr": {
|
|
1422
|
+
const other = b;
|
|
1423
|
+
const base = laterOf(a.base, other.base);
|
|
1424
|
+
return counterState(base, pruneDeltas(mergeDeltas(a.d, other.d), base));
|
|
1425
|
+
}
|
|
1426
|
+
case "max":
|
|
1427
|
+
case "min": {
|
|
1428
|
+
const other = b;
|
|
1429
|
+
let best = a.best;
|
|
1430
|
+
if (other.best !== null && betterExtremum(a.k, best, other.best)) best = other.best;
|
|
1431
|
+
return { k: a.k, best, reg: laterOf(a.reg, other.reg) };
|
|
1432
|
+
}
|
|
1433
|
+
case "rt": {
|
|
1434
|
+
const other = b;
|
|
1435
|
+
const u = { ...a.u };
|
|
1436
|
+
for (const [update, stamp] of Object.entries(other.u)) {
|
|
1437
|
+
u[update] = maxStamp(u[update] ?? null, stamp);
|
|
1438
|
+
}
|
|
1439
|
+
return { k: "rt", reset: laterOf(a.reset, other.reset), u };
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
// src/fold/types.ts
|
|
1445
|
+
var FOLD_STATE_VERSION = 2;
|
|
1446
|
+
|
|
1447
|
+
// src/fold/serialize.ts
|
|
1448
|
+
var FIELD_KINDS = /* @__PURE__ */ new Set(["reg", "set", "map", "ctr", "max", "min", "res", "rt"]);
|
|
1449
|
+
function serializeFoldState(state) {
|
|
1450
|
+
return canonicalize(state);
|
|
1451
|
+
}
|
|
1452
|
+
function isRecord(value) {
|
|
1453
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1454
|
+
}
|
|
1455
|
+
function deserializeFoldState(json) {
|
|
1456
|
+
let parsed;
|
|
1457
|
+
try {
|
|
1458
|
+
parsed = JSON.parse(json);
|
|
1459
|
+
} catch (error) {
|
|
1460
|
+
throw new FoldStateError("Stored fold state is not valid JSON.", {
|
|
1461
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
1462
|
+
});
|
|
1463
|
+
}
|
|
1464
|
+
if (!isRecord(parsed)) throw new FoldStateError("Stored fold state is not an object.");
|
|
1465
|
+
if (parsed.v !== FOLD_STATE_VERSION) {
|
|
1466
|
+
throw new FoldStateError(
|
|
1467
|
+
`Stored fold state has format version ${String(parsed.v)}; this build reads version ${FOLD_STATE_VERSION}.`,
|
|
1468
|
+
{ found: parsed.v, expected: FOLD_STATE_VERSION }
|
|
1469
|
+
);
|
|
1470
|
+
}
|
|
1471
|
+
if (typeof parsed.c !== "string" || typeof parsed.r !== "string" || !isRecord(parsed.f)) {
|
|
1472
|
+
throw new FoldStateError("Stored fold state is missing its collection, record id or fields.");
|
|
1473
|
+
}
|
|
1474
|
+
for (const [field, state2] of Object.entries(parsed.f)) {
|
|
1475
|
+
if (!isRecord(state2) || typeof state2.k !== "string" || !FIELD_KINDS.has(state2.k)) {
|
|
1476
|
+
throw new FoldStateError(`Stored fold state has an unknown kind for field "${field}".`, {
|
|
1477
|
+
field
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
const state = parsed;
|
|
1482
|
+
for (const fieldState of Object.values(state.f)) {
|
|
1483
|
+
if ((fieldState.k === "reg" || fieldState.k === "res") && fieldState.e.length === 0) {
|
|
1484
|
+
fieldState.val = void 0;
|
|
1485
|
+
}
|
|
1486
|
+
if (fieldState.k === "ctr" && fieldState.d.length === 0 && fieldState.base === null) {
|
|
1487
|
+
fieldState.val = void 0;
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
return state;
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
// src/fold/fold.ts
|
|
1494
|
+
var FOLD_RECORD_TRACE_FIELD = "*";
|
|
1495
|
+
var SECRET_REDACTED = "[secret]";
|
|
1496
|
+
function createFoldState(collection, recordId) {
|
|
1497
|
+
return {
|
|
1498
|
+
v: FOLD_STATE_VERSION,
|
|
1499
|
+
c: collection,
|
|
1500
|
+
r: recordId,
|
|
1501
|
+
cr: null,
|
|
1502
|
+
w: null,
|
|
1503
|
+
d: null,
|
|
1504
|
+
u: null,
|
|
1505
|
+
f: {}
|
|
1506
|
+
};
|
|
1507
|
+
}
|
|
1508
|
+
function isExcluded(op2, exclude) {
|
|
1509
|
+
if (exclude === void 0) return false;
|
|
1510
|
+
return typeof exclude === "function" ? exclude(op2) : exclude.has(op2.id);
|
|
1511
|
+
}
|
|
1512
|
+
function samePlanKind(state, plan) {
|
|
1513
|
+
if (state.k !== plan.kind) return false;
|
|
1514
|
+
return state.k !== "set" || state.ao === plan.appendOnly;
|
|
1515
|
+
}
|
|
1516
|
+
function fieldPlanFor(collection, field, existing, state) {
|
|
1517
|
+
const plan = planField(collection, field);
|
|
1518
|
+
if (existing !== void 0 && !samePlanKind(existing, plan)) {
|
|
1519
|
+
throw new FoldStateError(
|
|
1520
|
+
`Field "${field}" of ${state.c}/${state.r} is stored as fold kind "${existing.k}" but the schema now folds it as "${plan.kind}".`,
|
|
1521
|
+
{ collection: state.c, recordId: state.r, field, stored: existing.k, expected: plan.kind }
|
|
1522
|
+
);
|
|
1523
|
+
}
|
|
1524
|
+
return plan;
|
|
1525
|
+
}
|
|
1526
|
+
function fieldWriteStamp(op2, field, opStamp) {
|
|
1527
|
+
if (op2.type !== "insert" || op2.fieldVersions === void 0) return opStamp;
|
|
1528
|
+
const version = op2.fieldVersions[field];
|
|
1529
|
+
if (version !== void 0) return stampOf(version, op2.id);
|
|
1530
|
+
let oldest = opStamp;
|
|
1531
|
+
for (const candidate of Object.values(op2.fieldVersions)) {
|
|
1532
|
+
const stamp = stampOf(candidate, op2.id);
|
|
1533
|
+
if (compareStamps(stamp, oldest) < 0) oldest = stamp;
|
|
1534
|
+
}
|
|
1535
|
+
return oldest;
|
|
1536
|
+
}
|
|
1537
|
+
function classify(stamp, op2, field, plan, options) {
|
|
1538
|
+
if (!plan.authoritative) return stamp;
|
|
1539
|
+
const author = op2.type === "insert" ? op2.fieldVersions?.[field]?.nodeId ?? op2.nodeId : op2.nodeId;
|
|
1540
|
+
if (isAuthoritativeNodeId(author, options.authoritativeNodeIds)) {
|
|
1541
|
+
return { ...stamp, c: 1 };
|
|
1542
|
+
}
|
|
1543
|
+
return stamp;
|
|
1544
|
+
}
|
|
1545
|
+
function mergeCarriedState(state, op2, schema, options) {
|
|
1546
|
+
let carried;
|
|
1547
|
+
try {
|
|
1548
|
+
carried = deserializeFoldState(op2.foldState);
|
|
1549
|
+
} catch (error) {
|
|
1550
|
+
if (error instanceof FoldStateError) return null;
|
|
1551
|
+
throw error;
|
|
1552
|
+
}
|
|
1553
|
+
if (carried.c !== state.c || carried.r !== state.r) {
|
|
1554
|
+
throw new FoldStateError(
|
|
1555
|
+
`Operation ${op2.id} carries the fold state of ${carried.c}/${carried.r} but targets ${state.c}/${state.r}.`,
|
|
1556
|
+
{ operationId: op2.id, collection: op2.collection, recordId: op2.recordId }
|
|
1557
|
+
);
|
|
1558
|
+
}
|
|
1559
|
+
let joined;
|
|
1560
|
+
try {
|
|
1561
|
+
joined = joinStates(state, carried, schema);
|
|
1562
|
+
} catch (error) {
|
|
1563
|
+
if (error instanceof FoldStateError) return null;
|
|
1564
|
+
throw error;
|
|
1565
|
+
}
|
|
1566
|
+
const changed = serializeFoldState(joined) !== serializeFoldState(state);
|
|
1567
|
+
const traces = [];
|
|
1568
|
+
if ((options.traces ?? "conflicts") !== "none") {
|
|
1569
|
+
const collection = schema.collections[state.c];
|
|
1570
|
+
for (const [field, incomingState] of Object.entries(carried.f)) {
|
|
1571
|
+
const mine = state.f[field];
|
|
1572
|
+
if (mine === void 0) continue;
|
|
1573
|
+
const prior = safeMaterializeField(mine, field, options);
|
|
1574
|
+
const incoming = safeMaterializeField(incomingState, field, options);
|
|
1575
|
+
if (canonicalKey(prior) === canonicalKey(incoming)) continue;
|
|
1576
|
+
const plan = planField(collection, field);
|
|
1577
|
+
const output = safeMaterializeField(joined.f[field], field, options);
|
|
1578
|
+
const redact = (value) => plan.secret ? SECRET_REDACTED : value;
|
|
1579
|
+
traces.push({
|
|
1580
|
+
field,
|
|
1581
|
+
strategy: `scope-entry-${plan.strategy}`,
|
|
1582
|
+
inputA: redact(prior),
|
|
1583
|
+
inputB: redact(incoming),
|
|
1584
|
+
base: null,
|
|
1585
|
+
output: redact(output),
|
|
1586
|
+
tier: plan.tier,
|
|
1587
|
+
constraintViolated: null,
|
|
1588
|
+
duration: 0,
|
|
1589
|
+
operation: plan.secret ? redactOperation(op2, field) : op2,
|
|
1590
|
+
priorOperationId: fieldStamp(mine)?.o ?? null,
|
|
1591
|
+
conflict: true
|
|
1592
|
+
});
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
return { state: changed ? joined : state, traces, changed };
|
|
1596
|
+
}
|
|
1597
|
+
function buildWrite(op2, field, raw, stamp) {
|
|
1598
|
+
if (raw === void 0) return null;
|
|
1599
|
+
const insert = op2.type === "insert";
|
|
1600
|
+
const v = normalizeValue(raw);
|
|
1601
|
+
const atomic = op2.atomicOps?.[field];
|
|
1602
|
+
const a = atomic ? { type: atomic.type, value: normalizeValue(atomic.value) } : void 0;
|
|
1603
|
+
const prevRaw = insert ? void 0 : op2.previousData?.[field];
|
|
1604
|
+
const hasPrev = !insert && op2.previousData !== null && field in op2.previousData && prevRaw !== void 0;
|
|
1605
|
+
const prev = hasPrev ? normalizeValue(prevRaw) : void 0;
|
|
1606
|
+
if (!insert && a === void 0 && hasPrev && canonicalKey(prev) === canonicalKey(v)) return null;
|
|
1607
|
+
return a ? { s: stamp, insert, v, prev, hasPrev, a } : { s: stamp, insert, v, prev, hasPrev };
|
|
1608
|
+
}
|
|
1609
|
+
function safeMaterializeField(state, field, options) {
|
|
1610
|
+
if (state === void 0) return void 0;
|
|
1611
|
+
try {
|
|
1612
|
+
return materializeField(state, field, options.richtext);
|
|
1613
|
+
} catch (error) {
|
|
1614
|
+
if (error instanceof FoldConfigurationError) return void 0;
|
|
1615
|
+
throw error;
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
function redactOperation(op2, field) {
|
|
1619
|
+
const redact = (data) => data !== null && field in data ? { ...data, [field]: SECRET_REDACTED } : data;
|
|
1620
|
+
return { ...op2, data: redact(op2.data), previousData: redact(op2.previousData) };
|
|
1621
|
+
}
|
|
1622
|
+
function fieldTrace(op2, field, plan, write, prior, priorStamp, output, newest, error) {
|
|
1623
|
+
const lost = newest !== null && compareStamps(newest, write.s) > 0;
|
|
1624
|
+
const baseMismatch = prior !== void 0 && !write.insert && canonicalKey(prior) !== canonicalKey(write.prev);
|
|
1625
|
+
const insertOnto = write.insert && prior !== void 0;
|
|
1626
|
+
const strategy = plan.kind === "reg" && write.a ? `atomic-${write.a.type}` : plan.strategy;
|
|
1627
|
+
const trace = {
|
|
1628
|
+
field,
|
|
1629
|
+
strategy,
|
|
1630
|
+
inputA: plan.secret ? SECRET_REDACTED : prior,
|
|
1631
|
+
inputB: plan.secret ? SECRET_REDACTED : write.v,
|
|
1632
|
+
base: write.hasPrev ? plan.secret ? SECRET_REDACTED : write.prev : null,
|
|
1633
|
+
output: plan.secret ? SECRET_REDACTED : output,
|
|
1634
|
+
tier: plan.tier,
|
|
1635
|
+
constraintViolated: null,
|
|
1636
|
+
// The fold is pure: callers time the whole mergeOp if they need a duration.
|
|
1637
|
+
duration: 0,
|
|
1638
|
+
operation: plan.secret ? redactOperation(op2, field) : op2,
|
|
1639
|
+
priorOperationId: priorStamp?.o ?? null,
|
|
1640
|
+
conflict: lost || baseMismatch || insertOnto
|
|
1641
|
+
};
|
|
1642
|
+
if (error !== void 0) trace.error = error;
|
|
1643
|
+
return trace;
|
|
1644
|
+
}
|
|
1645
|
+
function isAlive(state) {
|
|
1646
|
+
return state.cr !== null && isAfter(state.w, state.d);
|
|
1647
|
+
}
|
|
1648
|
+
function mergeOp(state, input, schema, options = {}) {
|
|
1649
|
+
if (isExcluded(input, options.exclude)) return { state, traces: [], changed: false };
|
|
1650
|
+
const op2 = operationSchemaView(input, schema.version, options.transforms);
|
|
1651
|
+
if (op2 === null) return { state, traces: [], changed: false };
|
|
1652
|
+
if (op2.collection !== state.c || op2.recordId !== state.r) {
|
|
1653
|
+
throw new FoldStateError(
|
|
1654
|
+
`Operation ${op2.id} targets ${op2.collection}/${op2.recordId} but the fold state is for ${state.c}/${state.r}.`,
|
|
1655
|
+
{ operationId: op2.id, collection: op2.collection, recordId: op2.recordId }
|
|
1656
|
+
);
|
|
1657
|
+
}
|
|
1658
|
+
if (op2.foldState !== void 0 && op2.type === "insert") {
|
|
1659
|
+
const carried = mergeCarriedState(state, op2, schema, options);
|
|
1660
|
+
if (carried !== null) return carried;
|
|
1661
|
+
}
|
|
1662
|
+
const mode = options.traces ?? "conflicts";
|
|
1663
|
+
const traces = [];
|
|
1664
|
+
const stamp = stampOf(op2.timestamp, op2.id);
|
|
1665
|
+
const next = { ...state };
|
|
1666
|
+
let changed = false;
|
|
1667
|
+
const setRecordStamp = (key, value) => {
|
|
1668
|
+
if (value !== next[key]) {
|
|
1669
|
+
next[key] = value;
|
|
1670
|
+
changed = true;
|
|
1671
|
+
}
|
|
1672
|
+
};
|
|
1673
|
+
setRecordStamp("u", maxStamp(state.u, stamp));
|
|
1674
|
+
if (op2.type === "delete") {
|
|
1675
|
+
setRecordStamp("d", maxStamp(state.d, stamp));
|
|
1676
|
+
if (mode !== "none" && state.cr !== null && isAfter(state.w, stamp)) {
|
|
1677
|
+
traces.push(recordTrace(op2, true, true));
|
|
1678
|
+
}
|
|
1679
|
+
return { state: changed ? next : state, traces, changed };
|
|
1680
|
+
}
|
|
1681
|
+
if (op2.data === null) {
|
|
1682
|
+
if (op2.encrypted !== void 0) {
|
|
1683
|
+
if (op2.type === "insert") setRecordStamp("cr", minStamp(state.cr, stamp));
|
|
1684
|
+
setRecordStamp("w", maxStamp(state.w, stamp));
|
|
1685
|
+
}
|
|
1686
|
+
return { state: changed ? next : state, traces, changed };
|
|
1687
|
+
}
|
|
1688
|
+
if (op2.type === "insert") setRecordStamp("cr", minStamp(state.cr, stamp));
|
|
1689
|
+
setRecordStamp("w", maxStamp(state.w, stamp));
|
|
1690
|
+
if (mode !== "none" && isAfter(state.d, stamp)) traces.push(recordTrace(op2, false, false));
|
|
1691
|
+
const collection = schema.collections[op2.collection];
|
|
1692
|
+
let fields = state.f;
|
|
1693
|
+
for (const [field, raw] of Object.entries(op2.data)) {
|
|
1694
|
+
if (raw === void 0) continue;
|
|
1695
|
+
const existing = fields[field];
|
|
1696
|
+
const plan = fieldPlanFor(collection, field, existing, state);
|
|
1697
|
+
const write = buildWrite(
|
|
1698
|
+
op2,
|
|
1699
|
+
field,
|
|
1700
|
+
raw,
|
|
1701
|
+
classify(fieldWriteStamp(op2, field, stamp), op2, field, plan, options)
|
|
1702
|
+
);
|
|
1703
|
+
if (write === null) continue;
|
|
1704
|
+
const tracing = mode !== "none";
|
|
1705
|
+
const prior = tracing ? safeMaterializeField(existing, field, options) : void 0;
|
|
1706
|
+
const priorStamp = existing ? fieldStamp(existing) : null;
|
|
1707
|
+
const result = applyFieldWrite(
|
|
1708
|
+
existing ?? emptyField(plan),
|
|
1709
|
+
write,
|
|
1710
|
+
plan,
|
|
1711
|
+
options.richtextSubsumes
|
|
1712
|
+
);
|
|
1713
|
+
if (result.changed) {
|
|
1714
|
+
if (fields === state.f) fields = { ...state.f };
|
|
1715
|
+
fields[field] = result.state;
|
|
1716
|
+
changed = true;
|
|
1717
|
+
}
|
|
1718
|
+
if (tracing) {
|
|
1719
|
+
const trace = fieldTrace(
|
|
1720
|
+
op2,
|
|
1721
|
+
field,
|
|
1722
|
+
plan,
|
|
1723
|
+
write,
|
|
1724
|
+
prior,
|
|
1725
|
+
priorStamp,
|
|
1726
|
+
safeMaterializeField(result.state, field, options),
|
|
1727
|
+
fieldStamp(result.state),
|
|
1728
|
+
result.error
|
|
1729
|
+
);
|
|
1730
|
+
if (mode === "all" || trace.conflict || trace.error !== void 0) traces.push(trace);
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
next.f = fields;
|
|
1734
|
+
return { state: changed ? next : state, traces, changed };
|
|
1735
|
+
}
|
|
1736
|
+
function recordTrace(op2, isDelete, lost) {
|
|
1737
|
+
return {
|
|
1738
|
+
field: FOLD_RECORD_TRACE_FIELD,
|
|
1739
|
+
strategy: "delete-tombstone-lww",
|
|
1740
|
+
inputA: isDelete ? "live" : "deleted",
|
|
1741
|
+
inputB: isDelete ? "deleted" : "live",
|
|
1742
|
+
base: null,
|
|
1743
|
+
// A delete that lost keeps the record live; a write older than the newest
|
|
1744
|
+
// delete stays hidden.
|
|
1745
|
+
output: isDelete ? lost ? "live" : "deleted" : "deleted",
|
|
1746
|
+
tier: 1,
|
|
1747
|
+
constraintViolated: null,
|
|
1748
|
+
duration: 0,
|
|
1749
|
+
operation: op2,
|
|
1750
|
+
priorOperationId: null,
|
|
1751
|
+
conflict: true
|
|
1752
|
+
};
|
|
1753
|
+
}
|
|
1754
|
+
function foldRecord(ops, schema, options = {}) {
|
|
1755
|
+
const kept = ops.filter((op2) => !isExcluded(op2, options.exclude));
|
|
1756
|
+
const first = kept[0];
|
|
1757
|
+
if (first === void 0) return { state: null, traces: [] };
|
|
1758
|
+
const ordered = kept.map((op2) => ({ op: op2, stamp: stampOf(op2.timestamp, op2.id) })).sort((a, b) => compareStamps(a.stamp, b.stamp));
|
|
1759
|
+
let state = createFoldState(first.collection, first.recordId);
|
|
1760
|
+
const traces = [];
|
|
1761
|
+
const mergeOptions = {
|
|
1762
|
+
...options,
|
|
1763
|
+
exclude: void 0,
|
|
1764
|
+
traces: options.traces ?? "none"
|
|
1765
|
+
};
|
|
1766
|
+
for (const { op: op2 } of ordered) {
|
|
1767
|
+
const result = mergeOp(state, op2, schema, mergeOptions);
|
|
1768
|
+
state = result.state;
|
|
1769
|
+
traces.push(...result.traces);
|
|
1770
|
+
}
|
|
1771
|
+
return { state, traces };
|
|
1772
|
+
}
|
|
1773
|
+
function materialize(state, options = {}) {
|
|
1774
|
+
if (!isAlive(state)) return null;
|
|
1775
|
+
const record = {};
|
|
1776
|
+
for (const field of Object.keys(state.f).sort()) {
|
|
1777
|
+
const value = materializeField(state.f[field], field, options.richtext);
|
|
1778
|
+
if (value !== void 0) record[field] = value;
|
|
1779
|
+
}
|
|
1780
|
+
return record;
|
|
1781
|
+
}
|
|
1782
|
+
function isFoldStateLive(state) {
|
|
1783
|
+
return isAlive(state);
|
|
1784
|
+
}
|
|
1785
|
+
function joinStates(a, b, schema) {
|
|
1786
|
+
if (a.c !== b.c || a.r !== b.r) {
|
|
1787
|
+
throw new FoldStateError(`Cannot join fold states of ${a.c}/${a.r} and ${b.c}/${b.r}.`, {
|
|
1788
|
+
left: `${a.c}/${a.r}`,
|
|
1789
|
+
right: `${b.c}/${b.r}`
|
|
1790
|
+
});
|
|
1791
|
+
}
|
|
1792
|
+
const collection = schema.collections[a.c];
|
|
1793
|
+
const fields = { ...a.f };
|
|
1794
|
+
for (const [field, other] of Object.entries(b.f)) {
|
|
1795
|
+
const mine = fields[field];
|
|
1796
|
+
if (mine === void 0) {
|
|
1797
|
+
fieldPlanFor(collection, field, other, b);
|
|
1798
|
+
fields[field] = other;
|
|
1799
|
+
continue;
|
|
1800
|
+
}
|
|
1801
|
+
const plan = fieldPlanFor(collection, field, mine, a);
|
|
1802
|
+
fieldPlanFor(collection, field, other, b);
|
|
1803
|
+
fields[field] = joinFieldStates(mine, other, plan);
|
|
1804
|
+
}
|
|
1805
|
+
return {
|
|
1806
|
+
v: FOLD_STATE_VERSION,
|
|
1807
|
+
c: a.c,
|
|
1808
|
+
r: a.r,
|
|
1809
|
+
cr: minStamp(a.cr, b.cr),
|
|
1810
|
+
w: maxStamp(a.w, b.w),
|
|
1811
|
+
d: maxStamp(a.d, b.d),
|
|
1812
|
+
u: maxStamp(a.u, b.u),
|
|
1813
|
+
f: fields
|
|
1814
|
+
};
|
|
1815
|
+
}
|
|
1816
|
+
function getFoldFieldVersionStrings(state) {
|
|
1817
|
+
const out = {};
|
|
1818
|
+
for (const field of Object.keys(state.f).sort()) {
|
|
1819
|
+
const stamp = fieldStamp(state.f[field]);
|
|
1820
|
+
if (stamp !== null) out[field] = stamp.t;
|
|
1821
|
+
}
|
|
1822
|
+
return out;
|
|
1823
|
+
}
|
|
1824
|
+
function getFoldFieldVersions(state) {
|
|
1825
|
+
if (!isAlive(state) || state.cr === null || state.u === null) return null;
|
|
1826
|
+
const fields = {};
|
|
1827
|
+
for (const [field, fieldState] of Object.entries(state.f)) {
|
|
1828
|
+
const stamp = fieldStamp(fieldState);
|
|
1829
|
+
if (stamp !== null) fields[field] = stampTimestamp(stamp);
|
|
1830
|
+
}
|
|
1831
|
+
return { fields, created: stampTimestamp(state.cr), latest: stampTimestamp(state.u) };
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
// src/fold/plan.ts
|
|
1835
|
+
function hashText2(text) {
|
|
1836
|
+
let hash = 2166136261;
|
|
1837
|
+
for (let i = 0; i < text.length; i++) {
|
|
1838
|
+
hash ^= text.charCodeAt(i);
|
|
1839
|
+
hash = Math.imul(hash, 16777619) >>> 0;
|
|
1840
|
+
}
|
|
1841
|
+
return hash.toString(16).padStart(8, "0");
|
|
1842
|
+
}
|
|
1843
|
+
function collectionFoldPlanFingerprint(name, collection) {
|
|
1844
|
+
const fields = Object.keys(collection.fields).sort().map((field) => {
|
|
1845
|
+
const descriptor = collection.fields[field];
|
|
1846
|
+
const resolver = collection.resolvers?.[field];
|
|
1847
|
+
return `${field}:${descriptor?.kind ?? ""}:${descriptor?.mergeStrategy ?? ""}:${resolver ? hashText2(String(resolver)) : ""}`;
|
|
1848
|
+
});
|
|
1849
|
+
const resolverOnly = Object.keys(collection.resolvers ?? {}).filter((field) => !(field in collection.fields)).sort().map((field) => `${field}:resolver:${hashText2(String(collection.resolvers?.[field]))}`);
|
|
1850
|
+
return `${name}(${[...fields, ...resolverOnly].join(",")})`;
|
|
1851
|
+
}
|
|
1852
|
+
function foldPlanFingerprint(schema, transforms) {
|
|
1853
|
+
const parts = [`fold-v${FOLD_STATE_VERSION}`];
|
|
1854
|
+
for (const name of Object.keys(schema.collections).sort()) {
|
|
1855
|
+
const collection = schema.collections[name];
|
|
1856
|
+
if (!collection) continue;
|
|
1857
|
+
parts.push(collectionFoldPlanFingerprint(name, collection));
|
|
1858
|
+
}
|
|
1859
|
+
const xf = operationTransformsFingerprint(schema.version, transforms);
|
|
1860
|
+
if (xf !== "") parts.push(xf);
|
|
1861
|
+
return parts.join("|");
|
|
1862
|
+
}
|
|
1863
|
+
function foldPlanFingerprints(schema, transforms) {
|
|
1864
|
+
const out = {};
|
|
1865
|
+
const xf = operationTransformsFingerprint(schema.version, transforms);
|
|
1866
|
+
for (const name of Object.keys(schema.collections).sort()) {
|
|
1867
|
+
const collection = schema.collections[name];
|
|
1868
|
+
if (!collection) continue;
|
|
1869
|
+
out[name] = `fold-v${FOLD_STATE_VERSION}|${collectionFoldPlanFingerprint(name, collection)}${xf !== "" ? `|${xf}` : ""}`;
|
|
1870
|
+
}
|
|
1871
|
+
return out;
|
|
1872
|
+
}
|
|
1873
|
+
function fieldStateMatchesPlan(collection, field, state) {
|
|
1874
|
+
const plan = planField(collection, field);
|
|
1875
|
+
if (state.k !== plan.kind) return false;
|
|
1876
|
+
return state.k !== "set" || state.ao === plan.appendOnly;
|
|
1877
|
+
}
|
|
1878
|
+
function mismatchedFoldFields(state, schema) {
|
|
1879
|
+
const collection = schema.collections[state.c];
|
|
1880
|
+
return Object.keys(state.f).filter((field) => !fieldStateMatchesPlan(collection, field, state.f[field])).sort();
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
// src/fold/snapshot.ts
|
|
1884
|
+
var SNAPSHOT_CLEAR_ID = "\uFFFFkora-snapshot:0";
|
|
1885
|
+
var SNAPSHOT_WRITE_ID = "\uFFFFkora-snapshot:1";
|
|
1886
|
+
function snapshotField(plan, raw, clear, write) {
|
|
1887
|
+
const v = normalizeValue(raw);
|
|
1888
|
+
switch (plan.kind) {
|
|
1889
|
+
case "reg":
|
|
1890
|
+
return { k: "reg", e: [{ s: write, v }], val: v };
|
|
1891
|
+
case "res":
|
|
1892
|
+
return { k: "res", e: [{ s: write, v, z: 1 }], val: v };
|
|
1893
|
+
case "ctr":
|
|
1894
|
+
return { k: "ctr", base: { s: write, v }, d: [], val: v };
|
|
1895
|
+
case "max":
|
|
1896
|
+
case "min":
|
|
1897
|
+
return typeof v === "number" && !Number.isNaN(v) ? { k: plan.kind, best: { s: write, v }, reg: null } : { k: plan.kind, best: null, reg: { s: write, v } };
|
|
1898
|
+
case "set": {
|
|
1899
|
+
if (!Array.isArray(v)) {
|
|
1900
|
+
return {
|
|
1901
|
+
k: "set",
|
|
1902
|
+
ao: plan.appendOnly,
|
|
1903
|
+
sh: { s: write, arr: false, v },
|
|
1904
|
+
clr: write,
|
|
1905
|
+
el: {}
|
|
1906
|
+
};
|
|
1907
|
+
}
|
|
1908
|
+
const el = {};
|
|
1909
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1910
|
+
v.forEach((item, index) => {
|
|
1911
|
+
const canonical = canonicalKey(item);
|
|
1912
|
+
const n = counts.get(canonical) ?? 0;
|
|
1913
|
+
counts.set(canonical, n + 1);
|
|
1914
|
+
el[elementKey(canonical, n)] = {
|
|
1915
|
+
v: JSON.parse(canonical),
|
|
1916
|
+
n,
|
|
1917
|
+
a: write,
|
|
1918
|
+
f: { s: write, i: index },
|
|
1919
|
+
r: null
|
|
1920
|
+
};
|
|
1921
|
+
});
|
|
1922
|
+
return { k: "set", ao: plan.appendOnly, sh: { s: write, arr: true }, clr: clear, el };
|
|
1923
|
+
}
|
|
1924
|
+
case "map": {
|
|
1925
|
+
if (!isPlainObject(v)) {
|
|
1926
|
+
return { k: "map", sh: { s: write, obj: false, v }, clr: write, keys: {} };
|
|
1927
|
+
}
|
|
1928
|
+
const keys = {};
|
|
1929
|
+
for (const [key, member] of Object.entries(v)) keys[key] = { s: write, del: false, v: member };
|
|
1930
|
+
return { k: "map", sh: { s: write, obj: true }, clr: clear, keys };
|
|
1931
|
+
}
|
|
1932
|
+
case "rt": {
|
|
1933
|
+
if (v === null || typeof v === "string" || !isPlainObject(v) || !("$koraBytes" in v)) {
|
|
1934
|
+
return { k: "rt", reset: { s: write, v }, u: {} };
|
|
1935
|
+
}
|
|
1936
|
+
const update = String(v.$koraBytes);
|
|
1937
|
+
return { k: "rt", reset: { s: clear, v: null }, u: { [update]: write } };
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
function createSnapshotState(input, schema, options = {}) {
|
|
1942
|
+
const state = createFoldState(input.collection, input.recordId);
|
|
1943
|
+
const collection = schema.collections[input.collection];
|
|
1944
|
+
const seed = options.seed && options.seed.c === input.collection && options.seed.r === input.recordId ? options.seed : null;
|
|
1945
|
+
const fields = {};
|
|
1946
|
+
for (const [field, raw] of Object.entries(input.values)) {
|
|
1947
|
+
if (raw === void 0) continue;
|
|
1948
|
+
const seeded = seed?.f[field];
|
|
1949
|
+
if (seeded !== void 0 && fieldStateMatchesPlan(collection, field, seeded)) {
|
|
1950
|
+
fields[field] = seeded;
|
|
1951
|
+
continue;
|
|
1952
|
+
}
|
|
1953
|
+
const version = input.fieldVersions[field] ?? input.latest;
|
|
1954
|
+
const plan = planField(collection, field);
|
|
1955
|
+
const authoritative = plan.authoritative === true && isAuthoritativeNodeId(version.nodeId, options.authoritativeNodeIds);
|
|
1956
|
+
fields[field] = snapshotField(
|
|
1957
|
+
plan,
|
|
1958
|
+
raw,
|
|
1959
|
+
classed(stampOf(version, SNAPSHOT_CLEAR_ID), authoritative),
|
|
1960
|
+
classed(stampOf(version, SNAPSHOT_WRITE_ID), authoritative)
|
|
1961
|
+
);
|
|
1962
|
+
}
|
|
1963
|
+
if (seed !== null && seed.cr !== null && seed.u !== null) {
|
|
1964
|
+
return { ...state, cr: seed.cr, w: seed.w, d: seed.d, u: seed.u, f: fields };
|
|
1965
|
+
}
|
|
1966
|
+
const latestClear = stampOf(input.latest, SNAPSHOT_CLEAR_ID);
|
|
1967
|
+
const latestWrite = stampOf(input.latest, SNAPSHOT_WRITE_ID);
|
|
1968
|
+
return {
|
|
1969
|
+
...state,
|
|
1970
|
+
cr: stampOf(input.created, SNAPSHOT_WRITE_ID),
|
|
1971
|
+
w: input.deleted ? latestClear : latestWrite,
|
|
1972
|
+
d: input.deleted ? latestWrite : null,
|
|
1973
|
+
u: latestWrite,
|
|
1974
|
+
f: fields
|
|
1975
|
+
};
|
|
1976
|
+
}
|
|
1977
|
+
function classed(stamp, authoritative) {
|
|
1978
|
+
return authoritative ? { ...stamp, c: 1 } : stamp;
|
|
1979
|
+
}
|
|
1980
|
+
function adaptFoldState(state, schema, options = {}) {
|
|
1981
|
+
const mismatched = mismatchedFoldFields(state, schema);
|
|
1982
|
+
if (mismatched.length === 0) return { state, adapted: [] };
|
|
1983
|
+
const collection = schema.collections[state.c];
|
|
1984
|
+
const fields = { ...state.f };
|
|
1985
|
+
for (const field of mismatched) {
|
|
1986
|
+
const old = fields[field];
|
|
1987
|
+
delete fields[field];
|
|
1988
|
+
const stamp = fieldStamp(old);
|
|
1989
|
+
if (stamp === null) continue;
|
|
1990
|
+
let value;
|
|
1991
|
+
try {
|
|
1992
|
+
value = materializeField(old, field, options.richtext);
|
|
1993
|
+
} catch (error) {
|
|
1994
|
+
if (error instanceof FoldConfigurationError) continue;
|
|
1995
|
+
throw error;
|
|
1996
|
+
}
|
|
1997
|
+
if (value === void 0) continue;
|
|
1998
|
+
const plan = planField(collection, field);
|
|
1999
|
+
const authoritative = stamp.c === 1 && plan.authoritative === true;
|
|
2000
|
+
fields[field] = snapshotField(
|
|
2001
|
+
plan,
|
|
2002
|
+
value,
|
|
2003
|
+
classed({ t: stamp.t, o: SNAPSHOT_CLEAR_ID }, authoritative),
|
|
2004
|
+
classed({ t: stamp.t, o: SNAPSHOT_WRITE_ID }, authoritative)
|
|
2005
|
+
);
|
|
2006
|
+
}
|
|
2007
|
+
return { state: { ...state, f: fields }, adapted: mismatched };
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
// src/fold/trace.ts
|
|
2011
|
+
function toMergeTrace(trace, operationA) {
|
|
2012
|
+
return {
|
|
2013
|
+
operationA: operationA ?? trace.operation,
|
|
2014
|
+
operationB: trace.operation,
|
|
2015
|
+
field: trace.field,
|
|
2016
|
+
strategy: trace.strategy,
|
|
2017
|
+
inputA: trace.inputA,
|
|
2018
|
+
inputB: trace.inputB,
|
|
2019
|
+
base: trace.base,
|
|
2020
|
+
output: trace.output,
|
|
2021
|
+
tier: trace.tier,
|
|
2022
|
+
constraintViolated: trace.constraintViolated,
|
|
2023
|
+
duration: trace.duration
|
|
2024
|
+
};
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
// src/text/stored-text.ts
|
|
2028
|
+
var STORED_TEXT_ESCAPE = "\uFFFF";
|
|
2029
|
+
var NEEDS_ENCODING = /[\u0000\ud800-\udfff]/;
|
|
2030
|
+
function needsStoredTextEncoding(s) {
|
|
2031
|
+
return NEEDS_ENCODING.test(s);
|
|
2032
|
+
}
|
|
2033
|
+
function encodeStoredText(s) {
|
|
2034
|
+
if (!NEEDS_ENCODING.test(s)) return s;
|
|
535
2035
|
let out = "";
|
|
536
|
-
for (let i = 0; i <
|
|
537
|
-
const
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
2036
|
+
for (let i = 0; i < s.length; i++) {
|
|
2037
|
+
const code = s.charCodeAt(i);
|
|
2038
|
+
if (code === 0) {
|
|
2039
|
+
out += `${STORED_TEXT_ESCAPE}0`;
|
|
2040
|
+
} else if (code === 65535) {
|
|
2041
|
+
out += `${STORED_TEXT_ESCAPE}F`;
|
|
2042
|
+
} else if (code >= 55296 && code <= 56319) {
|
|
2043
|
+
const next = s.charCodeAt(i + 1);
|
|
2044
|
+
if (next >= 56320 && next <= 57343) {
|
|
2045
|
+
out += s.slice(i, i + 2);
|
|
2046
|
+
i++;
|
|
2047
|
+
} else {
|
|
2048
|
+
out += `${STORED_TEXT_ESCAPE}s${code.toString(16)}`;
|
|
2049
|
+
}
|
|
2050
|
+
} else if (code >= 56320 && code <= 57343) {
|
|
2051
|
+
out += `${STORED_TEXT_ESCAPE}s${code.toString(16)}`;
|
|
2052
|
+
} else {
|
|
2053
|
+
out += s.charAt(i);
|
|
2054
|
+
}
|
|
545
2055
|
}
|
|
546
2056
|
return out;
|
|
547
2057
|
}
|
|
548
|
-
function
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
let
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
if (value === void 0) {
|
|
557
|
-
throw new OperationError(`Invalid base64 character "${char}" in tagged binary value.`, {
|
|
558
|
-
char
|
|
559
|
-
});
|
|
2058
|
+
function decodeStoredText(s) {
|
|
2059
|
+
if (!s.includes(STORED_TEXT_ESCAPE)) return s;
|
|
2060
|
+
let out = "";
|
|
2061
|
+
for (let i = 0; i < s.length; i++) {
|
|
2062
|
+
const char = s[i];
|
|
2063
|
+
if (char !== STORED_TEXT_ESCAPE) {
|
|
2064
|
+
out += char;
|
|
2065
|
+
continue;
|
|
560
2066
|
}
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
2067
|
+
const tag = s[i + 1];
|
|
2068
|
+
if (tag === "0") {
|
|
2069
|
+
out += "\0";
|
|
2070
|
+
i += 1;
|
|
2071
|
+
} else if (tag === "F") {
|
|
2072
|
+
out += STORED_TEXT_ESCAPE;
|
|
2073
|
+
i += 1;
|
|
2074
|
+
} else if (tag === "s") {
|
|
2075
|
+
const hex = s.slice(i + 2, i + 6);
|
|
2076
|
+
const code = Number.parseInt(hex, 16);
|
|
2077
|
+
if (hex.length === 4 && Number.isInteger(code) && code >= 55296 && code <= 57343) {
|
|
2078
|
+
out += String.fromCharCode(code);
|
|
2079
|
+
i += 5;
|
|
2080
|
+
} else {
|
|
2081
|
+
out += char;
|
|
2082
|
+
}
|
|
2083
|
+
} else {
|
|
2084
|
+
out += char;
|
|
567
2085
|
}
|
|
568
2086
|
}
|
|
569
2087
|
return out;
|
|
570
2088
|
}
|
|
571
|
-
function
|
|
572
|
-
|
|
573
|
-
return value;
|
|
574
|
-
}
|
|
575
|
-
const bytes = value instanceof Uint8Array ? value : new Uint8Array(value);
|
|
576
|
-
return { $koraBytes: bytesToBase64(bytes) };
|
|
2089
|
+
function encodeStoredJsonValue(value) {
|
|
2090
|
+
return mapStrings(value, encodeStoredText);
|
|
577
2091
|
}
|
|
578
|
-
function
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
if (value
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
2092
|
+
function decodeStoredJsonValue(value) {
|
|
2093
|
+
return mapStrings(value, decodeStoredText);
|
|
2094
|
+
}
|
|
2095
|
+
function mapStrings(value, map) {
|
|
2096
|
+
if (typeof value === "string") return map(value);
|
|
2097
|
+
if (Array.isArray(value)) return value.map((member) => mapStrings(member, map));
|
|
2098
|
+
if (value !== null && typeof value === "object" && !ArrayBuffer.isView(value)) {
|
|
2099
|
+
const out = {};
|
|
2100
|
+
for (const [key, member] of Object.entries(value)) out[map(key)] = mapStrings(member, map);
|
|
2101
|
+
return out;
|
|
2102
|
+
}
|
|
2103
|
+
return value;
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
// src/fold/side-effect-id.ts
|
|
2107
|
+
var SIDE_EFFECT_DOMAIN = "kora/side-effect/v1";
|
|
2108
|
+
async function deriveSideEffectOpId(parentOpId, ruleId, targetRecordId) {
|
|
2109
|
+
for (const [name, value] of [
|
|
2110
|
+
["parentOpId", parentOpId],
|
|
2111
|
+
["ruleId", ruleId],
|
|
2112
|
+
["targetRecordId", targetRecordId]
|
|
2113
|
+
]) {
|
|
2114
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
2115
|
+
throw new OperationError(`deriveSideEffectOpId: ${name} must be a non-empty string`, {
|
|
2116
|
+
[name]: value
|
|
2117
|
+
});
|
|
596
2118
|
}
|
|
597
|
-
return bytes;
|
|
598
2119
|
}
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
2120
|
+
const canonical = canonicalize({
|
|
2121
|
+
domain: SIDE_EFFECT_DOMAIN,
|
|
2122
|
+
parent: parentOpId,
|
|
2123
|
+
rule: ruleId,
|
|
2124
|
+
target: targetRecordId
|
|
2125
|
+
});
|
|
2126
|
+
const digest = await globalThis.crypto.subtle.digest(
|
|
2127
|
+
"SHA-256",
|
|
2128
|
+
new TextEncoder().encode(canonical)
|
|
602
2129
|
);
|
|
2130
|
+
return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
603
2131
|
}
|
|
604
2132
|
|
|
605
2133
|
// src/scopes/sync-scope-bindings.ts
|
|
@@ -884,6 +2412,21 @@ function buildCollection(name, input) {
|
|
|
884
2412
|
validateStateMachineDefinition(name, input.stateMachine, fields);
|
|
885
2413
|
stateMachine = { ...input.stateMachine };
|
|
886
2414
|
}
|
|
2415
|
+
if (!stateMachine) {
|
|
2416
|
+
const transitionFields = Object.entries(fields).filter(
|
|
2417
|
+
([, descriptor]) => descriptor.kind === "enum" && descriptor.transitions
|
|
2418
|
+
);
|
|
2419
|
+
const only = transitionFields.length === 1 ? transitionFields[0] : void 0;
|
|
2420
|
+
if (only?.[1].transitions) {
|
|
2421
|
+
const derived = {
|
|
2422
|
+
field: only[0],
|
|
2423
|
+
transitions: { ...only[1].transitions },
|
|
2424
|
+
onInvalidTransition: "reject"
|
|
2425
|
+
};
|
|
2426
|
+
validateStateMachineDefinition(name, derived, fields);
|
|
2427
|
+
stateMachine = derived;
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
887
2430
|
return { fields, indexes, constraints, resolvers, scope, stateMachine };
|
|
888
2431
|
}
|
|
889
2432
|
function buildSyncRules(syncInput, collections) {
|
|
@@ -966,6 +2509,22 @@ function validateConstraint(collection, constraint, fields) {
|
|
|
966
2509
|
);
|
|
967
2510
|
}
|
|
968
2511
|
}
|
|
2512
|
+
if (constraint.where !== void 0 && constraint.type !== "referential") {
|
|
2513
|
+
for (const [key, value] of Object.entries(constraint.where)) {
|
|
2514
|
+
if (key !== "id" && !(key in fields)) {
|
|
2515
|
+
throw new SchemaValidationError(
|
|
2516
|
+
`Constraint where references field "${key}" which does not exist in collection "${collection}". Available fields: ${Object.keys(fields).join(", ")}`,
|
|
2517
|
+
{ collection, field: key }
|
|
2518
|
+
);
|
|
2519
|
+
}
|
|
2520
|
+
if (value !== null && typeof value === "object") {
|
|
2521
|
+
throw new SchemaValidationError(
|
|
2522
|
+
`Constraint where on "${collection}.${key}" uses ${Array.isArray(value) ? "an array" : `the operator object ${JSON.stringify(value)}`}, but constraint where matches by plain equality only, so the constraint would never apply. Use an equality value (for example { ${key}: 'published' }), or enforce the rule in validateOperation or a server route.`,
|
|
2523
|
+
{ collection, field: key }
|
|
2524
|
+
);
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
969
2528
|
if (constraint.onConflict === "priority-field" && !constraint.priorityField) {
|
|
970
2529
|
throw new SchemaValidationError(
|
|
971
2530
|
`Constraint with "priority-field" onConflict strategy in collection "${collection}" requires a priorityField`,
|
|
@@ -1009,170 +2568,6 @@ function validateRelation(name, relation, collections) {
|
|
|
1009
2568
|
}
|
|
1010
2569
|
}
|
|
1011
2570
|
|
|
1012
|
-
// src/schema/quote-ident.ts
|
|
1013
|
-
function quoteIdent(name) {
|
|
1014
|
-
return `"${name.replace(/"/g, '""')}"`;
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
// src/schema/sql-gen.ts
|
|
1018
|
-
function generateSQL(collectionName, collection, relations) {
|
|
1019
|
-
const statements = [];
|
|
1020
|
-
const columns = ["id TEXT PRIMARY KEY NOT NULL"];
|
|
1021
|
-
const indexedFields = new Set(collection.indexes);
|
|
1022
|
-
const fkFields = [];
|
|
1023
|
-
for (const [fieldName, descriptor] of Object.entries(collection.fields)) {
|
|
1024
|
-
let colDef = columnDefinition(fieldName, descriptor);
|
|
1025
|
-
if (relations) {
|
|
1026
|
-
for (const rel of Object.values(relations)) {
|
|
1027
|
-
if (rel.from === collectionName && rel.field === fieldName) {
|
|
1028
|
-
colDef += ` REFERENCES ${quoteIdent(rel.to)}(id)`;
|
|
1029
|
-
fkFields.push(fieldName);
|
|
1030
|
-
break;
|
|
1031
|
-
}
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
columns.push(colDef);
|
|
1035
|
-
}
|
|
1036
|
-
columns.push("_created_at INTEGER NOT NULL");
|
|
1037
|
-
columns.push("_updated_at INTEGER NOT NULL");
|
|
1038
|
-
columns.push("_version TEXT NOT NULL DEFAULT ''");
|
|
1039
|
-
columns.push("_field_versions TEXT NOT NULL DEFAULT '{}'");
|
|
1040
|
-
columns.push("_deleted INTEGER NOT NULL DEFAULT 0");
|
|
1041
|
-
statements.push(
|
|
1042
|
-
`CREATE TABLE IF NOT EXISTS ${quoteIdent(collectionName)} (
|
|
1043
|
-
${columns.join(",\n ")}
|
|
1044
|
-
)`
|
|
1045
|
-
);
|
|
1046
|
-
for (const [fieldName, descriptor] of Object.entries(collection.fields)) {
|
|
1047
|
-
const colDef = columnDefinition(fieldName, descriptor);
|
|
1048
|
-
statements.push(
|
|
1049
|
-
`--kora:safe-alter
|
|
1050
|
-
ALTER TABLE ${quoteIdent(collectionName)} ADD COLUMN ${colDef}`
|
|
1051
|
-
);
|
|
1052
|
-
}
|
|
1053
|
-
statements.push(
|
|
1054
|
-
`--kora:safe-alter
|
|
1055
|
-
ALTER TABLE ${quoteIdent(collectionName)} ADD COLUMN _version TEXT NOT NULL DEFAULT ''`
|
|
1056
|
-
);
|
|
1057
|
-
statements.push(
|
|
1058
|
-
`--kora:safe-alter
|
|
1059
|
-
ALTER TABLE ${quoteIdent(collectionName)} ADD COLUMN _field_versions TEXT NOT NULL DEFAULT '{}'`
|
|
1060
|
-
);
|
|
1061
|
-
for (const indexField of collection.indexes) {
|
|
1062
|
-
statements.push(
|
|
1063
|
-
`CREATE INDEX IF NOT EXISTS idx_${collectionName}_${indexField} ON ${quoteIdent(collectionName)} (${quoteIdent(indexField)})`
|
|
1064
|
-
);
|
|
1065
|
-
}
|
|
1066
|
-
for (const fkField of fkFields) {
|
|
1067
|
-
if (!indexedFields.has(fkField)) {
|
|
1068
|
-
statements.push(
|
|
1069
|
-
`CREATE INDEX IF NOT EXISTS idx_${collectionName}_${fkField} ON ${quoteIdent(collectionName)} (${quoteIdent(fkField)})`
|
|
1070
|
-
);
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
1073
|
-
statements.push(
|
|
1074
|
-
`CREATE TABLE IF NOT EXISTS ${quoteIdent(`_kora_ops_${collectionName}`)} (
|
|
1075
|
-
id TEXT PRIMARY KEY NOT NULL,
|
|
1076
|
-
node_id TEXT NOT NULL,
|
|
1077
|
-
type TEXT NOT NULL,
|
|
1078
|
-
record_id TEXT NOT NULL,
|
|
1079
|
-
data TEXT,
|
|
1080
|
-
previous_data TEXT,
|
|
1081
|
-
timestamp TEXT NOT NULL,
|
|
1082
|
-
sequence_number INTEGER NOT NULL,
|
|
1083
|
-
causal_deps TEXT NOT NULL,
|
|
1084
|
-
schema_version INTEGER NOT NULL
|
|
1085
|
-
)`
|
|
1086
|
-
);
|
|
1087
|
-
statements.push(
|
|
1088
|
-
`CREATE INDEX IF NOT EXISTS idx_kora_ops_${collectionName}_record_id ON ${quoteIdent(`_kora_ops_${collectionName}`)} (record_id)`
|
|
1089
|
-
);
|
|
1090
|
-
return statements;
|
|
1091
|
-
}
|
|
1092
|
-
function generateFullDDL(schema) {
|
|
1093
|
-
const statements = [];
|
|
1094
|
-
statements.push(
|
|
1095
|
-
"CREATE TABLE IF NOT EXISTS _kora_meta (\n key TEXT PRIMARY KEY NOT NULL,\n value TEXT NOT NULL\n)"
|
|
1096
|
-
);
|
|
1097
|
-
statements.push(
|
|
1098
|
-
"CREATE TABLE IF NOT EXISTS _kora_version_vector (\n node_id TEXT PRIMARY KEY NOT NULL,\n sequence_number INTEGER NOT NULL\n)"
|
|
1099
|
-
);
|
|
1100
|
-
statements.push(
|
|
1101
|
-
"CREATE TABLE IF NOT EXISTS _kora_sequences (\n name TEXT NOT NULL,\n scope TEXT NOT NULL DEFAULT '',\n node_id TEXT NOT NULL,\n counter INTEGER NOT NULL DEFAULT 0,\n PRIMARY KEY (name, scope, node_id)\n)"
|
|
1102
|
-
);
|
|
1103
|
-
statements.push(
|
|
1104
|
-
"CREATE TABLE IF NOT EXISTS _kora_sync_queue (\n id TEXT PRIMARY KEY NOT NULL,\n payload TEXT NOT NULL\n)"
|
|
1105
|
-
);
|
|
1106
|
-
statements.push(
|
|
1107
|
-
"CREATE TABLE IF NOT EXISTS _kora_sync_rejected (\n operation_id TEXT PRIMARY KEY NOT NULL,\n collection TEXT NOT NULL,\n record_id TEXT NOT NULL,\n code TEXT NOT NULL,\n message TEXT NOT NULL,\n retriable INTEGER NOT NULL,\n rejected_at INTEGER NOT NULL\n)"
|
|
1108
|
-
);
|
|
1109
|
-
statements.push(
|
|
1110
|
-
"CREATE TABLE IF NOT EXISTS _kora_audit_traces (\n id TEXT PRIMARY KEY NOT NULL,\n recorded_at INTEGER NOT NULL,\n event_type TEXT NOT NULL,\n collection TEXT NOT NULL,\n record_id TEXT NOT NULL,\n field TEXT NOT NULL,\n strategy TEXT NOT NULL,\n tier INTEGER NOT NULL,\n constraint_name TEXT,\n trace_json TEXT NOT NULL\n)"
|
|
1111
|
-
);
|
|
1112
|
-
statements.push(
|
|
1113
|
-
"CREATE INDEX IF NOT EXISTS idx_kora_audit_traces_recorded_at ON _kora_audit_traces (recorded_at)"
|
|
1114
|
-
);
|
|
1115
|
-
statements.push(
|
|
1116
|
-
"CREATE INDEX IF NOT EXISTS idx_kora_audit_traces_collection ON _kora_audit_traces (collection)"
|
|
1117
|
-
);
|
|
1118
|
-
for (const [name, collection] of Object.entries(schema.collections)) {
|
|
1119
|
-
statements.push(...generateSQL(name, collection, schema.relations));
|
|
1120
|
-
}
|
|
1121
|
-
return statements;
|
|
1122
|
-
}
|
|
1123
|
-
function columnDefinition(fieldName, descriptor) {
|
|
1124
|
-
const sqlType = mapFieldType(descriptor);
|
|
1125
|
-
const parts = [quoteIdent(fieldName), sqlType];
|
|
1126
|
-
if (descriptor.required && descriptor.defaultValue === void 0 && !descriptor.auto) {
|
|
1127
|
-
parts.push("NOT NULL");
|
|
1128
|
-
}
|
|
1129
|
-
if (descriptor.defaultValue !== void 0) {
|
|
1130
|
-
parts.push(`DEFAULT ${sqlDefault(descriptor.defaultValue)}`);
|
|
1131
|
-
}
|
|
1132
|
-
if (descriptor.kind === "enum" && descriptor.enumValues) {
|
|
1133
|
-
const values = descriptor.enumValues.map((v) => `'${v}'`).join(", ");
|
|
1134
|
-
parts.push(`CHECK (${quoteIdent(fieldName)} IN (${values}))`);
|
|
1135
|
-
}
|
|
1136
|
-
return parts.join(" ");
|
|
1137
|
-
}
|
|
1138
|
-
function mapFieldType(descriptor) {
|
|
1139
|
-
switch (descriptor.kind) {
|
|
1140
|
-
case "string":
|
|
1141
|
-
return "TEXT";
|
|
1142
|
-
case "number":
|
|
1143
|
-
return "REAL";
|
|
1144
|
-
case "boolean":
|
|
1145
|
-
return "INTEGER";
|
|
1146
|
-
case "enum":
|
|
1147
|
-
return "TEXT";
|
|
1148
|
-
case "timestamp":
|
|
1149
|
-
return "INTEGER";
|
|
1150
|
-
case "array":
|
|
1151
|
-
return "TEXT";
|
|
1152
|
-
// JSON-serialized
|
|
1153
|
-
case "object":
|
|
1154
|
-
return "TEXT";
|
|
1155
|
-
// JSON-serialized structured object
|
|
1156
|
-
case "json":
|
|
1157
|
-
return "TEXT";
|
|
1158
|
-
// JSON-serialized dynamic-key value
|
|
1159
|
-
case "blob":
|
|
1160
|
-
return "TEXT";
|
|
1161
|
-
// JSON-serialized content-addressed BlobRef (bytes stored out of band)
|
|
1162
|
-
case "secret":
|
|
1163
|
-
return "TEXT";
|
|
1164
|
-
case "richtext":
|
|
1165
|
-
return "BLOB";
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
1168
|
-
function sqlDefault(value) {
|
|
1169
|
-
if (value === null) return "NULL";
|
|
1170
|
-
if (typeof value === "string") return `'${value}'`;
|
|
1171
|
-
if (typeof value === "number") return String(value);
|
|
1172
|
-
if (typeof value === "boolean") return value ? "1" : "0";
|
|
1173
|
-
return `'${JSON.stringify(value)}'`;
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
2571
|
// src/schema/types.ts
|
|
1177
2572
|
var FieldBuilder = class _FieldBuilder {
|
|
1178
2573
|
_kind;
|
|
@@ -1187,15 +2582,21 @@ var FieldBuilder = class _FieldBuilder {
|
|
|
1187
2582
|
this._auto = auto;
|
|
1188
2583
|
this._mergeStrategy = mergeStrategy;
|
|
1189
2584
|
}
|
|
1190
|
-
/** Mark this field as optional (not required on insert) */
|
|
2585
|
+
/** Mark this field as optional (not required on insert; reads may return `null`). */
|
|
1191
2586
|
optional() {
|
|
1192
2587
|
return new _FieldBuilder(this._kind, false, this._defaultValue, this._auto, this._mergeStrategy);
|
|
1193
2588
|
}
|
|
1194
|
-
/**
|
|
2589
|
+
/**
|
|
2590
|
+
* Set a default value for this field. Implicitly makes the field optional on insert.
|
|
2591
|
+
* The value must be of the field's type (`t.number().default('x')` is a type error).
|
|
2592
|
+
*/
|
|
1195
2593
|
default(value) {
|
|
1196
2594
|
return new _FieldBuilder(this._kind, false, value, this._auto, this._mergeStrategy);
|
|
1197
2595
|
}
|
|
1198
|
-
/**
|
|
2596
|
+
/**
|
|
2597
|
+
* Mark this field as auto-populated (e.g., createdAt timestamps). Developers cannot
|
|
2598
|
+
* set auto fields; the framework fills `t.timestamp().auto()` with the insert time.
|
|
2599
|
+
*/
|
|
1199
2600
|
auto() {
|
|
1200
2601
|
return new _FieldBuilder(this._kind, false, void 0, true, this._mergeStrategy);
|
|
1201
2602
|
}
|
|
@@ -1213,13 +2614,7 @@ var FieldBuilder = class _FieldBuilder {
|
|
|
1213
2614
|
* - `'server-authoritative'`: Always prefer the remote/server value
|
|
1214
2615
|
*/
|
|
1215
2616
|
merge(strategy) {
|
|
1216
|
-
return new _FieldBuilder(
|
|
1217
|
-
this._kind,
|
|
1218
|
-
this._required,
|
|
1219
|
-
this._defaultValue,
|
|
1220
|
-
this._auto,
|
|
1221
|
-
strategy
|
|
1222
|
-
);
|
|
2617
|
+
return new _FieldBuilder(this._kind, this._required, this._defaultValue, this._auto, strategy);
|
|
1223
2618
|
}
|
|
1224
2619
|
/** @internal Build the final FieldDescriptor. Used by defineSchema(). */
|
|
1225
2620
|
_build() {
|
|
@@ -1235,6 +2630,16 @@ var FieldBuilder = class _FieldBuilder {
|
|
|
1235
2630
|
};
|
|
1236
2631
|
}
|
|
1237
2632
|
};
|
|
2633
|
+
function assertStorableEnumValues(values) {
|
|
2634
|
+
for (const value of values) {
|
|
2635
|
+
if (typeof value !== "string" || encodeStoredText(value) !== value) {
|
|
2636
|
+
throw new SchemaValidationError(
|
|
2637
|
+
`Enum value ${JSON.stringify(value)} cannot be stored: enum values must be well-formed strings without U+0000 or U+FFFF.`,
|
|
2638
|
+
{ value: String(value) }
|
|
2639
|
+
);
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
1238
2643
|
var EnumFieldBuilder = class _EnumFieldBuilder extends FieldBuilder {
|
|
1239
2644
|
_enumValues;
|
|
1240
2645
|
_transitions;
|
|
@@ -1340,14 +2745,14 @@ var EnumFieldBuilder = class _EnumFieldBuilder extends FieldBuilder {
|
|
|
1340
2745
|
}
|
|
1341
2746
|
};
|
|
1342
2747
|
var ArrayFieldBuilder = class _ArrayFieldBuilder extends FieldBuilder {
|
|
1343
|
-
|
|
2748
|
+
_item;
|
|
1344
2749
|
constructor(itemBuilder, required = true, defaultValue = void 0, auto = false, mergeStrategy = null) {
|
|
1345
2750
|
super("array", required, defaultValue, auto, mergeStrategy);
|
|
1346
|
-
this.
|
|
2751
|
+
this._item = itemBuilder;
|
|
1347
2752
|
}
|
|
1348
2753
|
optional() {
|
|
1349
2754
|
return new _ArrayFieldBuilder(
|
|
1350
|
-
|
|
2755
|
+
this._item,
|
|
1351
2756
|
false,
|
|
1352
2757
|
this._defaultValue,
|
|
1353
2758
|
this._auto,
|
|
@@ -1355,26 +2760,14 @@ var ArrayFieldBuilder = class _ArrayFieldBuilder extends FieldBuilder {
|
|
|
1355
2760
|
);
|
|
1356
2761
|
}
|
|
1357
2762
|
default(value) {
|
|
1358
|
-
return new _ArrayFieldBuilder(
|
|
1359
|
-
new FieldBuilder(this._itemKind),
|
|
1360
|
-
false,
|
|
1361
|
-
value,
|
|
1362
|
-
this._auto,
|
|
1363
|
-
this._mergeStrategy
|
|
1364
|
-
);
|
|
2763
|
+
return new _ArrayFieldBuilder(this._item, false, value, this._auto, this._mergeStrategy);
|
|
1365
2764
|
}
|
|
1366
2765
|
auto() {
|
|
1367
|
-
return new _ArrayFieldBuilder(
|
|
1368
|
-
new FieldBuilder(this._itemKind),
|
|
1369
|
-
false,
|
|
1370
|
-
void 0,
|
|
1371
|
-
true,
|
|
1372
|
-
this._mergeStrategy
|
|
1373
|
-
);
|
|
2766
|
+
return new _ArrayFieldBuilder(this._item, false, void 0, true, this._mergeStrategy);
|
|
1374
2767
|
}
|
|
1375
2768
|
merge(strategy) {
|
|
1376
2769
|
return new _ArrayFieldBuilder(
|
|
1377
|
-
|
|
2770
|
+
this._item,
|
|
1378
2771
|
this._required,
|
|
1379
2772
|
this._defaultValue,
|
|
1380
2773
|
this._auto,
|
|
@@ -1388,7 +2781,7 @@ var ArrayFieldBuilder = class _ArrayFieldBuilder extends FieldBuilder {
|
|
|
1388
2781
|
defaultValue: this._defaultValue,
|
|
1389
2782
|
auto: this._auto,
|
|
1390
2783
|
enumValues: null,
|
|
1391
|
-
itemKind: this.
|
|
2784
|
+
itemKind: this._item._build().kind,
|
|
1392
2785
|
mergeStrategy: this._mergeStrategy,
|
|
1393
2786
|
transitions: null
|
|
1394
2787
|
};
|
|
@@ -1522,6 +2915,9 @@ var SecretFieldBuilder = class _SecretFieldBuilder extends FieldBuilder {
|
|
|
1522
2915
|
this._mergeStrategy
|
|
1523
2916
|
);
|
|
1524
2917
|
}
|
|
2918
|
+
default(value) {
|
|
2919
|
+
return new _SecretFieldBuilder(this._secretMode, false, value, this._auto, this._mergeStrategy);
|
|
2920
|
+
}
|
|
1525
2921
|
auto() {
|
|
1526
2922
|
return new _SecretFieldBuilder(this._secretMode, false, void 0, true, this._mergeStrategy);
|
|
1527
2923
|
}
|
|
@@ -1548,42 +2944,220 @@ var SecretFieldBuilder = class _SecretFieldBuilder extends FieldBuilder {
|
|
|
1548
2944
|
};
|
|
1549
2945
|
}
|
|
1550
2946
|
};
|
|
2947
|
+
function scalar(kind) {
|
|
2948
|
+
return new FieldBuilder(kind, true, void 0, false);
|
|
2949
|
+
}
|
|
1551
2950
|
var t = {
|
|
2951
|
+
/** A string field. */
|
|
1552
2952
|
string() {
|
|
1553
|
-
return
|
|
2953
|
+
return scalar("string");
|
|
1554
2954
|
},
|
|
2955
|
+
/** A finite-number field. */
|
|
1555
2956
|
number() {
|
|
1556
|
-
return
|
|
2957
|
+
return scalar("number");
|
|
1557
2958
|
},
|
|
2959
|
+
/** A boolean field. */
|
|
1558
2960
|
boolean() {
|
|
1559
|
-
return
|
|
2961
|
+
return scalar("boolean");
|
|
1560
2962
|
},
|
|
2963
|
+
/** A timestamp field: integer milliseconds since the epoch (not a `Date`). */
|
|
1561
2964
|
timestamp() {
|
|
1562
|
-
return
|
|
2965
|
+
return scalar("timestamp");
|
|
1563
2966
|
},
|
|
2967
|
+
/** A collaborative rich-text field (Yjs). Reads return the Yjs update bytes. */
|
|
1564
2968
|
richtext() {
|
|
1565
|
-
return
|
|
2969
|
+
return scalar("richtext");
|
|
1566
2970
|
},
|
|
2971
|
+
/** An enum field whose value is one of `values`. */
|
|
1567
2972
|
enum(values) {
|
|
2973
|
+
assertStorableEnumValues(values);
|
|
1568
2974
|
return new EnumFieldBuilder(values, true, void 0, false);
|
|
1569
2975
|
},
|
|
2976
|
+
/** An array field whose items have the type of `itemBuilder`. */
|
|
1570
2977
|
array(itemBuilder) {
|
|
1571
2978
|
return new ArrayFieldBuilder(itemBuilder, true, void 0, false);
|
|
1572
2979
|
},
|
|
2980
|
+
/** A structured object field whose keys are declared by nested builders. */
|
|
1573
2981
|
object(fields) {
|
|
1574
2982
|
return new ObjectFieldBuilder(fields, true, void 0, false);
|
|
1575
2983
|
},
|
|
2984
|
+
/** A dynamic JSON field typed as `T` (compile-time only; not validated against `T`). */
|
|
1576
2985
|
json() {
|
|
1577
2986
|
return new JsonFieldBuilder("json", true, void 0, false);
|
|
1578
2987
|
},
|
|
2988
|
+
/** A blob reference field (`BlobRef`); the bytes live in the blob store. */
|
|
1579
2989
|
blob() {
|
|
1580
|
-
return
|
|
2990
|
+
return scalar("blob");
|
|
1581
2991
|
},
|
|
2992
|
+
/** A secret field (password, token): redacted from traces, hashed or encrypted at rest. */
|
|
1582
2993
|
secret() {
|
|
1583
2994
|
return new SecretFieldBuilder("encrypted", true, void 0, false);
|
|
1584
2995
|
}
|
|
1585
2996
|
};
|
|
1586
2997
|
|
|
2998
|
+
// src/schema/value-domain.ts
|
|
2999
|
+
var TIMESTAMP_MIN_MS = -864e13;
|
|
3000
|
+
var TIMESTAMP_MAX_MS = 864e13;
|
|
3001
|
+
var MAX_VALUE_DEPTH = 64;
|
|
3002
|
+
var DEFAULT_MAX_OPERATION_BYTES = 256 * 1024;
|
|
3003
|
+
function timestampDomainViolation(value) {
|
|
3004
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
3005
|
+
return `must be a timestamp (integer milliseconds), got ${describe(value)}`;
|
|
3006
|
+
}
|
|
3007
|
+
if (!Number.isInteger(value)) {
|
|
3008
|
+
return `must be a timestamp in whole milliseconds, got ${String(value)}. Round it (Math.round) or use Date.getTime()`;
|
|
3009
|
+
}
|
|
3010
|
+
if (value < TIMESTAMP_MIN_MS || value > TIMESTAMP_MAX_MS) {
|
|
3011
|
+
return `must be a timestamp between ${TIMESTAMP_MIN_MS} and ${TIMESTAMP_MAX_MS} (the range of a JavaScript Date), got ${String(value)}`;
|
|
3012
|
+
}
|
|
3013
|
+
return null;
|
|
3014
|
+
}
|
|
3015
|
+
function valueDepthViolation(value, maxDepth = MAX_VALUE_DEPTH) {
|
|
3016
|
+
const walk = (current, depth, path) => {
|
|
3017
|
+
if (current === null || typeof current !== "object") return null;
|
|
3018
|
+
if (depth > maxDepth) return path;
|
|
3019
|
+
if (current instanceof Uint8Array || current instanceof ArrayBuffer) return null;
|
|
3020
|
+
const entries = Array.isArray(current) ? current.map((item, index) => [`[${index}]`, item]) : Object.entries(current).map(
|
|
3021
|
+
([key, item]) => [`.${key}`, item]
|
|
3022
|
+
);
|
|
3023
|
+
for (const [suffix, item] of entries) {
|
|
3024
|
+
const found = walk(item, depth + 1, `${path}${suffix}`);
|
|
3025
|
+
if (found !== null) return found;
|
|
3026
|
+
}
|
|
3027
|
+
return null;
|
|
3028
|
+
};
|
|
3029
|
+
return walk(value, 1, "");
|
|
3030
|
+
}
|
|
3031
|
+
function protoKeyViolation(value) {
|
|
3032
|
+
const walk = (current, path) => {
|
|
3033
|
+
if (current === null || typeof current !== "object") return null;
|
|
3034
|
+
if (current instanceof Uint8Array || current instanceof ArrayBuffer) return null;
|
|
3035
|
+
if (Array.isArray(current)) {
|
|
3036
|
+
for (let index = 0; index < current.length; index++) {
|
|
3037
|
+
const found = walk(current[index], `${path}[${index}]`);
|
|
3038
|
+
if (found !== null) return found;
|
|
3039
|
+
}
|
|
3040
|
+
return null;
|
|
3041
|
+
}
|
|
3042
|
+
if (Object.prototype.hasOwnProperty.call(current, "__proto__")) return `${path}.__proto__`;
|
|
3043
|
+
for (const [key, item] of Object.entries(current)) {
|
|
3044
|
+
const found = walk(item, `${path}.${key}`);
|
|
3045
|
+
if (found !== null) return found;
|
|
3046
|
+
}
|
|
3047
|
+
return null;
|
|
3048
|
+
};
|
|
3049
|
+
return walk(value, "");
|
|
3050
|
+
}
|
|
3051
|
+
function operationValueViolation(descriptor, value) {
|
|
3052
|
+
if (value === null || value === void 0) return null;
|
|
3053
|
+
switch (descriptor.kind) {
|
|
3054
|
+
case "string":
|
|
3055
|
+
case "secret":
|
|
3056
|
+
return typeof value === "string" ? null : `must be a string, got ${describe(value)}`;
|
|
3057
|
+
case "enum":
|
|
3058
|
+
if (typeof value !== "string") return `must be a string (enum), got ${describe(value)}`;
|
|
3059
|
+
if (descriptor.enumValues && !descriptor.enumValues.includes(value)) {
|
|
3060
|
+
return `must be one of: ${descriptor.enumValues.join(", ")}. Got "${value}"`;
|
|
3061
|
+
}
|
|
3062
|
+
return null;
|
|
3063
|
+
case "number":
|
|
3064
|
+
return typeof value === "number" && Number.isFinite(value) ? null : `must be a finite number, got ${describe(value)}`;
|
|
3065
|
+
case "boolean":
|
|
3066
|
+
return typeof value === "boolean" ? null : `must be a boolean, got ${describe(value)}`;
|
|
3067
|
+
case "timestamp":
|
|
3068
|
+
return timestampDomainViolation(value);
|
|
3069
|
+
case "richtext":
|
|
3070
|
+
return typeof value === "string" || isKoraBytesValue(value) || value instanceof Uint8Array ? null : `must be richtext (Yjs bytes or a string), got ${describe(value)}`;
|
|
3071
|
+
case "blob":
|
|
3072
|
+
return isBlobRef(value) ? null : `must be a BlobRef, got ${describe(value)}`;
|
|
3073
|
+
case "array": {
|
|
3074
|
+
if (!Array.isArray(value)) return `must be an array, got ${describe(value)}`;
|
|
3075
|
+
const item = descriptor.itemKind;
|
|
3076
|
+
for (let index = 0; index < value.length; index++) {
|
|
3077
|
+
const element = value[index];
|
|
3078
|
+
if (item === "timestamp") {
|
|
3079
|
+
const violation = timestampDomainViolation(element);
|
|
3080
|
+
if (violation) return `[${index}] ${violation}`;
|
|
3081
|
+
} else if (item === "number" && (typeof element !== "number" || !Number.isFinite(element))) {
|
|
3082
|
+
return `[${index}] must be a finite number, got ${describe(element)}`;
|
|
3083
|
+
} else if ((item === "string" || item === "enum") && typeof element !== "string") {
|
|
3084
|
+
return `[${index}] must be a string, got ${describe(element)}`;
|
|
3085
|
+
} else if (item === "boolean" && typeof element !== "boolean") {
|
|
3086
|
+
return `[${index}] must be a boolean, got ${describe(element)}`;
|
|
3087
|
+
}
|
|
3088
|
+
}
|
|
3089
|
+
return structureViolation(value);
|
|
3090
|
+
}
|
|
3091
|
+
case "object": {
|
|
3092
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
3093
|
+
return `must be a plain object, got ${describe(value)}`;
|
|
3094
|
+
}
|
|
3095
|
+
for (const [name, nested] of Object.entries(descriptor.nestedFields ?? {})) {
|
|
3096
|
+
const violation = operationValueViolation(nested, value[name]);
|
|
3097
|
+
if (violation) return `.${name} ${violation}`;
|
|
3098
|
+
}
|
|
3099
|
+
return structureViolation(value);
|
|
3100
|
+
}
|
|
3101
|
+
case "json":
|
|
3102
|
+
return structureViolation(value);
|
|
3103
|
+
}
|
|
3104
|
+
}
|
|
3105
|
+
var WIRE_BYTES_KEY = "__kora_bytes__";
|
|
3106
|
+
function reservedWireShapeViolation(value) {
|
|
3107
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return null;
|
|
3108
|
+
const keys = Object.keys(value);
|
|
3109
|
+
return keys.length === 1 && keys[0] === WIRE_BYTES_KEY ? `is an object whose only key is "${WIRE_BYTES_KEY}", which the sync wire reserves for binary values; add another key or rename it` : null;
|
|
3110
|
+
}
|
|
3111
|
+
function structureViolation(value) {
|
|
3112
|
+
const reserved = reservedWireShapeViolation(value);
|
|
3113
|
+
if (reserved !== null) return reserved;
|
|
3114
|
+
const deep = valueDepthViolation(value);
|
|
3115
|
+
if (deep !== null) return `nests deeper than ${MAX_VALUE_DEPTH} levels at "${deep}"`;
|
|
3116
|
+
const proto = protoKeyViolation(value);
|
|
3117
|
+
if (proto !== null) return `holds a "__proto__" key at "${proto}", which is not supported`;
|
|
3118
|
+
const walk = (current, path) => {
|
|
3119
|
+
if (typeof current === "number") {
|
|
3120
|
+
return Number.isFinite(current) ? null : `holds ${String(current)} at "${path}"`;
|
|
3121
|
+
}
|
|
3122
|
+
if (current === null || typeof current !== "object") return null;
|
|
3123
|
+
const entries = Array.isArray(current) ? current.map((item, index) => [`${path}[${index}]`, item]) : Object.entries(current).map(
|
|
3124
|
+
([key, item]) => [`${path}.${key}`, item]
|
|
3125
|
+
);
|
|
3126
|
+
for (const [next, item] of entries) {
|
|
3127
|
+
const found = walk(item, next);
|
|
3128
|
+
if (found !== null) return found;
|
|
3129
|
+
}
|
|
3130
|
+
return null;
|
|
3131
|
+
};
|
|
3132
|
+
return walk(value, "");
|
|
3133
|
+
}
|
|
3134
|
+
function measureOperationBytes(op2) {
|
|
3135
|
+
return utf8Length(JSON.stringify(op2));
|
|
3136
|
+
}
|
|
3137
|
+
function utf8Length(text) {
|
|
3138
|
+
let bytes = 0;
|
|
3139
|
+
for (let i = 0; i < text.length; i++) {
|
|
3140
|
+
const code = text.charCodeAt(i);
|
|
3141
|
+
if (code < 128) bytes += 1;
|
|
3142
|
+
else if (code < 2048) bytes += 2;
|
|
3143
|
+
else if (code >= 55296 && code <= 56319 && i + 1 < text.length) {
|
|
3144
|
+
const next = text.charCodeAt(i + 1);
|
|
3145
|
+
if (next >= 56320 && next <= 57343) {
|
|
3146
|
+
bytes += 4;
|
|
3147
|
+
i++;
|
|
3148
|
+
} else bytes += 3;
|
|
3149
|
+
} else bytes += 3;
|
|
3150
|
+
}
|
|
3151
|
+
return bytes;
|
|
3152
|
+
}
|
|
3153
|
+
function describe(value) {
|
|
3154
|
+
if (value === null) return "null";
|
|
3155
|
+
if (Array.isArray(value)) return "array";
|
|
3156
|
+
if (typeof value === "number") return Number.isFinite(value) ? "number" : String(value);
|
|
3157
|
+
if (value instanceof Date) return "Date";
|
|
3158
|
+
return typeof value;
|
|
3159
|
+
}
|
|
3160
|
+
|
|
1587
3161
|
// src/schema/validation.ts
|
|
1588
3162
|
function validateRecord(collection, collectionDef, data, operationType) {
|
|
1589
3163
|
if (operationType === "delete") {
|
|
@@ -1614,7 +3188,7 @@ function validateRecord(collection, collectionDef, data, operationType) {
|
|
|
1614
3188
|
result[fieldName] = value;
|
|
1615
3189
|
} else if (value !== void 0 && value !== null) {
|
|
1616
3190
|
validateFieldValue(collection, fieldName, descriptor, value);
|
|
1617
|
-
result[fieldName] = value;
|
|
3191
|
+
result[fieldName] = canonicalFieldValue(collection, fieldName, descriptor, value);
|
|
1618
3192
|
} else {
|
|
1619
3193
|
result[fieldName] = value;
|
|
1620
3194
|
}
|
|
@@ -1638,10 +3212,29 @@ function validateRecord(collection, collectionDef, data, operationType) {
|
|
|
1638
3212
|
continue;
|
|
1639
3213
|
}
|
|
1640
3214
|
validateFieldValue(collection, fieldName, descriptor, value);
|
|
1641
|
-
result[fieldName] = value;
|
|
3215
|
+
result[fieldName] = canonicalFieldValue(collection, fieldName, descriptor, value);
|
|
1642
3216
|
}
|
|
1643
3217
|
return result;
|
|
1644
3218
|
}
|
|
3219
|
+
function canonicalFieldValue(collection, fieldName, descriptor, value) {
|
|
3220
|
+
if (descriptor.kind === "richtext") return value;
|
|
3221
|
+
try {
|
|
3222
|
+
return canonicalValue(value, `${collection}.${fieldName}`);
|
|
3223
|
+
} catch (error) {
|
|
3224
|
+
if (error instanceof NonCanonicalValueError) {
|
|
3225
|
+
throw new SchemaValidationError(
|
|
3226
|
+
`Field "${fieldName}" in collection "${collection}": ${error.message}`,
|
|
3227
|
+
{
|
|
3228
|
+
collection,
|
|
3229
|
+
field: fieldName,
|
|
3230
|
+
path: error.path,
|
|
3231
|
+
receivedType: error.receivedType
|
|
3232
|
+
}
|
|
3233
|
+
);
|
|
3234
|
+
}
|
|
3235
|
+
throw error;
|
|
3236
|
+
}
|
|
3237
|
+
}
|
|
1645
3238
|
function validateFieldValue(collection, fieldName, descriptor, value) {
|
|
1646
3239
|
switch (descriptor.kind) {
|
|
1647
3240
|
case "string": {
|
|
@@ -1654,9 +3247,9 @@ function validateFieldValue(collection, fieldName, descriptor, value) {
|
|
|
1654
3247
|
break;
|
|
1655
3248
|
}
|
|
1656
3249
|
case "number": {
|
|
1657
|
-
if (typeof value !== "number" || Number.
|
|
3250
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
1658
3251
|
throw new SchemaValidationError(
|
|
1659
|
-
`Field "${fieldName}" in collection "${collection}" must be a number, got ${typeof value}`,
|
|
3252
|
+
`Field "${fieldName}" in collection "${collection}" must be a finite number, got ${typeof value === "number" ? String(value) : typeof value}`,
|
|
1660
3253
|
{ collection, field: fieldName, expectedType: "number", receivedType: typeof value }
|
|
1661
3254
|
);
|
|
1662
3255
|
}
|
|
@@ -1672,14 +3265,16 @@ function validateFieldValue(collection, fieldName, descriptor, value) {
|
|
|
1672
3265
|
break;
|
|
1673
3266
|
}
|
|
1674
3267
|
case "timestamp": {
|
|
1675
|
-
|
|
3268
|
+
const violation = timestampDomainViolation(value);
|
|
3269
|
+
if (violation !== null) {
|
|
1676
3270
|
throw new SchemaValidationError(
|
|
1677
|
-
`Field "${fieldName}" in collection "${collection}"
|
|
3271
|
+
`Field "${fieldName}" in collection "${collection}" ${violation}`,
|
|
1678
3272
|
{
|
|
1679
3273
|
collection,
|
|
1680
3274
|
field: fieldName,
|
|
1681
3275
|
expectedType: "timestamp",
|
|
1682
|
-
receivedType: typeof value
|
|
3276
|
+
receivedType: typeof value,
|
|
3277
|
+
received: value
|
|
1683
3278
|
}
|
|
1684
3279
|
);
|
|
1685
3280
|
}
|
|
@@ -1716,6 +3311,14 @@ function validateFieldValue(collection, fieldName, descriptor, value) {
|
|
|
1716
3311
|
const expectedType = jsTypeForKind(descriptor.itemKind);
|
|
1717
3312
|
for (let i = 0; i < value.length; i++) {
|
|
1718
3313
|
const item = value[i];
|
|
3314
|
+
if ((descriptor.itemKind === "timestamp" || descriptor.itemKind === "number") && item !== null && item !== void 0) {
|
|
3315
|
+
validateFieldValue(
|
|
3316
|
+
collection,
|
|
3317
|
+
`${fieldName}[${i}]`,
|
|
3318
|
+
{ ...descriptor, kind: descriptor.itemKind, itemKind: null },
|
|
3319
|
+
item
|
|
3320
|
+
);
|
|
3321
|
+
}
|
|
1719
3322
|
if (!matchesJsType(item, expectedType)) {
|
|
1720
3323
|
throw new SchemaValidationError(
|
|
1721
3324
|
`Field "${fieldName}[${i}]" in collection "${collection}" must be a ${descriptor.itemKind}, got ${typeof item}`,
|
|
@@ -1729,6 +3332,7 @@ function validateFieldValue(collection, fieldName, descriptor, value) {
|
|
|
1729
3332
|
}
|
|
1730
3333
|
}
|
|
1731
3334
|
}
|
|
3335
|
+
assertStructureInDomain(collection, fieldName, value);
|
|
1732
3336
|
break;
|
|
1733
3337
|
}
|
|
1734
3338
|
case "richtext": {
|
|
@@ -1746,7 +3350,7 @@ function validateFieldValue(collection, fieldName, descriptor, value) {
|
|
|
1746
3350
|
break;
|
|
1747
3351
|
}
|
|
1748
3352
|
case "object": {
|
|
1749
|
-
if (!
|
|
3353
|
+
if (!isPlainObject2(value)) {
|
|
1750
3354
|
throw new SchemaValidationError(
|
|
1751
3355
|
`Field "${fieldName}" in collection "${collection}" must be a plain object, got ${describeType(value)}`,
|
|
1752
3356
|
{
|
|
@@ -1770,6 +3374,7 @@ function validateFieldValue(collection, fieldName, descriptor, value) {
|
|
|
1770
3374
|
}
|
|
1771
3375
|
}
|
|
1772
3376
|
}
|
|
3377
|
+
assertStructureInDomain(collection, fieldName, value);
|
|
1773
3378
|
break;
|
|
1774
3379
|
}
|
|
1775
3380
|
case "json": {
|
|
@@ -1786,6 +3391,7 @@ function validateFieldValue(collection, fieldName, descriptor, value) {
|
|
|
1786
3391
|
}
|
|
1787
3392
|
);
|
|
1788
3393
|
}
|
|
3394
|
+
assertStructureInDomain(collection, fieldName, value);
|
|
1789
3395
|
break;
|
|
1790
3396
|
}
|
|
1791
3397
|
case "blob": {
|
|
@@ -1813,17 +3419,47 @@ function validateFieldValue(collection, fieldName, descriptor, value) {
|
|
|
1813
3419
|
}
|
|
1814
3420
|
}
|
|
1815
3421
|
}
|
|
1816
|
-
function
|
|
3422
|
+
function assertStructureInDomain(collection, fieldName, value) {
|
|
3423
|
+
const reserved = reservedWireShapeViolation(value);
|
|
3424
|
+
if (reserved !== null) {
|
|
3425
|
+
throw new SchemaValidationError(
|
|
3426
|
+
`Field "${fieldName}" in collection "${collection}" ${reserved}.`,
|
|
3427
|
+
{ collection, field: fieldName }
|
|
3428
|
+
);
|
|
3429
|
+
}
|
|
3430
|
+
const deep = valueDepthViolation(value);
|
|
3431
|
+
if (deep !== null) {
|
|
3432
|
+
throw new SchemaValidationError(
|
|
3433
|
+
`Field "${fieldName}" in collection "${collection}" nests deeper than ${MAX_VALUE_DEPTH} levels (at "${fieldName}${deep}"). Flatten the value.`,
|
|
3434
|
+
{ collection, field: fieldName, path: `${fieldName}${deep}`, maxDepth: MAX_VALUE_DEPTH }
|
|
3435
|
+
);
|
|
3436
|
+
}
|
|
3437
|
+
const proto = protoKeyViolation(value);
|
|
3438
|
+
if (proto !== null) {
|
|
3439
|
+
throw new SchemaValidationError(
|
|
3440
|
+
`Field "${fieldName}" in collection "${collection}" holds a "__proto__" key (at "${fieldName}${proto}"), which cannot be stored and synced unchanged. Rename the key.`,
|
|
3441
|
+
{ collection, field: fieldName, path: `${fieldName}${proto}` }
|
|
3442
|
+
);
|
|
3443
|
+
}
|
|
3444
|
+
}
|
|
3445
|
+
function assertResolvedFieldValue(collection, fieldName, descriptor, value) {
|
|
3446
|
+
if (value === null || value === void 0 || descriptor.kind === "richtext") return;
|
|
3447
|
+
validateFieldValue(collection, fieldName, descriptor, value);
|
|
3448
|
+
}
|
|
3449
|
+
function isPlainObject2(value) {
|
|
1817
3450
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
1818
3451
|
return false;
|
|
1819
3452
|
}
|
|
1820
3453
|
const proto = Object.getPrototypeOf(value);
|
|
1821
3454
|
return proto === null || proto === Object.prototype;
|
|
1822
3455
|
}
|
|
1823
|
-
function findNonJsonSerializable(value, path, seen = /* @__PURE__ */ new WeakSet()) {
|
|
3456
|
+
function findNonJsonSerializable(value, path, seen = /* @__PURE__ */ new WeakSet(), nested = false) {
|
|
1824
3457
|
if (value === null) {
|
|
1825
3458
|
return null;
|
|
1826
3459
|
}
|
|
3460
|
+
if (value === void 0 && nested) {
|
|
3461
|
+
return null;
|
|
3462
|
+
}
|
|
1827
3463
|
const type = typeof value;
|
|
1828
3464
|
if (type === "string" || type === "number" || type === "boolean") {
|
|
1829
3465
|
if (type === "number" && !Number.isFinite(value)) {
|
|
@@ -1836,7 +3472,7 @@ function findNonJsonSerializable(value, path, seen = /* @__PURE__ */ new WeakSet
|
|
|
1836
3472
|
}
|
|
1837
3473
|
if (Array.isArray(value)) {
|
|
1838
3474
|
for (let index = 0; index < value.length; index++) {
|
|
1839
|
-
const invalid = findNonJsonSerializable(value[index], `${path}[${index}]`, seen);
|
|
3475
|
+
const invalid = findNonJsonSerializable(value[index], `${path}[${index}]`, seen, true);
|
|
1840
3476
|
if (invalid) return invalid;
|
|
1841
3477
|
}
|
|
1842
3478
|
return null;
|
|
@@ -1848,7 +3484,7 @@ function findNonJsonSerializable(value, path, seen = /* @__PURE__ */ new WeakSet
|
|
|
1848
3484
|
}
|
|
1849
3485
|
seen.add(objectValue);
|
|
1850
3486
|
for (const [key, nestedValue] of Object.entries(objectValue)) {
|
|
1851
|
-
const invalid = findNonJsonSerializable(nestedValue, `${path}.${key}`, seen);
|
|
3487
|
+
const invalid = findNonJsonSerializable(nestedValue, `${path}.${key}`, seen, true);
|
|
1852
3488
|
if (invalid) return invalid;
|
|
1853
3489
|
}
|
|
1854
3490
|
seen.delete(objectValue);
|
|
@@ -1967,6 +3603,17 @@ function buildScopeMap(schema, scopeValues) {
|
|
|
1967
3603
|
return result;
|
|
1968
3604
|
}
|
|
1969
3605
|
|
|
3606
|
+
// src/scopes/scope-grant.ts
|
|
3607
|
+
var SCOPE_CLAIMS_KEY = "$claims";
|
|
3608
|
+
function claimScopes(claims, explicit) {
|
|
3609
|
+
return { ...explicit ?? {}, [SCOPE_CLAIMS_KEY]: { ...claims } };
|
|
3610
|
+
}
|
|
3611
|
+
function getScopeClaims(grant) {
|
|
3612
|
+
if (!grant) return null;
|
|
3613
|
+
const claims = grant[SCOPE_CLAIMS_KEY];
|
|
3614
|
+
return claims && typeof claims === "object" ? claims : null;
|
|
3615
|
+
}
|
|
3616
|
+
|
|
1970
3617
|
// src/scopes/extract-scope-from-claims.ts
|
|
1971
3618
|
function collectSchemaScopeFields(schema) {
|
|
1972
3619
|
return collectSchemaScopeValueKeys(schema);
|
|
@@ -2021,29 +3668,6 @@ function defaultSequenceFormat(name) {
|
|
|
2021
3668
|
return `${name}-{seq:4}`;
|
|
2022
3669
|
}
|
|
2023
3670
|
|
|
2024
|
-
// src/migration/apply-operation-transforms.ts
|
|
2025
|
-
var MAX_TRANSFORM_STEPS = 32;
|
|
2026
|
-
function applyOperationTransforms(operation, targetSchemaVersion, transforms) {
|
|
2027
|
-
let current = operation;
|
|
2028
|
-
for (let step = 0; step < MAX_TRANSFORM_STEPS; step++) {
|
|
2029
|
-
if (current.schemaVersion === targetSchemaVersion) {
|
|
2030
|
-
return current;
|
|
2031
|
-
}
|
|
2032
|
-
const transform = transforms.find(
|
|
2033
|
-
(candidate) => candidate.fromVersion === current.schemaVersion
|
|
2034
|
-
);
|
|
2035
|
-
if (!transform) {
|
|
2036
|
-
return null;
|
|
2037
|
-
}
|
|
2038
|
-
const next = transform.transform(current);
|
|
2039
|
-
if (next === null) {
|
|
2040
|
-
return null;
|
|
2041
|
-
}
|
|
2042
|
-
current = next;
|
|
2043
|
-
}
|
|
2044
|
-
return null;
|
|
2045
|
-
}
|
|
2046
|
-
|
|
2047
3671
|
// src/migrations/migration-builder.ts
|
|
2048
3672
|
var RollbackBuilder = class {
|
|
2049
3673
|
_steps = [];
|
|
@@ -2149,17 +3773,29 @@ var MigrationBuilder = class _MigrationBuilder {
|
|
|
2149
3773
|
}
|
|
2150
3774
|
/**
|
|
2151
3775
|
* Backfill records in a collection using a transform function.
|
|
2152
|
-
* The transform receives each record
|
|
2153
|
-
* Runs after structural changes (addField, renameField,
|
|
3776
|
+
* The transform receives each live record (typed values, as the app reads them) and
|
|
3777
|
+
* returns the fields to change. Runs after structural changes (addField, renameField,
|
|
3778
|
+
* etc.), in the same transaction as them and the schema version write.
|
|
3779
|
+
*
|
|
3780
|
+
* Changed records are written as update operations (mutation name `migration:v<N>`)
|
|
3781
|
+
* through the normal local write path, so backfilled values sync. Pass
|
|
3782
|
+
* `{ localOnly: true }` for device-local data that must not sync.
|
|
2154
3783
|
*
|
|
2155
3784
|
* @param collection - The collection name
|
|
2156
3785
|
* @param transform - Forward transform function
|
|
2157
|
-
* @param
|
|
3786
|
+
* @param reverseOrOptions - Reverse transform for rollback support, or options
|
|
2158
3787
|
*/
|
|
2159
|
-
backfill(collection, transform,
|
|
3788
|
+
backfill(collection, transform, reverseOrOptions) {
|
|
3789
|
+
const options = typeof reverseOrOptions === "function" ? { reverseTransform: reverseOrOptions } : reverseOrOptions ?? {};
|
|
2160
3790
|
return new _MigrationBuilder([
|
|
2161
3791
|
...this.steps,
|
|
2162
|
-
{
|
|
3792
|
+
{
|
|
3793
|
+
type: "backfill",
|
|
3794
|
+
collection,
|
|
3795
|
+
transform,
|
|
3796
|
+
...options.reverseTransform ? { reverseTransform: options.reverseTransform } : {},
|
|
3797
|
+
...options.localOnly ? { localOnly: true } : {}
|
|
3798
|
+
}
|
|
2163
3799
|
]);
|
|
2164
3800
|
}
|
|
2165
3801
|
/**
|
|
@@ -2306,11 +3942,13 @@ function migrationStepsToSQL(steps) {
|
|
|
2306
3942
|
break;
|
|
2307
3943
|
case "addIndex":
|
|
2308
3944
|
statements.push(
|
|
2309
|
-
`CREATE INDEX IF NOT EXISTS
|
|
3945
|
+
`CREATE INDEX IF NOT EXISTS ${quoteIdent(collectionIndexName(step.collection, step.field))} ON ${quoteIdent(step.collection)} (${quoteIdent(step.field)})`
|
|
2310
3946
|
);
|
|
2311
3947
|
break;
|
|
2312
3948
|
case "removeIndex":
|
|
2313
|
-
statements.push(
|
|
3949
|
+
statements.push(
|
|
3950
|
+
`DROP INDEX IF EXISTS ${quoteIdent(collectionIndexName(step.collection, step.field))}`
|
|
3951
|
+
);
|
|
2314
3952
|
break;
|
|
2315
3953
|
case "backfill":
|
|
2316
3954
|
break;
|
|
@@ -2323,18 +3961,14 @@ function rollbackStepsToSQL(migration) {
|
|
|
2323
3961
|
return migrationStepsToSQL(rollbackSteps);
|
|
2324
3962
|
}
|
|
2325
3963
|
function addFieldSQL(collection, field, descriptor) {
|
|
2326
|
-
const sqlType =
|
|
3964
|
+
const sqlType = mapFieldType(descriptor);
|
|
2327
3965
|
const parts = [`ALTER TABLE ${quoteIdent(collection)} ADD COLUMN ${quoteIdent(field)}`, sqlType];
|
|
2328
3966
|
if (descriptor.defaultValue !== void 0) {
|
|
2329
|
-
parts.push(`DEFAULT ${
|
|
2330
|
-
}
|
|
2331
|
-
if (descriptor.kind === "enum" && descriptor.enumValues) {
|
|
2332
|
-
const values = descriptor.enumValues.map((v) => `'${v}'`).join(", ");
|
|
2333
|
-
parts.push(`CHECK (${quoteIdent(field)} IN (${values}))`);
|
|
3967
|
+
parts.push(`DEFAULT ${sqlDefaultLiteral(descriptor.defaultValue)}`);
|
|
2334
3968
|
}
|
|
2335
3969
|
return parts.join(" ");
|
|
2336
3970
|
}
|
|
2337
|
-
function
|
|
3971
|
+
function mapFieldType(descriptor) {
|
|
2338
3972
|
switch (descriptor.kind) {
|
|
2339
3973
|
case "string":
|
|
2340
3974
|
return "TEXT";
|
|
@@ -2360,13 +3994,6 @@ function mapFieldType2(descriptor) {
|
|
|
2360
3994
|
return "BLOB";
|
|
2361
3995
|
}
|
|
2362
3996
|
}
|
|
2363
|
-
function sqlDefault2(value) {
|
|
2364
|
-
if (value === null) return "NULL";
|
|
2365
|
-
if (typeof value === "string") return `'${value}'`;
|
|
2366
|
-
if (typeof value === "number") return String(value);
|
|
2367
|
-
if (typeof value === "boolean") return value ? "1" : "0";
|
|
2368
|
-
return `'${JSON.stringify(value)}'`;
|
|
2369
|
-
}
|
|
2370
3997
|
|
|
2371
3998
|
// src/codegen/proto-generator.ts
|
|
2372
3999
|
var SCALAR_TYPE_MAP = {
|
|
@@ -2623,50 +4250,91 @@ export {
|
|
|
2623
4250
|
CONNECTION_QUALITIES,
|
|
2624
4251
|
CausalTracker,
|
|
2625
4252
|
ClockDriftError,
|
|
4253
|
+
DEFAULT_MAX_OPERATION_BYTES,
|
|
4254
|
+
DEFAULT_OPERATION_HASH_VERSION,
|
|
2626
4255
|
EnumFieldBuilder,
|
|
4256
|
+
FOLD_RECORD_TRACE_FIELD,
|
|
4257
|
+
FOLD_STATE_VERSION,
|
|
2627
4258
|
FieldBuilder,
|
|
4259
|
+
FoldConfigurationError,
|
|
4260
|
+
FoldStateError,
|
|
2628
4261
|
HybridLogicalClock,
|
|
2629
4262
|
InvalidTimestampError,
|
|
4263
|
+
JsonFieldBuilder,
|
|
2630
4264
|
KORA_ERROR_FIX_SUGGESTIONS,
|
|
2631
4265
|
KoraError,
|
|
2632
4266
|
MAX_LOGICAL,
|
|
4267
|
+
MAX_VALUE_DEPTH,
|
|
2633
4268
|
MERGE_STRATEGIES,
|
|
2634
4269
|
MergeConflictError,
|
|
2635
4270
|
MigrationBuilder,
|
|
2636
4271
|
MigrationRollbackError,
|
|
2637
4272
|
MissingSecretKeyError,
|
|
4273
|
+
NonCanonicalValueError,
|
|
4274
|
+
ObjectFieldBuilder,
|
|
2638
4275
|
OperationError,
|
|
4276
|
+
OperationTooLargeError,
|
|
4277
|
+
OperationTransformCoverageError,
|
|
4278
|
+
OperationTransformError,
|
|
4279
|
+
RESERVED_NODE_ID_PREFIX,
|
|
2639
4280
|
RemoteClockDriftError,
|
|
2640
4281
|
RollbackBuilder,
|
|
4282
|
+
SCOPE_CLAIMS_KEY,
|
|
4283
|
+
SERVER_NODE_ID_PREFIX,
|
|
4284
|
+
STORED_TEXT_ESCAPE,
|
|
2641
4285
|
SchemaValidationError,
|
|
4286
|
+
SecretFieldBuilder,
|
|
2642
4287
|
StorageError,
|
|
2643
4288
|
SyncError,
|
|
4289
|
+
TIMESTAMP_MAX_MS,
|
|
4290
|
+
TIMESTAMP_MIN_MS,
|
|
4291
|
+
adaptFoldState,
|
|
2644
4292
|
advanceVector,
|
|
2645
4293
|
applyAtomicOp,
|
|
2646
4294
|
applyOperationTransforms,
|
|
4295
|
+
assertOperationTransformCoverage,
|
|
4296
|
+
assertResolvedFieldValue,
|
|
2647
4297
|
base64ToBytes,
|
|
2648
4298
|
buildScopeMap,
|
|
2649
4299
|
buildStateMachineConstraints,
|
|
2650
4300
|
bytesToBase64,
|
|
2651
4301
|
canAutoRollback,
|
|
4302
|
+
canonicalValue,
|
|
4303
|
+
canonicalizeLegacyOperation,
|
|
4304
|
+
canonicalizeOperationBody,
|
|
4305
|
+
canonicalizeProvenLegacyClear,
|
|
4306
|
+
claimScopes,
|
|
2652
4307
|
collectSchemaScopeFields,
|
|
2653
4308
|
collectSchemaScopeValueKeys,
|
|
2654
4309
|
computeDelta,
|
|
4310
|
+
computeOperationId,
|
|
2655
4311
|
createBlobRef,
|
|
4312
|
+
createFoldState,
|
|
2656
4313
|
createOperation,
|
|
2657
4314
|
createReversibleMigration,
|
|
4315
|
+
createSnapshotState,
|
|
2658
4316
|
createVersionVector,
|
|
2659
4317
|
decodeBytesFromOpData,
|
|
4318
|
+
decodeStoredJsonValue,
|
|
4319
|
+
decodeStoredText,
|
|
2660
4320
|
decryptSecret,
|
|
2661
4321
|
defaultApplyFailureReason,
|
|
2662
4322
|
defaultSequenceFormat,
|
|
2663
4323
|
defineSchema,
|
|
4324
|
+
deriveSideEffectOpId,
|
|
4325
|
+
deserializeFoldState,
|
|
2664
4326
|
deserializeVector,
|
|
2665
4327
|
dominates,
|
|
2666
4328
|
encodeBytesForOpData,
|
|
4329
|
+
encodeStoredJsonValue,
|
|
4330
|
+
encodeStoredText,
|
|
2667
4331
|
encryptSecret,
|
|
4332
|
+
expandFieldVersionedOperations,
|
|
2668
4333
|
extractScopeValuesFromClaims,
|
|
2669
4334
|
extractTimestamp,
|
|
4335
|
+
foldPlanFingerprint,
|
|
4336
|
+
foldPlanFingerprints,
|
|
4337
|
+
foldRecord,
|
|
2670
4338
|
formatSequenceValue,
|
|
2671
4339
|
generateFullDDL,
|
|
2672
4340
|
generateProtoDefinitions,
|
|
@@ -2674,35 +4342,61 @@ export {
|
|
|
2674
4342
|
generateSQL,
|
|
2675
4343
|
generateUUIDv7,
|
|
2676
4344
|
getCollectionScopeBindings,
|
|
4345
|
+
getFoldFieldVersionStrings,
|
|
4346
|
+
getFoldFieldVersions,
|
|
2677
4347
|
getKoraErrorFix,
|
|
4348
|
+
getScopeClaims,
|
|
2678
4349
|
getTransitionMap,
|
|
2679
4350
|
hasSchemaSyncRules,
|
|
2680
4351
|
hashBlob,
|
|
2681
4352
|
hashSecret,
|
|
2682
4353
|
isApplyFailure,
|
|
2683
4354
|
isAtomicOp,
|
|
4355
|
+
isAuthoritativeNodeId,
|
|
2684
4356
|
isBlobRef,
|
|
2685
4357
|
isCollectionSyncScoped,
|
|
4358
|
+
isFoldStateLive,
|
|
2686
4359
|
isKoraBytesValue,
|
|
2687
4360
|
isLegacyNumericByteObject,
|
|
4361
|
+
isReservedNodeId,
|
|
4362
|
+
isServerNodeId,
|
|
2688
4363
|
isValidOperation,
|
|
2689
4364
|
isValidUUIDv7,
|
|
4365
|
+
joinStates,
|
|
4366
|
+
materialize,
|
|
4367
|
+
measureOperationBytes,
|
|
4368
|
+
mergeArraySet,
|
|
4369
|
+
mergeOp,
|
|
2690
4370
|
mergeVectors,
|
|
2691
4371
|
migrate,
|
|
2692
4372
|
migrationStepsToSQL,
|
|
4373
|
+
mismatchedFoldFields,
|
|
4374
|
+
missingTransformPaths,
|
|
4375
|
+
needsStoredTextEncoding,
|
|
2693
4376
|
op,
|
|
4377
|
+
operationSchemaView,
|
|
4378
|
+
operationTransformsFingerprint,
|
|
4379
|
+
operationValueViolation,
|
|
4380
|
+
protoKeyViolation,
|
|
2694
4381
|
quoteIdent,
|
|
4382
|
+
replayFieldVersionsForRecord,
|
|
2695
4383
|
replayOperationsForRecord,
|
|
4384
|
+
reservedWireShapeViolation,
|
|
2696
4385
|
resolveAtomicOp,
|
|
2697
4386
|
revealSecret,
|
|
2698
4387
|
rollbackStepsToSQL,
|
|
4388
|
+
serializeFoldState,
|
|
2699
4389
|
serializeVector,
|
|
2700
4390
|
t,
|
|
4391
|
+
timestampDomainViolation,
|
|
2701
4392
|
toAtomicOp,
|
|
4393
|
+
toMergeTrace,
|
|
2702
4394
|
transformSecretFieldsForWrite,
|
|
2703
4395
|
validateRecord,
|
|
2704
4396
|
validateTransition,
|
|
4397
|
+
valueDepthViolation,
|
|
2705
4398
|
vectorsEqual,
|
|
4399
|
+
verifyOperationId,
|
|
2706
4400
|
verifyOperationIntegrity,
|
|
2707
4401
|
verifySecret,
|
|
2708
4402
|
verifySecretValue
|