@korajs/core 1.0.0-beta.11 → 1.0.0-beta.13
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-B3R_emUU.d.ts → build-scope-map-CIFa58oA.d.ts} +1 -1
- package/dist/{build-scope-map--0H8l6Tw.d.cts → build-scope-map-ClIq_doj.d.cts} +1 -1
- package/dist/{chunk-5IICSH6H.js → chunk-QUD3DMSD.js} +668 -24
- package/dist/chunk-QUD3DMSD.js.map +1 -0
- package/dist/events-BSU-0z5O.d.cts +1737 -0
- package/dist/events-BSU-0z5O.d.ts +1737 -0
- package/dist/index.cjs +2539 -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 +2025 -340
- 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-ByOZIQU-.d.ts +622 -0
- package/dist/sql-gen-DdOkMojr.d.cts +622 -0
- package/package.json +2 -1
- package/dist/chunk-5IICSH6H.js.map +0 -1
- package/dist/events-BPbYUbrE.d.cts +0 -999
- package/dist/events-BPbYUbrE.d.ts +0 -999
- package/dist/operation-DbynlVcp.d.cts +0 -164
- package/dist/operation-Dt0AtT8u.d.ts +0 -164
package/dist/index.cjs
CHANGED
|
@@ -27,50 +27,91 @@ __export(index_exports, {
|
|
|
27
27
|
CONNECTION_QUALITIES: () => CONNECTION_QUALITIES,
|
|
28
28
|
CausalTracker: () => CausalTracker,
|
|
29
29
|
ClockDriftError: () => ClockDriftError,
|
|
30
|
+
DEFAULT_MAX_OPERATION_BYTES: () => DEFAULT_MAX_OPERATION_BYTES,
|
|
31
|
+
DEFAULT_OPERATION_HASH_VERSION: () => DEFAULT_OPERATION_HASH_VERSION,
|
|
30
32
|
EnumFieldBuilder: () => EnumFieldBuilder,
|
|
33
|
+
FOLD_RECORD_TRACE_FIELD: () => FOLD_RECORD_TRACE_FIELD,
|
|
34
|
+
FOLD_STATE_VERSION: () => FOLD_STATE_VERSION,
|
|
31
35
|
FieldBuilder: () => FieldBuilder,
|
|
36
|
+
FoldConfigurationError: () => FoldConfigurationError,
|
|
37
|
+
FoldStateError: () => FoldStateError,
|
|
32
38
|
HybridLogicalClock: () => HybridLogicalClock,
|
|
33
39
|
InvalidTimestampError: () => InvalidTimestampError,
|
|
40
|
+
JsonFieldBuilder: () => JsonFieldBuilder,
|
|
34
41
|
KORA_ERROR_FIX_SUGGESTIONS: () => KORA_ERROR_FIX_SUGGESTIONS,
|
|
35
42
|
KoraError: () => KoraError,
|
|
36
43
|
MAX_LOGICAL: () => MAX_LOGICAL,
|
|
44
|
+
MAX_VALUE_DEPTH: () => MAX_VALUE_DEPTH,
|
|
37
45
|
MERGE_STRATEGIES: () => MERGE_STRATEGIES,
|
|
38
46
|
MergeConflictError: () => MergeConflictError,
|
|
39
47
|
MigrationBuilder: () => MigrationBuilder,
|
|
40
48
|
MigrationRollbackError: () => MigrationRollbackError,
|
|
41
49
|
MissingSecretKeyError: () => MissingSecretKeyError,
|
|
50
|
+
NonCanonicalValueError: () => NonCanonicalValueError,
|
|
51
|
+
ObjectFieldBuilder: () => ObjectFieldBuilder,
|
|
42
52
|
OperationError: () => OperationError,
|
|
53
|
+
OperationTooLargeError: () => OperationTooLargeError,
|
|
54
|
+
OperationTransformCoverageError: () => OperationTransformCoverageError,
|
|
55
|
+
OperationTransformError: () => OperationTransformError,
|
|
56
|
+
RESERVED_NODE_ID_PREFIX: () => RESERVED_NODE_ID_PREFIX,
|
|
43
57
|
RemoteClockDriftError: () => RemoteClockDriftError,
|
|
44
58
|
RollbackBuilder: () => RollbackBuilder,
|
|
59
|
+
SCOPE_CLAIMS_KEY: () => SCOPE_CLAIMS_KEY,
|
|
60
|
+
SERVER_NODE_ID_PREFIX: () => SERVER_NODE_ID_PREFIX,
|
|
61
|
+
STORED_TEXT_ESCAPE: () => STORED_TEXT_ESCAPE,
|
|
45
62
|
SchemaValidationError: () => SchemaValidationError,
|
|
63
|
+
SecretFieldBuilder: () => SecretFieldBuilder,
|
|
46
64
|
StorageError: () => StorageError,
|
|
47
65
|
SyncError: () => SyncError,
|
|
66
|
+
TIMESTAMP_MAX_MS: () => TIMESTAMP_MAX_MS,
|
|
67
|
+
TIMESTAMP_MIN_MS: () => TIMESTAMP_MIN_MS,
|
|
68
|
+
adaptFoldState: () => adaptFoldState,
|
|
48
69
|
advanceVector: () => advanceVector,
|
|
49
70
|
applyAtomicOp: () => applyAtomicOp,
|
|
50
71
|
applyOperationTransforms: () => applyOperationTransforms,
|
|
72
|
+
assertOperationTransformCoverage: () => assertOperationTransformCoverage,
|
|
73
|
+
assertResolvedFieldValue: () => assertResolvedFieldValue,
|
|
51
74
|
base64ToBytes: () => base64ToBytes,
|
|
52
75
|
buildScopeMap: () => buildScopeMap,
|
|
53
76
|
buildStateMachineConstraints: () => buildStateMachineConstraints,
|
|
54
77
|
bytesToBase64: () => bytesToBase64,
|
|
55
78
|
canAutoRollback: () => canAutoRollback,
|
|
79
|
+
canonicalValue: () => canonicalValue,
|
|
80
|
+
canonicalizeLegacyOperation: () => canonicalizeLegacyOperation,
|
|
81
|
+
canonicalizeOperationBody: () => canonicalizeOperationBody,
|
|
82
|
+
canonicalizeProvenLegacyClear: () => canonicalizeProvenLegacyClear,
|
|
83
|
+
claimScopes: () => claimScopes,
|
|
56
84
|
collectSchemaScopeFields: () => collectSchemaScopeFields,
|
|
57
85
|
collectSchemaScopeValueKeys: () => collectSchemaScopeValueKeys,
|
|
58
86
|
computeDelta: () => computeDelta,
|
|
87
|
+
computeOperationId: () => computeOperationId,
|
|
59
88
|
createBlobRef: () => createBlobRef,
|
|
89
|
+
createFoldState: () => createFoldState,
|
|
60
90
|
createOperation: () => createOperation,
|
|
61
91
|
createReversibleMigration: () => createReversibleMigration,
|
|
92
|
+
createSnapshotState: () => createSnapshotState,
|
|
62
93
|
createVersionVector: () => createVersionVector,
|
|
63
94
|
decodeBytesFromOpData: () => decodeBytesFromOpData,
|
|
95
|
+
decodeStoredJsonValue: () => decodeStoredJsonValue,
|
|
96
|
+
decodeStoredText: () => decodeStoredText,
|
|
64
97
|
decryptSecret: () => decryptSecret,
|
|
65
98
|
defaultApplyFailureReason: () => defaultApplyFailureReason,
|
|
66
99
|
defaultSequenceFormat: () => defaultSequenceFormat,
|
|
67
100
|
defineSchema: () => defineSchema,
|
|
101
|
+
deriveSideEffectOpId: () => deriveSideEffectOpId,
|
|
102
|
+
deserializeFoldState: () => deserializeFoldState,
|
|
68
103
|
deserializeVector: () => deserializeVector,
|
|
69
104
|
dominates: () => dominates,
|
|
70
105
|
encodeBytesForOpData: () => encodeBytesForOpData,
|
|
106
|
+
encodeStoredJsonValue: () => encodeStoredJsonValue,
|
|
107
|
+
encodeStoredText: () => encodeStoredText,
|
|
71
108
|
encryptSecret: () => encryptSecret,
|
|
109
|
+
expandFieldVersionedOperations: () => expandFieldVersionedOperations,
|
|
72
110
|
extractScopeValuesFromClaims: () => extractScopeValuesFromClaims,
|
|
73
111
|
extractTimestamp: () => extractTimestamp,
|
|
112
|
+
foldPlanFingerprint: () => foldPlanFingerprint,
|
|
113
|
+
foldPlanFingerprints: () => foldPlanFingerprints,
|
|
114
|
+
foldRecord: () => foldRecord,
|
|
74
115
|
formatSequenceValue: () => formatSequenceValue,
|
|
75
116
|
generateFullDDL: () => generateFullDDL,
|
|
76
117
|
generateProtoDefinitions: () => generateProtoDefinitions,
|
|
@@ -78,35 +119,61 @@ __export(index_exports, {
|
|
|
78
119
|
generateSQL: () => generateSQL,
|
|
79
120
|
generateUUIDv7: () => generateUUIDv7,
|
|
80
121
|
getCollectionScopeBindings: () => getCollectionScopeBindings,
|
|
122
|
+
getFoldFieldVersionStrings: () => getFoldFieldVersionStrings,
|
|
123
|
+
getFoldFieldVersions: () => getFoldFieldVersions,
|
|
81
124
|
getKoraErrorFix: () => getKoraErrorFix,
|
|
125
|
+
getScopeClaims: () => getScopeClaims,
|
|
82
126
|
getTransitionMap: () => getTransitionMap,
|
|
83
127
|
hasSchemaSyncRules: () => hasSchemaSyncRules,
|
|
84
128
|
hashBlob: () => hashBlob,
|
|
85
129
|
hashSecret: () => hashSecret,
|
|
86
130
|
isApplyFailure: () => isApplyFailure,
|
|
87
131
|
isAtomicOp: () => isAtomicOp,
|
|
132
|
+
isAuthoritativeNodeId: () => isAuthoritativeNodeId,
|
|
88
133
|
isBlobRef: () => isBlobRef,
|
|
89
134
|
isCollectionSyncScoped: () => isCollectionSyncScoped,
|
|
135
|
+
isFoldStateLive: () => isFoldStateLive,
|
|
90
136
|
isKoraBytesValue: () => isKoraBytesValue,
|
|
91
137
|
isLegacyNumericByteObject: () => isLegacyNumericByteObject,
|
|
138
|
+
isReservedNodeId: () => isReservedNodeId,
|
|
139
|
+
isServerNodeId: () => isServerNodeId,
|
|
92
140
|
isValidOperation: () => isValidOperation,
|
|
93
141
|
isValidUUIDv7: () => isValidUUIDv7,
|
|
142
|
+
joinStates: () => joinStates,
|
|
143
|
+
materialize: () => materialize,
|
|
144
|
+
measureOperationBytes: () => measureOperationBytes,
|
|
145
|
+
mergeArraySet: () => mergeArraySet,
|
|
146
|
+
mergeOp: () => mergeOp,
|
|
94
147
|
mergeVectors: () => mergeVectors,
|
|
95
148
|
migrate: () => migrate,
|
|
96
149
|
migrationStepsToSQL: () => migrationStepsToSQL,
|
|
150
|
+
mismatchedFoldFields: () => mismatchedFoldFields,
|
|
151
|
+
missingTransformPaths: () => missingTransformPaths,
|
|
152
|
+
needsStoredTextEncoding: () => needsStoredTextEncoding,
|
|
97
153
|
op: () => op,
|
|
154
|
+
operationSchemaView: () => operationSchemaView,
|
|
155
|
+
operationTransformsFingerprint: () => operationTransformsFingerprint,
|
|
156
|
+
operationValueViolation: () => operationValueViolation,
|
|
157
|
+
protoKeyViolation: () => protoKeyViolation,
|
|
98
158
|
quoteIdent: () => quoteIdent,
|
|
159
|
+
replayFieldVersionsForRecord: () => replayFieldVersionsForRecord,
|
|
99
160
|
replayOperationsForRecord: () => replayOperationsForRecord,
|
|
161
|
+
reservedWireShapeViolation: () => reservedWireShapeViolation,
|
|
100
162
|
resolveAtomicOp: () => resolveAtomicOp,
|
|
101
163
|
revealSecret: () => revealSecret,
|
|
102
164
|
rollbackStepsToSQL: () => rollbackStepsToSQL,
|
|
165
|
+
serializeFoldState: () => serializeFoldState,
|
|
103
166
|
serializeVector: () => serializeVector,
|
|
104
167
|
t: () => t,
|
|
168
|
+
timestampDomainViolation: () => timestampDomainViolation,
|
|
105
169
|
toAtomicOp: () => toAtomicOp,
|
|
170
|
+
toMergeTrace: () => toMergeTrace,
|
|
106
171
|
transformSecretFieldsForWrite: () => transformSecretFieldsForWrite,
|
|
107
172
|
validateRecord: () => validateRecord,
|
|
108
173
|
validateTransition: () => validateTransition,
|
|
174
|
+
valueDepthViolation: () => valueDepthViolation,
|
|
109
175
|
vectorsEqual: () => vectorsEqual,
|
|
176
|
+
verifyOperationId: () => verifyOperationId,
|
|
110
177
|
verifyOperationIntegrity: () => verifyOperationIntegrity,
|
|
111
178
|
verifySecret: () => verifySecret,
|
|
112
179
|
verifySecretValue: () => verifySecretValue
|
|
@@ -174,6 +241,24 @@ var OperationError = class extends KoraError {
|
|
|
174
241
|
this.name = "OperationError";
|
|
175
242
|
}
|
|
176
243
|
};
|
|
244
|
+
var OperationTooLargeError = class extends KoraError {
|
|
245
|
+
constructor(collection, recordId, bytes, maxBytes) {
|
|
246
|
+
super(
|
|
247
|
+
`A write to ${collection}/${recordId} would produce an operation of ${bytes} bytes, more than the ${maxBytes}-byte limit (maxOperationBytes). Nothing was written. Store large content as a blob (t.blob()) or split it across records.`,
|
|
248
|
+
"OPERATION_TOO_LARGE",
|
|
249
|
+
{ collection, recordId, bytes, maxBytes }
|
|
250
|
+
);
|
|
251
|
+
this.collection = collection;
|
|
252
|
+
this.recordId = recordId;
|
|
253
|
+
this.bytes = bytes;
|
|
254
|
+
this.maxBytes = maxBytes;
|
|
255
|
+
this.name = "OperationTooLargeError";
|
|
256
|
+
}
|
|
257
|
+
collection;
|
|
258
|
+
recordId;
|
|
259
|
+
bytes;
|
|
260
|
+
maxBytes;
|
|
261
|
+
};
|
|
177
262
|
var MergeConflictError = class extends KoraError {
|
|
178
263
|
constructor(operationA, operationB, field) {
|
|
179
264
|
super(
|
|
@@ -799,8 +884,305 @@ function defaultApplyFailureReason(result, overrides) {
|
|
|
799
884
|
return { ...base, ...overrides };
|
|
800
885
|
}
|
|
801
886
|
|
|
887
|
+
// src/operations/op-data-binary.ts
|
|
888
|
+
function isKoraBytesValue(value) {
|
|
889
|
+
if (typeof value !== "object" || value === null) {
|
|
890
|
+
return false;
|
|
891
|
+
}
|
|
892
|
+
const record = value;
|
|
893
|
+
return Object.keys(record).length === 1 && typeof record.$koraBytes === "string";
|
|
894
|
+
}
|
|
895
|
+
function isLegacyNumericByteObject(value) {
|
|
896
|
+
if (typeof value !== "object" || value === null || ArrayBuffer.isView(value)) {
|
|
897
|
+
return false;
|
|
898
|
+
}
|
|
899
|
+
const record = value;
|
|
900
|
+
const keys = Object.keys(record);
|
|
901
|
+
if (keys.length === 0) {
|
|
902
|
+
return false;
|
|
903
|
+
}
|
|
904
|
+
for (let i = 0; i < keys.length; i++) {
|
|
905
|
+
const byte = record[String(i)];
|
|
906
|
+
if (typeof byte !== "number" || !Number.isInteger(byte) || byte < 0 || byte > 255) {
|
|
907
|
+
return false;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
return true;
|
|
911
|
+
}
|
|
912
|
+
var BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
913
|
+
var BASE64_REVERSE = new Map(
|
|
914
|
+
Array.from(BASE64_ALPHABET, (char, index) => [char, index])
|
|
915
|
+
);
|
|
916
|
+
function bytesToBase64(bytes) {
|
|
917
|
+
let out = "";
|
|
918
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
919
|
+
const b0 = bytes[i] ?? 0;
|
|
920
|
+
const b1 = bytes[i + 1] ?? 0;
|
|
921
|
+
const b2 = bytes[i + 2] ?? 0;
|
|
922
|
+
const triple = b0 << 16 | b1 << 8 | b2;
|
|
923
|
+
out += BASE64_ALPHABET[triple >> 18 & 63] ?? "";
|
|
924
|
+
out += BASE64_ALPHABET[triple >> 12 & 63] ?? "";
|
|
925
|
+
out += i + 1 < bytes.length ? BASE64_ALPHABET[triple >> 6 & 63] ?? "" : "=";
|
|
926
|
+
out += i + 2 < bytes.length ? BASE64_ALPHABET[triple & 63] ?? "" : "=";
|
|
927
|
+
}
|
|
928
|
+
return out;
|
|
929
|
+
}
|
|
930
|
+
function base64ToBytes(base64) {
|
|
931
|
+
const cleaned = base64.replace(/=+$/, "");
|
|
932
|
+
const out = new Uint8Array(Math.floor(cleaned.length * 6 / 8));
|
|
933
|
+
let buffer = 0;
|
|
934
|
+
let bits = 0;
|
|
935
|
+
let index = 0;
|
|
936
|
+
for (const char of cleaned) {
|
|
937
|
+
const value = BASE64_REVERSE.get(char);
|
|
938
|
+
if (value === void 0) {
|
|
939
|
+
throw new OperationError(`Invalid base64 character "${char}" in tagged binary value.`, {
|
|
940
|
+
char
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
buffer = buffer << 6 | value;
|
|
944
|
+
bits += 6;
|
|
945
|
+
if (bits >= 8) {
|
|
946
|
+
bits -= 8;
|
|
947
|
+
out[index] = buffer >> bits & 255;
|
|
948
|
+
index += 1;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
return out;
|
|
952
|
+
}
|
|
953
|
+
function encodeBytesForOpData(value) {
|
|
954
|
+
if (typeof value === "string") {
|
|
955
|
+
return value;
|
|
956
|
+
}
|
|
957
|
+
const bytes = value instanceof Uint8Array ? value : new Uint8Array(value);
|
|
958
|
+
return { $koraBytes: bytesToBase64(bytes) };
|
|
959
|
+
}
|
|
960
|
+
function decodeBytesFromOpData(value) {
|
|
961
|
+
if (typeof value === "string") {
|
|
962
|
+
return value;
|
|
963
|
+
}
|
|
964
|
+
if (value instanceof Uint8Array) {
|
|
965
|
+
return value;
|
|
966
|
+
}
|
|
967
|
+
if (value instanceof ArrayBuffer) {
|
|
968
|
+
return new Uint8Array(value);
|
|
969
|
+
}
|
|
970
|
+
if (isKoraBytesValue(value)) {
|
|
971
|
+
return base64ToBytes(value.$koraBytes);
|
|
972
|
+
}
|
|
973
|
+
if (isLegacyNumericByteObject(value)) {
|
|
974
|
+
const keys = Object.keys(value);
|
|
975
|
+
const bytes = new Uint8Array(keys.length);
|
|
976
|
+
for (let i = 0; i < keys.length; i++) {
|
|
977
|
+
bytes[i] = value[String(i)] ?? 0;
|
|
978
|
+
}
|
|
979
|
+
return bytes;
|
|
980
|
+
}
|
|
981
|
+
throw new OperationError(
|
|
982
|
+
"Binary op-data value must be a string, Uint8Array, ArrayBuffer, tagged { $koraBytes } object, or legacy numeric-key byte object.",
|
|
983
|
+
{ receivedType: typeof value }
|
|
984
|
+
);
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// src/operations/canonical-body.ts
|
|
988
|
+
var NonCanonicalValueError = class extends KoraError {
|
|
989
|
+
constructor(message, path, receivedType) {
|
|
990
|
+
super(message, "NON_CANONICAL_VALUE", { path, receivedType });
|
|
991
|
+
this.path = path;
|
|
992
|
+
this.receivedType = receivedType;
|
|
993
|
+
this.name = "NonCanonicalValueError";
|
|
994
|
+
}
|
|
995
|
+
path;
|
|
996
|
+
receivedType;
|
|
997
|
+
};
|
|
998
|
+
function canonicalizeOperationBody(input) {
|
|
999
|
+
const isUpdate = input.type === "update";
|
|
1000
|
+
const where = `${input.collection}/${input.recordId}`;
|
|
1001
|
+
const data = canonicalTopLevel(input.data, isUpdate, "data", where);
|
|
1002
|
+
const previousData = canonicalTopLevel(input.previousData, isUpdate, "previousData", where);
|
|
1003
|
+
let atomicOps = input.atomicOps;
|
|
1004
|
+
if (atomicOps !== void 0) {
|
|
1005
|
+
const canonical2 = canonicalTopLevel(
|
|
1006
|
+
atomicOps,
|
|
1007
|
+
false,
|
|
1008
|
+
"atomicOps",
|
|
1009
|
+
where
|
|
1010
|
+
);
|
|
1011
|
+
atomicOps = canonical2 ?? void 0;
|
|
1012
|
+
}
|
|
1013
|
+
if (data === input.data && previousData === input.previousData && atomicOps === input.atomicOps) {
|
|
1014
|
+
return input;
|
|
1015
|
+
}
|
|
1016
|
+
return {
|
|
1017
|
+
...input,
|
|
1018
|
+
data,
|
|
1019
|
+
previousData,
|
|
1020
|
+
...atomicOps !== void 0 ? { atomicOps } : {}
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
function canonicalValue(value, path) {
|
|
1024
|
+
return canonical(value, path, /* @__PURE__ */ new Set());
|
|
1025
|
+
}
|
|
1026
|
+
function canonicalTopLevel(record, undefinedIsNull, label, where) {
|
|
1027
|
+
if (record === null || record === void 0) return null;
|
|
1028
|
+
if (!isPlainObject(record)) {
|
|
1029
|
+
throw new NonCanonicalValueError(
|
|
1030
|
+
`Operation ${label} of ${where} must be a plain object, got ${describe(record)}.`,
|
|
1031
|
+
label,
|
|
1032
|
+
describe(record)
|
|
1033
|
+
);
|
|
1034
|
+
}
|
|
1035
|
+
let changed = false;
|
|
1036
|
+
const out = {};
|
|
1037
|
+
for (const [key, member] of Object.entries(record)) {
|
|
1038
|
+
if (member === void 0) {
|
|
1039
|
+
changed = true;
|
|
1040
|
+
if (undefinedIsNull) out[key] = null;
|
|
1041
|
+
continue;
|
|
1042
|
+
}
|
|
1043
|
+
const next = canonical(member, `${where}.${key}`, /* @__PURE__ */ new Set());
|
|
1044
|
+
if (!Object.is(next, member)) changed = true;
|
|
1045
|
+
out[key] = next;
|
|
1046
|
+
}
|
|
1047
|
+
if (Object.getPrototypeOf(record) !== Object.prototype) changed = true;
|
|
1048
|
+
return changed ? out : record;
|
|
1049
|
+
}
|
|
1050
|
+
function canonical(value, path, ancestors) {
|
|
1051
|
+
switch (typeof value) {
|
|
1052
|
+
case "string":
|
|
1053
|
+
case "boolean":
|
|
1054
|
+
return value;
|
|
1055
|
+
case "number":
|
|
1056
|
+
if (!Number.isFinite(value)) {
|
|
1057
|
+
throw refusal(path, String(value), 'Use a finite number, or null for "no value".');
|
|
1058
|
+
}
|
|
1059
|
+
return Object.is(value, -0) ? 0 : value;
|
|
1060
|
+
case "undefined":
|
|
1061
|
+
return null;
|
|
1062
|
+
case "bigint":
|
|
1063
|
+
throw refusal(path, "bigint", "Convert it to a number or a string.");
|
|
1064
|
+
case "function":
|
|
1065
|
+
throw refusal(path, "function", "Store data only, not functions.");
|
|
1066
|
+
case "symbol":
|
|
1067
|
+
throw refusal(path, "symbol", "Use a string instead.");
|
|
1068
|
+
}
|
|
1069
|
+
if (value === null) return null;
|
|
1070
|
+
const object = value;
|
|
1071
|
+
if (object instanceof Uint8Array) return { $koraBytes: bytesToBase64(object) };
|
|
1072
|
+
if (object instanceof ArrayBuffer) return { $koraBytes: bytesToBase64(new Uint8Array(object)) };
|
|
1073
|
+
if (object instanceof Date) {
|
|
1074
|
+
const time = object.getTime();
|
|
1075
|
+
if (Number.isNaN(time)) {
|
|
1076
|
+
throw refusal(path, "invalid Date", "Pass a valid Date, or null.");
|
|
1077
|
+
}
|
|
1078
|
+
return object.toISOString();
|
|
1079
|
+
}
|
|
1080
|
+
if (ancestors.has(object)) {
|
|
1081
|
+
throw refusal(path, "circular reference", "Remove the cycle: JSON cannot hold it.");
|
|
1082
|
+
}
|
|
1083
|
+
if (Array.isArray(object)) {
|
|
1084
|
+
ancestors.add(object);
|
|
1085
|
+
let changed2 = false;
|
|
1086
|
+
const out2 = [];
|
|
1087
|
+
for (let index = 0; index < object.length; index++) {
|
|
1088
|
+
if (!(index in object)) {
|
|
1089
|
+
ancestors.delete(object);
|
|
1090
|
+
throw refusal(
|
|
1091
|
+
`${path}[${String(index)}]`,
|
|
1092
|
+
"array hole",
|
|
1093
|
+
"Use a dense array (fill the gap with null)."
|
|
1094
|
+
);
|
|
1095
|
+
}
|
|
1096
|
+
const item = object[index];
|
|
1097
|
+
const next = canonical(item, `${path}[${String(index)}]`, ancestors);
|
|
1098
|
+
if (!Object.is(next, item)) changed2 = true;
|
|
1099
|
+
out2.push(next);
|
|
1100
|
+
}
|
|
1101
|
+
ancestors.delete(object);
|
|
1102
|
+
return changed2 ? out2 : object;
|
|
1103
|
+
}
|
|
1104
|
+
if (!isPlainObject(object)) {
|
|
1105
|
+
const type = describe(object);
|
|
1106
|
+
throw refusal(
|
|
1107
|
+
path,
|
|
1108
|
+
type,
|
|
1109
|
+
type === "Map" ? "Convert it with Object.fromEntries(map)." : type === "Set" ? "Convert it with [...set]." : "Convert it to a plain object (JSON data only)."
|
|
1110
|
+
);
|
|
1111
|
+
}
|
|
1112
|
+
if (typeof object.toJSON === "function") {
|
|
1113
|
+
throw refusal(path, "object with toJSON", "Store the value toJSON() returns instead.");
|
|
1114
|
+
}
|
|
1115
|
+
if (Object.prototype.hasOwnProperty.call(object, "__proto__")) {
|
|
1116
|
+
throw refusal(`${path}.__proto__`, '"__proto__" key', "Rename the key.");
|
|
1117
|
+
}
|
|
1118
|
+
ancestors.add(object);
|
|
1119
|
+
let changed = Object.getPrototypeOf(object) !== Object.prototype;
|
|
1120
|
+
const out = {};
|
|
1121
|
+
for (const [key, member] of Object.entries(object)) {
|
|
1122
|
+
if (member === void 0) {
|
|
1123
|
+
changed = true;
|
|
1124
|
+
continue;
|
|
1125
|
+
}
|
|
1126
|
+
const next = canonical(member, `${path}.${key}`, ancestors);
|
|
1127
|
+
if (!Object.is(next, member)) changed = true;
|
|
1128
|
+
out[key] = next;
|
|
1129
|
+
}
|
|
1130
|
+
ancestors.delete(object);
|
|
1131
|
+
return changed ? out : object;
|
|
1132
|
+
}
|
|
1133
|
+
function refusal(path, receivedType, fix) {
|
|
1134
|
+
return new NonCanonicalValueError(
|
|
1135
|
+
`Value at "${path}" (${receivedType}) has no JSON form, so it cannot be stored or synced unchanged. ${fix}`,
|
|
1136
|
+
path,
|
|
1137
|
+
receivedType
|
|
1138
|
+
);
|
|
1139
|
+
}
|
|
1140
|
+
function isPlainObject(value) {
|
|
1141
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
1142
|
+
const proto = Object.getPrototypeOf(value);
|
|
1143
|
+
return proto === Object.prototype || proto === null;
|
|
1144
|
+
}
|
|
1145
|
+
function describe(value) {
|
|
1146
|
+
if (value === null) return "null";
|
|
1147
|
+
if (Array.isArray(value)) return "array";
|
|
1148
|
+
if (typeof value !== "object") return typeof value;
|
|
1149
|
+
const ctor = value.constructor;
|
|
1150
|
+
return typeof ctor?.name === "string" && ctor.name.length > 0 ? ctor.name : "object";
|
|
1151
|
+
}
|
|
1152
|
+
async function canonicalizeProvenLegacyClear(op2) {
|
|
1153
|
+
const restored = canonicalizeLegacyOperation(op2);
|
|
1154
|
+
if (restored === op2) return op2;
|
|
1155
|
+
if (await verifyOperationId(op2)) return op2;
|
|
1156
|
+
if (await verifyOperationId(restored)) return restored;
|
|
1157
|
+
if (op2.hashVersion === void 0 && await verifyOperationId({ ...restored, hashVersion: 2 })) {
|
|
1158
|
+
return restored;
|
|
1159
|
+
}
|
|
1160
|
+
return op2;
|
|
1161
|
+
}
|
|
1162
|
+
function canonicalizeLegacyOperation(op2) {
|
|
1163
|
+
if (op2.type !== "update" || op2.hashVersion === 2 || op2.encrypted !== void 0) return op2;
|
|
1164
|
+
const previous = op2.previousData;
|
|
1165
|
+
if (previous === null || previous === void 0) return op2;
|
|
1166
|
+
const data = op2.data ?? {};
|
|
1167
|
+
const missing = Object.keys(previous).filter((key) => !(key in data));
|
|
1168
|
+
if (missing.length === 0) return op2;
|
|
1169
|
+
const out = { ...data };
|
|
1170
|
+
for (const key of missing) out[key] = null;
|
|
1171
|
+
return { ...op2, data: out };
|
|
1172
|
+
}
|
|
1173
|
+
|
|
802
1174
|
// src/operations/content-hash.ts
|
|
803
|
-
|
|
1175
|
+
var DEFAULT_OPERATION_HASH_VERSION = 2;
|
|
1176
|
+
async function computeOperationId(input, timestampOrVersion) {
|
|
1177
|
+
if (typeof timestampOrVersion === "string") {
|
|
1178
|
+
return sha256Hex(canonicalize(v1HashInput(input, timestampOrVersion)));
|
|
1179
|
+
}
|
|
1180
|
+
const op2 = input;
|
|
1181
|
+
const serializedTs = HybridLogicalClock.serialize(op2.timestamp);
|
|
1182
|
+
if (timestampOrVersion === 1) return sha256Hex(canonicalize(v1HashInput(op2, serializedTs)));
|
|
1183
|
+
return sha256Hex(canonicalize(v2HashInput(op2, serializedTs)));
|
|
1184
|
+
}
|
|
1185
|
+
function v1HashInput(input, timestamp) {
|
|
804
1186
|
const hashInput = {
|
|
805
1187
|
type: input.type,
|
|
806
1188
|
collection: input.collection,
|
|
@@ -812,8 +1194,30 @@ async function computeOperationId(input, timestamp) {
|
|
|
812
1194
|
if (input.atomicOps !== void 0 && Object.keys(input.atomicOps).length > 0) {
|
|
813
1195
|
hashInput.atomicOps = input.atomicOps;
|
|
814
1196
|
}
|
|
815
|
-
|
|
816
|
-
|
|
1197
|
+
return hashInput;
|
|
1198
|
+
}
|
|
1199
|
+
function v2HashInput(op2, timestamp) {
|
|
1200
|
+
const body = canonicalizeOperationBody(op2);
|
|
1201
|
+
const atomicOps = body.atomicOps !== void 0 && Object.keys(body.atomicOps).length > 0 ? body.atomicOps : null;
|
|
1202
|
+
return {
|
|
1203
|
+
hashVersion: 2,
|
|
1204
|
+
type: op2.type,
|
|
1205
|
+
collection: op2.collection,
|
|
1206
|
+
recordId: op2.recordId,
|
|
1207
|
+
data: body.data,
|
|
1208
|
+
previousData: body.previousData,
|
|
1209
|
+
timestamp,
|
|
1210
|
+
nodeId: op2.nodeId,
|
|
1211
|
+
sequenceNumber: op2.sequenceNumber,
|
|
1212
|
+
// A set of parents: their order carries no meaning, so a relay that reorders
|
|
1213
|
+
// them neither breaks verification nor creates a second id for one op.
|
|
1214
|
+
causalDeps: [...op2.causalDeps].sort(),
|
|
1215
|
+
schemaVersion: op2.schemaVersion,
|
|
1216
|
+
atomicOps
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
async function sha256Hex(canonical2) {
|
|
1220
|
+
const encoded = new TextEncoder().encode(canonical2);
|
|
817
1221
|
const hashBuffer = await globalThis.crypto.subtle.digest("SHA-256", encoded);
|
|
818
1222
|
return bufferToHex(hashBuffer);
|
|
819
1223
|
}
|
|
@@ -844,11 +1248,12 @@ function bufferToHex(buffer) {
|
|
|
844
1248
|
}
|
|
845
1249
|
|
|
846
1250
|
// src/operations/operation.ts
|
|
847
|
-
async function createOperation(
|
|
848
|
-
validateOperationParams(
|
|
1251
|
+
async function createOperation(rawInput, clock, options = {}) {
|
|
1252
|
+
validateOperationParams(rawInput);
|
|
1253
|
+
const input = canonicalizeOperationBody(rawInput);
|
|
1254
|
+
const hashVersion = options.hashVersion ?? DEFAULT_OPERATION_HASH_VERSION;
|
|
849
1255
|
const timestamp = clock.now();
|
|
850
|
-
const
|
|
851
|
-
const id = await computeOperationId(input, serializedTs);
|
|
1256
|
+
const id = hashVersion === 1 ? await computeOperationId(input, HybridLogicalClock.serialize(timestamp)) : await computeOperationId({ ...input, timestamp }, hashVersion);
|
|
852
1257
|
const operation = {
|
|
853
1258
|
id,
|
|
854
1259
|
nodeId: input.nodeId,
|
|
@@ -863,7 +1268,8 @@ async function createOperation(input, clock) {
|
|
|
863
1268
|
schemaVersion: input.schemaVersion,
|
|
864
1269
|
...input.atomicOps !== void 0 && Object.keys(input.atomicOps).length > 0 ? { atomicOps: { ...input.atomicOps } } : {},
|
|
865
1270
|
...input.transactionId !== void 0 ? { transactionId: input.transactionId } : {},
|
|
866
|
-
...input.mutationName !== void 0 ? { mutationName: input.mutationName } : {}
|
|
1271
|
+
...input.mutationName !== void 0 ? { mutationName: input.mutationName } : {},
|
|
1272
|
+
...hashVersion !== 1 ? { hashVersion } : {}
|
|
867
1273
|
};
|
|
868
1274
|
return deepFreeze(operation);
|
|
869
1275
|
}
|
|
@@ -931,23 +1337,35 @@ function validateOperationParams(input) {
|
|
|
931
1337
|
});
|
|
932
1338
|
}
|
|
933
1339
|
}
|
|
934
|
-
async function
|
|
935
|
-
const
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
1340
|
+
async function verifyOperationId(op2) {
|
|
1341
|
+
const version = op2.hashVersion ?? 1;
|
|
1342
|
+
if (version !== 1 && version !== 2) return false;
|
|
1343
|
+
let expectedId;
|
|
1344
|
+
try {
|
|
1345
|
+
expectedId = await computeOperationId(
|
|
1346
|
+
{
|
|
1347
|
+
nodeId: op2.nodeId,
|
|
1348
|
+
type: op2.type,
|
|
1349
|
+
collection: op2.collection,
|
|
1350
|
+
recordId: op2.recordId,
|
|
1351
|
+
data: op2.data,
|
|
1352
|
+
previousData: op2.previousData,
|
|
1353
|
+
timestamp: op2.timestamp,
|
|
1354
|
+
sequenceNumber: op2.sequenceNumber,
|
|
1355
|
+
causalDeps: op2.causalDeps,
|
|
1356
|
+
schemaVersion: op2.schemaVersion,
|
|
1357
|
+
...op2.atomicOps !== void 0 ? { atomicOps: op2.atomicOps } : {}
|
|
1358
|
+
},
|
|
1359
|
+
version
|
|
1360
|
+
);
|
|
1361
|
+
} catch {
|
|
1362
|
+
return false;
|
|
1363
|
+
}
|
|
949
1364
|
return op2.id === expectedId;
|
|
950
1365
|
}
|
|
1366
|
+
async function verifyOperationIntegrity(op2) {
|
|
1367
|
+
return verifyOperationId(op2);
|
|
1368
|
+
}
|
|
951
1369
|
function isValidOperation(value) {
|
|
952
1370
|
if (typeof value !== "object" || value === null) return false;
|
|
953
1371
|
const op2 = value;
|
|
@@ -1068,6 +1486,44 @@ function toAtomicOp(sentinel) {
|
|
|
1068
1486
|
}
|
|
1069
1487
|
|
|
1070
1488
|
// src/operations/replay-record.ts
|
|
1489
|
+
function mergeArraySet(local, remote, base) {
|
|
1490
|
+
const serialize = (v) => JSON.stringify(v);
|
|
1491
|
+
const baseSet = new Set(base.map(serialize));
|
|
1492
|
+
const localSet = new Set(local.map(serialize));
|
|
1493
|
+
const remoteSet = new Set(remote.map(serialize));
|
|
1494
|
+
const keep = (s) => localSet.has(s) && remoteSet.has(s) || !baseSet.has(s) && (localSet.has(s) || remoteSet.has(s));
|
|
1495
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1496
|
+
const result = [];
|
|
1497
|
+
const addIfKept = (serialized, value) => {
|
|
1498
|
+
if (!seen.has(serialized) && keep(serialized)) {
|
|
1499
|
+
seen.add(serialized);
|
|
1500
|
+
result.push(value);
|
|
1501
|
+
}
|
|
1502
|
+
};
|
|
1503
|
+
for (const item of base) {
|
|
1504
|
+
addIfKept(serialize(item), item);
|
|
1505
|
+
}
|
|
1506
|
+
const others = /* @__PURE__ */ new Map();
|
|
1507
|
+
for (const item of [...local, ...remote]) {
|
|
1508
|
+
const s = serialize(item);
|
|
1509
|
+
if (!baseSet.has(s) && !others.has(s)) {
|
|
1510
|
+
others.set(s, item);
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
for (const s of [...others.keys()].sort()) {
|
|
1514
|
+
addIfKept(s, others.get(s));
|
|
1515
|
+
}
|
|
1516
|
+
return result;
|
|
1517
|
+
}
|
|
1518
|
+
function resolveArrayWrite(running, value, base) {
|
|
1519
|
+
if (!Array.isArray(running) || !Array.isArray(value) || !Array.isArray(base)) {
|
|
1520
|
+
return value;
|
|
1521
|
+
}
|
|
1522
|
+
if (canonicalize(running) === canonicalize(base)) {
|
|
1523
|
+
return value;
|
|
1524
|
+
}
|
|
1525
|
+
return mergeArraySet(running, value, base);
|
|
1526
|
+
}
|
|
1071
1527
|
function replayOperationsForRecord(ops) {
|
|
1072
1528
|
let record = null;
|
|
1073
1529
|
let deleted = false;
|
|
@@ -1090,6 +1546,8 @@ function replayOperationsForRecord(ops) {
|
|
|
1090
1546
|
const atomicOp = op2.atomicOps?.[field];
|
|
1091
1547
|
if (atomicOp && lastWriterKind[field] === `atomic:${atomicOp.type}`) {
|
|
1092
1548
|
next[field] = applyAtomicOp(next[field], atomicOp);
|
|
1549
|
+
} else if (!atomicOp && op2.previousData && field in op2.previousData) {
|
|
1550
|
+
next[field] = resolveArrayWrite(next[field], value, op2.previousData[field]);
|
|
1093
1551
|
} else {
|
|
1094
1552
|
next[field] = value;
|
|
1095
1553
|
}
|
|
@@ -1106,105 +1564,1655 @@ function replayOperationsForRecord(ops) {
|
|
|
1106
1564
|
}
|
|
1107
1565
|
return deleted ? null : record;
|
|
1108
1566
|
}
|
|
1567
|
+
function replayFieldVersionsForRecord(ops) {
|
|
1568
|
+
let fields = null;
|
|
1569
|
+
let deleted = false;
|
|
1570
|
+
const first = ops[0];
|
|
1571
|
+
const last = ops[ops.length - 1];
|
|
1572
|
+
if (!first || !last) return null;
|
|
1573
|
+
for (const op2 of ops) {
|
|
1574
|
+
switch (op2.type) {
|
|
1575
|
+
case "insert":
|
|
1576
|
+
if (op2.data) {
|
|
1577
|
+
fields = {};
|
|
1578
|
+
for (const field of Object.keys(op2.data)) fields[field] = { ...op2.timestamp };
|
|
1579
|
+
deleted = false;
|
|
1580
|
+
}
|
|
1581
|
+
break;
|
|
1582
|
+
case "update":
|
|
1583
|
+
if (op2.data) {
|
|
1584
|
+
const next = { ...fields ?? {} };
|
|
1585
|
+
for (const field of Object.keys(op2.data)) next[field] = { ...op2.timestamp };
|
|
1586
|
+
fields = next;
|
|
1587
|
+
deleted = false;
|
|
1588
|
+
}
|
|
1589
|
+
break;
|
|
1590
|
+
case "delete":
|
|
1591
|
+
deleted = true;
|
|
1592
|
+
break;
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
if (deleted || fields === null) return null;
|
|
1596
|
+
return { fields, created: { ...first.timestamp }, latest: { ...last.timestamp } };
|
|
1597
|
+
}
|
|
1598
|
+
function expandFieldVersionedOperations(ops) {
|
|
1599
|
+
const expanded = [];
|
|
1600
|
+
for (const op2 of ops) {
|
|
1601
|
+
if (op2.type !== "insert" || !op2.fieldVersions || !op2.data) {
|
|
1602
|
+
expanded.push(op2);
|
|
1603
|
+
continue;
|
|
1604
|
+
}
|
|
1605
|
+
const { fieldVersions, ...base } = op2;
|
|
1606
|
+
expanded.push(base);
|
|
1607
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1608
|
+
let newest = op2.timestamp;
|
|
1609
|
+
for (const v of Object.values(fieldVersions)) {
|
|
1610
|
+
if (HybridLogicalClock.compare(v, newest) > 0) newest = v;
|
|
1611
|
+
}
|
|
1612
|
+
for (const [field, value] of Object.entries(op2.data)) {
|
|
1613
|
+
const version = fieldVersions[field] ?? newest;
|
|
1614
|
+
if (HybridLogicalClock.compare(version, op2.timestamp) <= 0) continue;
|
|
1615
|
+
const key = `${version.wallTime}:${version.logical}:${version.nodeId}`;
|
|
1616
|
+
const group = groups.get(key) ?? { timestamp: version, data: {} };
|
|
1617
|
+
group.data[field] = value;
|
|
1618
|
+
groups.set(key, group);
|
|
1619
|
+
}
|
|
1620
|
+
let index = 0;
|
|
1621
|
+
for (const group of groups.values()) {
|
|
1622
|
+
expanded.push({
|
|
1623
|
+
...base,
|
|
1624
|
+
id: `${op2.id}#field-version-${index++}`,
|
|
1625
|
+
type: "update",
|
|
1626
|
+
data: group.data,
|
|
1627
|
+
previousData: null,
|
|
1628
|
+
timestamp: { ...group.timestamp }
|
|
1629
|
+
});
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
return expanded.sort((a, b) => HybridLogicalClock.compare(a.timestamp, b.timestamp));
|
|
1633
|
+
}
|
|
1109
1634
|
|
|
1110
|
-
// src/
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1635
|
+
// src/migration/apply-operation-transforms.ts
|
|
1636
|
+
var MAX_TRANSFORM_STEPS = 32;
|
|
1637
|
+
function applyOperationTransforms(operation, targetSchemaVersion, transforms) {
|
|
1638
|
+
let current = operation;
|
|
1639
|
+
for (let step = 0; step < MAX_TRANSFORM_STEPS; step++) {
|
|
1640
|
+
if (current.schemaVersion === targetSchemaVersion) {
|
|
1641
|
+
return current;
|
|
1642
|
+
}
|
|
1643
|
+
const transform = transforms.find(
|
|
1644
|
+
(candidate) => candidate.fromVersion === current.schemaVersion
|
|
1645
|
+
);
|
|
1646
|
+
if (!transform) {
|
|
1647
|
+
return null;
|
|
1648
|
+
}
|
|
1649
|
+
const next = transform.transform(current);
|
|
1650
|
+
if (next === null) {
|
|
1651
|
+
return null;
|
|
1652
|
+
}
|
|
1653
|
+
current = next;
|
|
1114
1654
|
}
|
|
1115
|
-
|
|
1116
|
-
return Object.keys(record).length === 1 && typeof record.$koraBytes === "string";
|
|
1655
|
+
return null;
|
|
1117
1656
|
}
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1657
|
+
|
|
1658
|
+
// src/migration/operation-view.ts
|
|
1659
|
+
var OperationTransformError = class extends KoraError {
|
|
1660
|
+
constructor(message, context) {
|
|
1661
|
+
super(message, "INVALID_OPERATION_TRANSFORM", {
|
|
1662
|
+
...context,
|
|
1663
|
+
fix: "An OperationTransform may rewrite data, previousData, atomicOps and schemaVersion only, deterministically, with JSON values."
|
|
1664
|
+
});
|
|
1665
|
+
this.name = "OperationTransformError";
|
|
1121
1666
|
}
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
if (
|
|
1125
|
-
|
|
1667
|
+
};
|
|
1668
|
+
function operationSchemaView(op2, targetSchemaVersion, transforms) {
|
|
1669
|
+
if (op2.schemaVersion === targetSchemaVersion) return op2;
|
|
1670
|
+
if (transforms === void 0 || transforms.length === 0) return op2;
|
|
1671
|
+
if (op2.encrypted !== void 0) return op2;
|
|
1672
|
+
let view;
|
|
1673
|
+
try {
|
|
1674
|
+
view = applyOperationTransforms(op2, targetSchemaVersion, transforms);
|
|
1675
|
+
} catch (error) {
|
|
1676
|
+
throw new OperationTransformError(
|
|
1677
|
+
`The schema transform of operation ${op2.id} (schema v${op2.schemaVersion} -> v${targetSchemaVersion}) threw: ${error instanceof Error ? error.message : String(error)}`,
|
|
1678
|
+
{ operationId: op2.id, fromVersion: op2.schemaVersion, toVersion: targetSchemaVersion }
|
|
1679
|
+
);
|
|
1680
|
+
}
|
|
1681
|
+
if (view === null || view === op2) return view;
|
|
1682
|
+
for (const key of ["id", "nodeId", "type", "collection", "recordId"]) {
|
|
1683
|
+
if (view[key] !== op2[key]) {
|
|
1684
|
+
throw new OperationTransformError(
|
|
1685
|
+
`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.`,
|
|
1686
|
+
{ operationId: op2.id, field: key }
|
|
1687
|
+
);
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
const a = op2.timestamp;
|
|
1691
|
+
const b = view.timestamp;
|
|
1692
|
+
if (a.wallTime !== b.wallTime || a.logical !== b.logical || a.nodeId !== b.nodeId) {
|
|
1693
|
+
throw new OperationTransformError(
|
|
1694
|
+
`The schema transform of operation ${op2.id} changed its timestamp. A transform may rewrite data, previousData, atomicOps and schemaVersion only.`,
|
|
1695
|
+
{ operationId: op2.id, field: "timestamp" }
|
|
1696
|
+
);
|
|
1697
|
+
}
|
|
1698
|
+
try {
|
|
1699
|
+
return canonicalizeOperationBody(view);
|
|
1700
|
+
} catch (error) {
|
|
1701
|
+
throw new OperationTransformError(
|
|
1702
|
+
`The schema transform of operation ${op2.id} produced a value with no JSON form: ${error instanceof Error ? error.message : String(error)}`,
|
|
1703
|
+
{ operationId: op2.id }
|
|
1704
|
+
);
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
function hashText(text) {
|
|
1708
|
+
let hash = 2166136261;
|
|
1709
|
+
for (let i = 0; i < text.length; i++) {
|
|
1710
|
+
hash ^= text.charCodeAt(i);
|
|
1711
|
+
hash = Math.imul(hash, 16777619) >>> 0;
|
|
1712
|
+
}
|
|
1713
|
+
return hash.toString(16).padStart(8, "0");
|
|
1714
|
+
}
|
|
1715
|
+
function operationTransformsFingerprint(targetSchemaVersion, transforms) {
|
|
1716
|
+
if (transforms === void 0 || transforms.length === 0) return "";
|
|
1717
|
+
const parts = transforms.map((t2) => `${t2.fromVersion}>${t2.toVersion}:${hashText(String(t2.transform))}`).sort();
|
|
1718
|
+
return `xf@v${targetSchemaVersion}(${parts.join(",")})`;
|
|
1719
|
+
}
|
|
1720
|
+
var OperationTransformCoverageError = class extends KoraError {
|
|
1721
|
+
constructor(message, versions, targetSchemaVersion, context = {}) {
|
|
1722
|
+
super(message, "OPERATION_TRANSFORM_MISSING", {
|
|
1723
|
+
...context,
|
|
1724
|
+
versions: [...versions],
|
|
1725
|
+
targetSchemaVersion,
|
|
1726
|
+
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).`
|
|
1727
|
+
});
|
|
1728
|
+
this.versions = versions;
|
|
1729
|
+
this.targetSchemaVersion = targetSchemaVersion;
|
|
1730
|
+
this.name = "OperationTransformCoverageError";
|
|
1731
|
+
}
|
|
1732
|
+
versions;
|
|
1733
|
+
targetSchemaVersion;
|
|
1734
|
+
};
|
|
1735
|
+
function missingTransformPaths(storedVersions, targetSchemaVersion, transforms) {
|
|
1736
|
+
if (transforms === void 0 || transforms.length === 0) return [];
|
|
1737
|
+
const missing = [];
|
|
1738
|
+
for (const version of new Set(storedVersions)) {
|
|
1739
|
+
if (version === targetSchemaVersion) continue;
|
|
1740
|
+
let current = version;
|
|
1741
|
+
let reached = false;
|
|
1742
|
+
for (let step = 0; step < 32; step++) {
|
|
1743
|
+
if (current === targetSchemaVersion) {
|
|
1744
|
+
reached = true;
|
|
1745
|
+
break;
|
|
1746
|
+
}
|
|
1747
|
+
const next = transforms.find((candidate) => candidate.fromVersion === current);
|
|
1748
|
+
if (!next) break;
|
|
1749
|
+
current = next.toVersion;
|
|
1750
|
+
}
|
|
1751
|
+
if (!reached) missing.push({ version, missingFrom: current });
|
|
1752
|
+
}
|
|
1753
|
+
return missing.sort((a, b) => a.version - b.version);
|
|
1754
|
+
}
|
|
1755
|
+
function assertOperationTransformCoverage(storedVersions, targetSchemaVersion, transforms, replica) {
|
|
1756
|
+
const missing = missingTransformPaths(storedVersions, targetSchemaVersion, transforms);
|
|
1757
|
+
if (missing.length === 0) return;
|
|
1758
|
+
const versions = missing.map((entry) => entry.version);
|
|
1759
|
+
const steps = [...new Set(missing.map((entry) => entry.missingFrom))];
|
|
1760
|
+
const plural = versions.length > 1;
|
|
1761
|
+
throw new OperationTransformCoverageError(
|
|
1762
|
+
`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.`,
|
|
1763
|
+
versions,
|
|
1764
|
+
targetSchemaVersion,
|
|
1765
|
+
{ replica, missingFrom: steps }
|
|
1766
|
+
);
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
// src/fold/authority.ts
|
|
1770
|
+
var RESERVED_NODE_ID_PREFIX = "kora:";
|
|
1771
|
+
var SERVER_NODE_ID_PREFIX = "kora:server:";
|
|
1772
|
+
function isReservedNodeId(nodeId) {
|
|
1773
|
+
return nodeId.startsWith(RESERVED_NODE_ID_PREFIX);
|
|
1774
|
+
}
|
|
1775
|
+
function isServerNodeId(nodeId) {
|
|
1776
|
+
return nodeId.startsWith(SERVER_NODE_ID_PREFIX) && nodeId.length > SERVER_NODE_ID_PREFIX.length;
|
|
1777
|
+
}
|
|
1778
|
+
function isAuthoritativeNodeId(nodeId, explicit) {
|
|
1779
|
+
return isServerNodeId(nodeId) || (explicit?.has(nodeId) ?? false);
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
// src/fold/errors.ts
|
|
1783
|
+
var FoldStateError = class extends KoraError {
|
|
1784
|
+
constructor(message, context) {
|
|
1785
|
+
super(message, "FOLD_STATE_INVALID", {
|
|
1786
|
+
fix: "Rebuild the record state from its operation log with foldRecord(ops, schema).",
|
|
1787
|
+
...context
|
|
1788
|
+
});
|
|
1789
|
+
this.name = "FoldStateError";
|
|
1790
|
+
}
|
|
1791
|
+
};
|
|
1792
|
+
var FoldConfigurationError = class extends KoraError {
|
|
1793
|
+
constructor(message, context) {
|
|
1794
|
+
super(message, "FOLD_CONFIGURATION", context);
|
|
1795
|
+
this.name = "FoldConfigurationError";
|
|
1796
|
+
}
|
|
1797
|
+
};
|
|
1798
|
+
|
|
1799
|
+
// src/fold/field-kind.ts
|
|
1800
|
+
var LWW_PLAN = {
|
|
1801
|
+
kind: "reg",
|
|
1802
|
+
appendOnly: false,
|
|
1803
|
+
strategy: "lww",
|
|
1804
|
+
tier: 1,
|
|
1805
|
+
secret: false
|
|
1806
|
+
};
|
|
1807
|
+
function planField(collection, field) {
|
|
1808
|
+
const descriptor = collection?.fields[field];
|
|
1809
|
+
const secret = descriptor?.kind === "secret";
|
|
1810
|
+
const resolver = collection?.resolvers?.[field];
|
|
1811
|
+
if (resolver) {
|
|
1812
|
+
return { kind: "res", appendOnly: false, resolver, strategy: "custom", tier: 3, secret };
|
|
1813
|
+
}
|
|
1814
|
+
if (!descriptor) return LWW_PLAN;
|
|
1815
|
+
switch (descriptor.mergeStrategy) {
|
|
1816
|
+
case "counter":
|
|
1817
|
+
return { kind: "ctr", appendOnly: false, strategy: "schema-counter", tier: 1, secret };
|
|
1818
|
+
case "max":
|
|
1819
|
+
return { kind: "max", appendOnly: false, strategy: "schema-max", tier: 1, secret };
|
|
1820
|
+
case "min":
|
|
1821
|
+
return { kind: "min", appendOnly: false, strategy: "schema-min", tier: 1, secret };
|
|
1822
|
+
case "append-only":
|
|
1823
|
+
if (descriptor.kind === "array") {
|
|
1824
|
+
return {
|
|
1825
|
+
kind: "set",
|
|
1826
|
+
appendOnly: true,
|
|
1827
|
+
strategy: "schema-append-only",
|
|
1828
|
+
tier: 1,
|
|
1829
|
+
secret
|
|
1830
|
+
};
|
|
1831
|
+
}
|
|
1832
|
+
return { ...LWW_PLAN, secret };
|
|
1833
|
+
case "lww":
|
|
1834
|
+
return { ...LWW_PLAN, secret };
|
|
1835
|
+
case "server-authoritative":
|
|
1836
|
+
return {
|
|
1837
|
+
...LWW_PLAN,
|
|
1838
|
+
strategy: "schema-server-authoritative",
|
|
1839
|
+
secret,
|
|
1840
|
+
authoritative: true
|
|
1841
|
+
};
|
|
1842
|
+
default:
|
|
1843
|
+
break;
|
|
1844
|
+
}
|
|
1845
|
+
switch (descriptor.kind) {
|
|
1846
|
+
case "array":
|
|
1847
|
+
return { kind: "set", appendOnly: false, strategy: "lww-element-set", tier: 1, secret };
|
|
1848
|
+
case "object":
|
|
1849
|
+
case "json":
|
|
1850
|
+
return { kind: "map", appendOnly: false, strategy: "object-key-lww", tier: 1, secret };
|
|
1851
|
+
case "richtext":
|
|
1852
|
+
return { kind: "rt", appendOnly: false, strategy: "crdt-text", tier: 1, secret };
|
|
1853
|
+
default:
|
|
1854
|
+
return { ...LWW_PLAN, secret };
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
// src/fold/stamp.ts
|
|
1859
|
+
function compareStamps(a, b) {
|
|
1860
|
+
const ca = a.c ?? 0;
|
|
1861
|
+
const cb = b.c ?? 0;
|
|
1862
|
+
if (ca !== cb) return ca - cb;
|
|
1863
|
+
if (a.t !== b.t) return a.t < b.t ? -1 : 1;
|
|
1864
|
+
if (a.o !== b.o) return a.o < b.o ? -1 : 1;
|
|
1865
|
+
return 0;
|
|
1866
|
+
}
|
|
1867
|
+
function maxStamp(a, b) {
|
|
1868
|
+
if (a === null) return b;
|
|
1869
|
+
if (b === null) return a;
|
|
1870
|
+
return compareStamps(a, b) >= 0 ? a : b;
|
|
1871
|
+
}
|
|
1872
|
+
function minStamp(a, b) {
|
|
1873
|
+
if (a === null) return b;
|
|
1874
|
+
if (b === null) return a;
|
|
1875
|
+
return compareStamps(a, b) <= 0 ? a : b;
|
|
1876
|
+
}
|
|
1877
|
+
function isAfter(a, b) {
|
|
1878
|
+
if (a === null) return false;
|
|
1879
|
+
if (b === null) return true;
|
|
1880
|
+
return compareStamps(a, b) > 0;
|
|
1881
|
+
}
|
|
1882
|
+
function stampOf(timestamp, opId) {
|
|
1883
|
+
return { t: HybridLogicalClock.serialize(timestamp), o: opId };
|
|
1884
|
+
}
|
|
1885
|
+
function stampTimestamp(stamp) {
|
|
1886
|
+
return HybridLogicalClock.deserialize(stamp.t);
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
// src/fold/values.ts
|
|
1890
|
+
function isPlainObject2(value) {
|
|
1891
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
1892
|
+
const proto = Object.getPrototypeOf(value);
|
|
1893
|
+
return proto === null || proto === Object.prototype;
|
|
1894
|
+
}
|
|
1895
|
+
function normalizeValue(value) {
|
|
1896
|
+
if (value instanceof Uint8Array) return { $koraBytes: bytesToBase64(value) };
|
|
1897
|
+
if (value instanceof ArrayBuffer) return { $koraBytes: bytesToBase64(new Uint8Array(value)) };
|
|
1898
|
+
if (Array.isArray(value)) return value.map(normalizeValue);
|
|
1899
|
+
if (isPlainObject2(value)) {
|
|
1900
|
+
const out = {};
|
|
1901
|
+
for (const [key, member] of Object.entries(value)) {
|
|
1902
|
+
if (member !== void 0) out[key] = normalizeValue(member);
|
|
1903
|
+
}
|
|
1904
|
+
return out;
|
|
1905
|
+
}
|
|
1906
|
+
return value;
|
|
1907
|
+
}
|
|
1908
|
+
function canonicalKey(value) {
|
|
1909
|
+
return canonicalize(normalizeValue(value));
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
// src/fold/field-states.ts
|
|
1913
|
+
function emptyField(plan) {
|
|
1914
|
+
switch (plan.kind) {
|
|
1915
|
+
case "reg":
|
|
1916
|
+
return { k: "reg", e: [], val: void 0 };
|
|
1917
|
+
case "res":
|
|
1918
|
+
return { k: "res", e: [], val: void 0 };
|
|
1919
|
+
case "set":
|
|
1920
|
+
return { k: "set", ao: plan.appendOnly, sh: null, clr: null, el: {} };
|
|
1921
|
+
case "map":
|
|
1922
|
+
return { k: "map", sh: null, clr: null, keys: {} };
|
|
1923
|
+
case "ctr":
|
|
1924
|
+
return { k: "ctr", base: null, d: [], val: void 0 };
|
|
1925
|
+
case "max":
|
|
1926
|
+
case "min":
|
|
1927
|
+
return { k: plan.kind, best: null, reg: null };
|
|
1928
|
+
case "rt":
|
|
1929
|
+
return { k: "rt", reset: null, u: {} };
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
function entryKind(entry) {
|
|
1933
|
+
return entry.a ? entry.a.type : "set";
|
|
1934
|
+
}
|
|
1935
|
+
function chainFold(entries) {
|
|
1936
|
+
let value;
|
|
1937
|
+
let last = null;
|
|
1938
|
+
for (const entry of entries) {
|
|
1939
|
+
value = entry.a && last === entry.a.type ? applyAtomicOp(value, entry.a) : entry.v;
|
|
1940
|
+
last = entryKind(entry);
|
|
1941
|
+
}
|
|
1942
|
+
return value;
|
|
1943
|
+
}
|
|
1944
|
+
function insertionIndex(entries, stamp) {
|
|
1945
|
+
let lo = 0;
|
|
1946
|
+
let hi = entries.length;
|
|
1947
|
+
while (lo < hi) {
|
|
1948
|
+
const mid = lo + hi >>> 1;
|
|
1949
|
+
const entry = entries[mid];
|
|
1950
|
+
const cmp = compareStamps(entry.s, stamp);
|
|
1951
|
+
if (cmp === 0) return -1;
|
|
1952
|
+
if (cmp < 0) lo = mid + 1;
|
|
1953
|
+
else hi = mid;
|
|
1954
|
+
}
|
|
1955
|
+
return lo;
|
|
1956
|
+
}
|
|
1957
|
+
function pruneChain(entries) {
|
|
1958
|
+
for (let i = entries.length - 1; i > 0; i--) {
|
|
1959
|
+
if (!entries[i].a) return entries.slice(i);
|
|
1960
|
+
}
|
|
1961
|
+
return entries;
|
|
1962
|
+
}
|
|
1963
|
+
function toEntry(write) {
|
|
1964
|
+
return write.a ? { s: write.s, v: write.v, a: write.a } : { s: write.s, v: write.v };
|
|
1965
|
+
}
|
|
1966
|
+
function applyRegister(state, write) {
|
|
1967
|
+
const entries = state.e;
|
|
1968
|
+
const index = insertionIndex(entries, write.s);
|
|
1969
|
+
if (index === -1) return { state, changed: false };
|
|
1970
|
+
const first = entries[0];
|
|
1971
|
+
if (index === 0 && first !== void 0 && !first.a) return { state, changed: false };
|
|
1972
|
+
const entry = toEntry(write);
|
|
1973
|
+
if (index === entries.length) {
|
|
1974
|
+
if (!entry.a) return { state: { k: "reg", e: [entry], val: entry.v }, changed: true };
|
|
1975
|
+
const last = entries[entries.length - 1];
|
|
1976
|
+
const val = last !== void 0 && entryKind(last) === entry.a.type ? applyAtomicOp(state.val, entry.a) : entry.v;
|
|
1977
|
+
return { state: { k: "reg", e: [...entries, entry], val }, changed: true };
|
|
1978
|
+
}
|
|
1979
|
+
const next = pruneChain([...entries.slice(0, index), entry, ...entries.slice(index)]);
|
|
1980
|
+
return { state: { k: "reg", e: next, val: chainFold(next) }, changed: true };
|
|
1981
|
+
}
|
|
1982
|
+
function mergeLogs(a, b) {
|
|
1983
|
+
const out = [];
|
|
1984
|
+
let i = 0;
|
|
1985
|
+
let j = 0;
|
|
1986
|
+
while (i < a.length || j < b.length) {
|
|
1987
|
+
const x = a[i];
|
|
1988
|
+
const y = b[j];
|
|
1989
|
+
if (y === void 0 || x !== void 0 && compareStamps(x.s, y.s) < 0) {
|
|
1990
|
+
out.push(x);
|
|
1991
|
+
i++;
|
|
1992
|
+
} else if (x === void 0 || compareStamps(x.s, y.s) > 0) {
|
|
1993
|
+
out.push(y);
|
|
1994
|
+
j++;
|
|
1995
|
+
} else {
|
|
1996
|
+
out.push(x);
|
|
1997
|
+
i++;
|
|
1998
|
+
j++;
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
return out;
|
|
2002
|
+
}
|
|
2003
|
+
function resolveStep(plan, current, entry) {
|
|
2004
|
+
try {
|
|
2005
|
+
return { value: canonicalValue(plan.resolver?.(current, entry.v, entry.b), "resolver") };
|
|
2006
|
+
} catch (error) {
|
|
2007
|
+
return { value: entry.v, error: error instanceof Error ? error.message : String(error) };
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
function resolverFold(plan, entries) {
|
|
2011
|
+
let value;
|
|
2012
|
+
let error;
|
|
2013
|
+
entries.forEach((entry, index) => {
|
|
2014
|
+
if (index === 0) {
|
|
2015
|
+
value = entry.v;
|
|
2016
|
+
return;
|
|
2017
|
+
}
|
|
2018
|
+
const step = resolveStep(plan, value, entry);
|
|
2019
|
+
value = step.value;
|
|
2020
|
+
if (step.error !== void 0) error = step.error;
|
|
2021
|
+
});
|
|
2022
|
+
return error === void 0 ? { value } : { value, error };
|
|
2023
|
+
}
|
|
2024
|
+
function pruneResolverLog(entries) {
|
|
2025
|
+
for (let i = entries.length - 1; i > 0; i--) {
|
|
2026
|
+
if (entries[i].z === 1) return entries.slice(i);
|
|
2027
|
+
}
|
|
2028
|
+
return entries;
|
|
2029
|
+
}
|
|
2030
|
+
function resolverState(entries, result) {
|
|
2031
|
+
return result.error === void 0 ? { k: "res", e: entries, val: result.value } : { k: "res", e: entries, val: result.value, err: result.error };
|
|
2032
|
+
}
|
|
2033
|
+
function applyResolver(state, write, plan) {
|
|
2034
|
+
const index = insertionIndex(state.e, write.s);
|
|
2035
|
+
if (index === -1) return { state, changed: false };
|
|
2036
|
+
if (index === 0 && state.e[0]?.z === 1) return { state, changed: false };
|
|
2037
|
+
const entry = { s: write.s, v: write.v };
|
|
2038
|
+
if (write.insert) entry.b = null;
|
|
2039
|
+
else if (write.hasPrev) entry.b = write.prev;
|
|
2040
|
+
if (index === state.e.length) {
|
|
2041
|
+
const entries2 = [...state.e, entry];
|
|
2042
|
+
if (state.e.length === 0)
|
|
2043
|
+
return { state: resolverState(entries2, { value: entry.v }), changed: true };
|
|
2044
|
+
const step = resolveStep(plan, state.val, entry);
|
|
2045
|
+
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 };
|
|
2046
|
+
return {
|
|
2047
|
+
state: next,
|
|
2048
|
+
changed: true,
|
|
2049
|
+
...step.error !== void 0 ? { error: step.error } : {}
|
|
2050
|
+
};
|
|
2051
|
+
}
|
|
2052
|
+
const entries = [...state.e.slice(0, index), entry, ...state.e.slice(index)];
|
|
2053
|
+
const result = resolverFold(plan, entries);
|
|
2054
|
+
return { state: resolverState(entries, result), changed: true };
|
|
2055
|
+
}
|
|
2056
|
+
function laterShape(current, next) {
|
|
2057
|
+
return current === null || compareStamps(next.s, current.s) > 0 ? next : null;
|
|
2058
|
+
}
|
|
2059
|
+
function firstAddBefore(a, b) {
|
|
2060
|
+
if (a === null) return true;
|
|
2061
|
+
const cmp = compareStamps(b.s, a.s);
|
|
2062
|
+
return cmp < 0 || cmp === 0 && b.i < a.i;
|
|
2063
|
+
}
|
|
2064
|
+
function elementKey(canonicalValue2, occurrence) {
|
|
2065
|
+
return `${canonicalValue2}#${occurrence}`;
|
|
2066
|
+
}
|
|
2067
|
+
function elementWriter(base) {
|
|
2068
|
+
let el = base;
|
|
2069
|
+
let changed = false;
|
|
2070
|
+
const read = (value, occurrence) => {
|
|
2071
|
+
const canonical2 = canonicalKey(value);
|
|
2072
|
+
const key = elementKey(canonical2, occurrence);
|
|
2073
|
+
const existing = el[key];
|
|
2074
|
+
return [
|
|
2075
|
+
key,
|
|
2076
|
+
existing ? { ...existing } : { v: JSON.parse(canonical2), n: occurrence, a: null, f: null, r: null }
|
|
2077
|
+
];
|
|
2078
|
+
};
|
|
2079
|
+
const store = (key, element) => {
|
|
2080
|
+
if (el === base) el = { ...base };
|
|
2081
|
+
el[key] = element;
|
|
2082
|
+
changed = true;
|
|
2083
|
+
};
|
|
2084
|
+
return {
|
|
2085
|
+
add(value, occurrence, stamp, index) {
|
|
2086
|
+
const [key, element] = read(value, occurrence);
|
|
2087
|
+
let touched = false;
|
|
2088
|
+
if (isAfter(stamp, element.a)) {
|
|
2089
|
+
element.a = stamp;
|
|
2090
|
+
touched = true;
|
|
2091
|
+
}
|
|
2092
|
+
const first = { s: stamp, i: index };
|
|
2093
|
+
if (firstAddBefore(element.f, first)) {
|
|
2094
|
+
element.f = first;
|
|
2095
|
+
touched = true;
|
|
2096
|
+
}
|
|
2097
|
+
if (touched) store(key, element);
|
|
2098
|
+
},
|
|
2099
|
+
remove(value, occurrence, stamp) {
|
|
2100
|
+
const [key, element] = read(value, occurrence);
|
|
2101
|
+
if (isAfter(stamp, element.r)) {
|
|
2102
|
+
element.r = stamp;
|
|
2103
|
+
store(key, element);
|
|
2104
|
+
}
|
|
2105
|
+
},
|
|
2106
|
+
result: () => ({ el, changed })
|
|
2107
|
+
};
|
|
2108
|
+
}
|
|
2109
|
+
function countOccurrences(items) {
|
|
2110
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2111
|
+
const occurrences = items.map((item) => {
|
|
2112
|
+
const canonical2 = canonicalKey(item);
|
|
2113
|
+
const k = counts.get(canonical2) ?? 0;
|
|
2114
|
+
counts.set(canonical2, k + 1);
|
|
2115
|
+
return { item, canonical: canonical2, k };
|
|
2116
|
+
});
|
|
2117
|
+
return { counts, occurrences };
|
|
2118
|
+
}
|
|
2119
|
+
function applyElementSet(state, write) {
|
|
2120
|
+
const writer = elementWriter(state.el);
|
|
2121
|
+
let sh = state.sh;
|
|
2122
|
+
let clr = state.clr;
|
|
2123
|
+
let changed = false;
|
|
2124
|
+
const setShape = (arr, value) => {
|
|
2125
|
+
const next = arr ? { s: write.s, arr } : { s: write.s, arr, v: value };
|
|
2126
|
+
const later = laterShape(sh, next);
|
|
2127
|
+
if (later) {
|
|
2128
|
+
sh = later;
|
|
2129
|
+
changed = true;
|
|
2130
|
+
}
|
|
2131
|
+
};
|
|
2132
|
+
const atomic = write.a;
|
|
2133
|
+
const prev = !write.insert && Array.isArray(write.prev) ? write.prev : null;
|
|
2134
|
+
const atomicElement = atomic !== void 0 && (atomic.type === "append" || atomic.type === "remove");
|
|
2135
|
+
if (atomicElement && (prev === null || !Array.isArray(write.v))) {
|
|
2136
|
+
setShape(true);
|
|
2137
|
+
if (atomic.type === "append") writer.add(atomic.value, 0, write.s, 0);
|
|
2138
|
+
else if (!state.ao) writer.remove(atomic.value, 0, write.s);
|
|
2139
|
+
} else if (!Array.isArray(write.v)) {
|
|
2140
|
+
setShape(false, write.v);
|
|
2141
|
+
const nextClr = maxStamp(clr, write.s);
|
|
2142
|
+
if (nextClr !== clr) {
|
|
2143
|
+
clr = nextClr;
|
|
2144
|
+
changed = true;
|
|
2145
|
+
}
|
|
2146
|
+
} else {
|
|
2147
|
+
setShape(true);
|
|
2148
|
+
const next = countOccurrences(write.v);
|
|
2149
|
+
const before = prev ? countOccurrences(prev) : null;
|
|
2150
|
+
next.occurrences.forEach(({ item, canonical: canonical2, k }, index) => {
|
|
2151
|
+
if (k >= (before?.counts.get(canonical2) ?? 0)) writer.add(item, k, write.s, index);
|
|
2152
|
+
});
|
|
2153
|
+
if (before && !state.ao) {
|
|
2154
|
+
for (const { item, canonical: canonical2, k } of before.occurrences) {
|
|
2155
|
+
if (k >= (next.counts.get(canonical2) ?? 0)) writer.remove(item, k, write.s);
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
const elements = writer.result();
|
|
2160
|
+
if (!changed && !elements.changed) return { state, changed: false };
|
|
2161
|
+
return { state: { k: "set", ao: state.ao, sh, clr, el: elements.el }, changed: true };
|
|
2162
|
+
}
|
|
2163
|
+
function isElementPresent(element, state) {
|
|
2164
|
+
if (element.a === null || element.f === null) return false;
|
|
2165
|
+
if (state.ao) return true;
|
|
2166
|
+
return isAfter(element.a, element.r) && isAfter(element.a, state.clr);
|
|
2167
|
+
}
|
|
2168
|
+
function materializeElementSet(state) {
|
|
2169
|
+
if (state.sh === null) return void 0;
|
|
2170
|
+
if (!state.sh.arr) return state.sh.v;
|
|
2171
|
+
const present = Object.entries(state.el).filter(([, element]) => isElementPresent(element, state));
|
|
2172
|
+
present.sort(([ka, a], [kb, b]) => {
|
|
2173
|
+
const fa = a.f;
|
|
2174
|
+
const fb = b.f;
|
|
2175
|
+
return compareStamps(fa.s, fb.s) || fa.i - fb.i || (ka < kb ? -1 : ka > kb ? 1 : 0);
|
|
2176
|
+
});
|
|
2177
|
+
return present.map(([, element]) => element.v);
|
|
2178
|
+
}
|
|
2179
|
+
function applyKeyMap(state, write) {
|
|
2180
|
+
let sh = state.sh;
|
|
2181
|
+
let clr = state.clr;
|
|
2182
|
+
let keys = state.keys;
|
|
2183
|
+
let changed = false;
|
|
2184
|
+
const setKey = (key, next) => {
|
|
2185
|
+
const current = keys[key];
|
|
2186
|
+
if (current === void 0 || compareStamps(next.s, current.s) > 0) {
|
|
2187
|
+
if (keys === state.keys) keys = { ...state.keys };
|
|
2188
|
+
keys[key] = next;
|
|
2189
|
+
changed = true;
|
|
2190
|
+
}
|
|
2191
|
+
};
|
|
2192
|
+
if (!isPlainObject2(write.v)) {
|
|
2193
|
+
const later = laterShape(sh, { s: write.s, obj: false, v: write.v });
|
|
2194
|
+
if (later) {
|
|
2195
|
+
sh = later;
|
|
2196
|
+
changed = true;
|
|
2197
|
+
}
|
|
2198
|
+
const nextClr = maxStamp(clr, write.s);
|
|
2199
|
+
if (nextClr !== clr) {
|
|
2200
|
+
clr = nextClr;
|
|
2201
|
+
changed = true;
|
|
2202
|
+
}
|
|
2203
|
+
} else {
|
|
2204
|
+
const later = laterShape(sh, { s: write.s, obj: true });
|
|
2205
|
+
if (later) {
|
|
2206
|
+
sh = later;
|
|
2207
|
+
changed = true;
|
|
2208
|
+
}
|
|
2209
|
+
const prev = !write.insert && isPlainObject2(write.prev) ? write.prev : null;
|
|
2210
|
+
for (const [key, value] of Object.entries(write.v)) {
|
|
2211
|
+
if (prev && key in prev && canonicalKey(prev[key]) === canonicalKey(value)) continue;
|
|
2212
|
+
setKey(key, { s: write.s, del: false, v: value });
|
|
2213
|
+
}
|
|
2214
|
+
if (prev) {
|
|
2215
|
+
for (const key of Object.keys(prev)) {
|
|
2216
|
+
if (!(key in write.v)) setKey(key, { s: write.s, del: true });
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
if (!changed) return { state, changed: false };
|
|
2221
|
+
return { state: { k: "map", sh, clr, keys }, changed: true };
|
|
2222
|
+
}
|
|
2223
|
+
function materializeKeyMap(state) {
|
|
2224
|
+
if (state.sh === null) return void 0;
|
|
2225
|
+
if (!state.sh.obj) return state.sh.v;
|
|
2226
|
+
const out = {};
|
|
2227
|
+
for (const key of Object.keys(state.keys).sort()) {
|
|
2228
|
+
const entry = state.keys[key];
|
|
2229
|
+
if (entry.del || !isAfter(entry.s, state.clr)) continue;
|
|
2230
|
+
out[key] = entry.v;
|
|
2231
|
+
}
|
|
2232
|
+
return out;
|
|
2233
|
+
}
|
|
2234
|
+
function counterDelta(write) {
|
|
2235
|
+
if (write.a?.type === "increment" && typeof write.a.value === "number") return write.a.value;
|
|
2236
|
+
if (!write.insert && !write.a && typeof write.v === "number" && typeof write.prev === "number") {
|
|
2237
|
+
return write.v - write.prev;
|
|
2238
|
+
}
|
|
2239
|
+
return null;
|
|
2240
|
+
}
|
|
2241
|
+
function deltaIndex(deltas, stamp) {
|
|
2242
|
+
let lo = 0;
|
|
2243
|
+
let hi = deltas.length;
|
|
2244
|
+
while (lo < hi) {
|
|
2245
|
+
const mid = lo + hi >>> 1;
|
|
2246
|
+
const cmp = compareStamps(deltas[mid].s, stamp);
|
|
2247
|
+
if (cmp === 0) return -1;
|
|
2248
|
+
if (cmp < 0) lo = mid + 1;
|
|
2249
|
+
else hi = mid;
|
|
2250
|
+
}
|
|
2251
|
+
return lo;
|
|
2252
|
+
}
|
|
2253
|
+
function counterValue(base, deltas) {
|
|
2254
|
+
if (deltas.length === 0) return base?.v;
|
|
2255
|
+
let sum = typeof base?.v === "number" ? base.v : 0;
|
|
2256
|
+
for (const delta of deltas) sum += delta.n;
|
|
2257
|
+
return sum;
|
|
2258
|
+
}
|
|
2259
|
+
function counterState(base, d) {
|
|
2260
|
+
return { k: "ctr", base, d, val: counterValue(base, d) };
|
|
2261
|
+
}
|
|
2262
|
+
function pruneDeltas(d, base) {
|
|
2263
|
+
if (base === null) return [...d];
|
|
2264
|
+
return d.filter((delta) => compareStamps(delta.s, base.s) > 0);
|
|
2265
|
+
}
|
|
2266
|
+
function applyCounter(state, write) {
|
|
2267
|
+
const n = counterDelta(write);
|
|
2268
|
+
if (n !== null) {
|
|
2269
|
+
if (state.base !== null && compareStamps(write.s, state.base.s) < 0) {
|
|
2270
|
+
return { state, changed: false };
|
|
2271
|
+
}
|
|
2272
|
+
const index = deltaIndex(state.d, write.s);
|
|
2273
|
+
if (index === -1) return { state, changed: false };
|
|
2274
|
+
const delta = { s: write.s, n };
|
|
2275
|
+
if (index === state.d.length) {
|
|
2276
|
+
const val = state.d.length === 0 ? counterValue(state.base, [delta]) : state.val + n;
|
|
2277
|
+
return { state: { k: "ctr", base: state.base, d: [...state.d, delta], val }, changed: true };
|
|
2278
|
+
}
|
|
2279
|
+
const d = [...state.d.slice(0, index), delta, ...state.d.slice(index)];
|
|
2280
|
+
return { state: counterState(state.base, d), changed: true };
|
|
2281
|
+
}
|
|
2282
|
+
if (state.base !== null && compareStamps(write.s, state.base.s) <= 0) {
|
|
2283
|
+
return { state, changed: false };
|
|
2284
|
+
}
|
|
2285
|
+
const base = { s: write.s, v: write.v };
|
|
2286
|
+
return { state: counterState(base, pruneDeltas(state.d, base)), changed: true };
|
|
2287
|
+
}
|
|
2288
|
+
function mergeDeltas(a, b) {
|
|
2289
|
+
const out = [];
|
|
2290
|
+
let i = 0;
|
|
2291
|
+
let j = 0;
|
|
2292
|
+
while (i < a.length || j < b.length) {
|
|
2293
|
+
const x = a[i];
|
|
2294
|
+
const y = b[j];
|
|
2295
|
+
if (y === void 0 || x !== void 0 && compareStamps(x.s, y.s) < 0) {
|
|
2296
|
+
out.push(x);
|
|
2297
|
+
i++;
|
|
2298
|
+
} else if (x === void 0 || compareStamps(x.s, y.s) > 0) {
|
|
2299
|
+
out.push(y);
|
|
2300
|
+
j++;
|
|
2301
|
+
} else {
|
|
2302
|
+
out.push(x);
|
|
2303
|
+
i++;
|
|
2304
|
+
j++;
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
return out;
|
|
2308
|
+
}
|
|
2309
|
+
function betterExtremum(kind, current, next) {
|
|
2310
|
+
if (current === null) return true;
|
|
2311
|
+
if (next.v !== current.v) return kind === "max" ? next.v > current.v : next.v < current.v;
|
|
2312
|
+
return compareStamps(next.s, current.s) > 0;
|
|
2313
|
+
}
|
|
2314
|
+
function applyExtremum(state, write) {
|
|
2315
|
+
if (typeof write.v === "number" && !Number.isNaN(write.v)) {
|
|
2316
|
+
const next = { s: write.s, v: write.v };
|
|
2317
|
+
if (!betterExtremum(state.k, state.best, next)) return { state, changed: false };
|
|
2318
|
+
return { state: { k: state.k, best: next, reg: state.reg }, changed: true };
|
|
2319
|
+
}
|
|
2320
|
+
if (state.reg !== null && compareStamps(write.s, state.reg.s) <= 0) {
|
|
2321
|
+
return { state, changed: false };
|
|
2322
|
+
}
|
|
2323
|
+
return { state: { k: state.k, best: state.best, reg: { s: write.s, v: write.v } }, changed: true };
|
|
2324
|
+
}
|
|
2325
|
+
function decodeRichtext(value) {
|
|
2326
|
+
if (value === null || value === void 0) return { reset: null };
|
|
2327
|
+
try {
|
|
2328
|
+
const decoded = decodeBytesFromOpData(value);
|
|
2329
|
+
if (typeof decoded === "string") return { reset: decoded };
|
|
2330
|
+
return { update: bytesToBase64(decoded) };
|
|
2331
|
+
} catch {
|
|
2332
|
+
return { reset: value };
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
function applyRichtext(state, write, subsumes) {
|
|
2336
|
+
const decoded = decodeRichtext(write.v);
|
|
2337
|
+
if ("reset" in decoded) {
|
|
2338
|
+
if (state.reset !== null && compareStamps(write.s, state.reset.s) <= 0) {
|
|
2339
|
+
return { state, changed: false };
|
|
2340
|
+
}
|
|
2341
|
+
return {
|
|
2342
|
+
state: { k: "rt", reset: { s: write.s, v: decoded.reset }, u: state.u },
|
|
2343
|
+
changed: true
|
|
2344
|
+
};
|
|
2345
|
+
}
|
|
2346
|
+
const current = state.u[decoded.update];
|
|
2347
|
+
if (current !== void 0 && compareStamps(write.s, current) <= 0) {
|
|
2348
|
+
return { state, changed: false };
|
|
2349
|
+
}
|
|
2350
|
+
if (subsumes === void 0) {
|
|
2351
|
+
return {
|
|
2352
|
+
state: { k: "rt", reset: state.reset, u: { ...state.u, [decoded.update]: write.s } },
|
|
2353
|
+
changed: true
|
|
2354
|
+
};
|
|
2355
|
+
}
|
|
2356
|
+
const incoming = base64ToBytes(decoded.update);
|
|
2357
|
+
const u = {};
|
|
2358
|
+
for (const [update, stamp] of Object.entries(state.u)) {
|
|
2359
|
+
if (update === decoded.update) continue;
|
|
2360
|
+
const bytes = base64ToBytes(update);
|
|
2361
|
+
if (compareStamps(write.s, stamp) <= 0 && subsumes(incoming, bytes)) {
|
|
2362
|
+
return { state, changed: false };
|
|
2363
|
+
}
|
|
2364
|
+
if (compareStamps(stamp, write.s) <= 0 && subsumes(bytes, incoming)) continue;
|
|
2365
|
+
u[update] = stamp;
|
|
2366
|
+
}
|
|
2367
|
+
u[decoded.update] = write.s;
|
|
2368
|
+
return { state: { k: "rt", reset: state.reset, u }, changed: true };
|
|
2369
|
+
}
|
|
2370
|
+
function materializeRichtext(state, merger, field) {
|
|
2371
|
+
const live = Object.entries(state.u).filter(([, stamp]) => isAfter(stamp, state.reset?.s ?? null)).map(([update]) => update).sort();
|
|
2372
|
+
if (live.length === 0) return state.reset ? state.reset.v : void 0;
|
|
2373
|
+
if (live.length === 1) return { $koraBytes: live[0] };
|
|
2374
|
+
if (!merger) {
|
|
2375
|
+
throw new FoldConfigurationError(
|
|
2376
|
+
`Richtext field "${field}" has ${live.length} concurrent Yjs updates and no richtext merger was given.`,
|
|
2377
|
+
{
|
|
2378
|
+
field,
|
|
2379
|
+
updates: live.length,
|
|
2380
|
+
fix: "Pass { richtext: mergeYjsUpdates } (from @korajs/store) in the fold options."
|
|
2381
|
+
}
|
|
2382
|
+
);
|
|
2383
|
+
}
|
|
2384
|
+
return { $koraBytes: bytesToBase64(merger(live.map((update) => base64ToBytes(update)))) };
|
|
2385
|
+
}
|
|
2386
|
+
function applyFieldWrite(state, write, plan, subsumes) {
|
|
2387
|
+
switch (state.k) {
|
|
2388
|
+
case "reg":
|
|
2389
|
+
return applyRegister(state, write);
|
|
2390
|
+
case "res":
|
|
2391
|
+
return applyResolver(state, write, plan);
|
|
2392
|
+
case "set":
|
|
2393
|
+
return applyElementSet(state, write);
|
|
2394
|
+
case "map":
|
|
2395
|
+
return applyKeyMap(state, write);
|
|
2396
|
+
case "ctr":
|
|
2397
|
+
return applyCounter(state, write);
|
|
2398
|
+
case "max":
|
|
2399
|
+
case "min":
|
|
2400
|
+
return applyExtremum(state, write);
|
|
2401
|
+
case "rt":
|
|
2402
|
+
return applyRichtext(state, write, subsumes);
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
function materializeField(state, field, merger) {
|
|
2406
|
+
switch (state.k) {
|
|
2407
|
+
case "reg":
|
|
2408
|
+
case "res":
|
|
2409
|
+
return state.e.length === 0 ? void 0 : state.val;
|
|
2410
|
+
case "set":
|
|
2411
|
+
return materializeElementSet(state);
|
|
2412
|
+
case "map":
|
|
2413
|
+
return materializeKeyMap(state);
|
|
2414
|
+
case "ctr":
|
|
2415
|
+
return state.val;
|
|
2416
|
+
case "max":
|
|
2417
|
+
case "min":
|
|
2418
|
+
return state.best !== null ? state.best.v : state.reg?.v;
|
|
2419
|
+
case "rt":
|
|
2420
|
+
return materializeRichtext(state, merger, field);
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
function fieldStamp(state) {
|
|
2424
|
+
switch (state.k) {
|
|
2425
|
+
case "reg":
|
|
2426
|
+
case "res":
|
|
2427
|
+
return state.e[state.e.length - 1]?.s ?? null;
|
|
2428
|
+
case "set": {
|
|
2429
|
+
let newest = maxStamp(state.sh?.s ?? null, state.clr);
|
|
2430
|
+
for (const element of Object.values(state.el)) {
|
|
2431
|
+
newest = maxStamp(maxStamp(newest, element.a), element.r);
|
|
2432
|
+
}
|
|
2433
|
+
return newest;
|
|
2434
|
+
}
|
|
2435
|
+
case "map": {
|
|
2436
|
+
let newest = maxStamp(state.sh?.s ?? null, state.clr);
|
|
2437
|
+
for (const entry of Object.values(state.keys)) newest = maxStamp(newest, entry.s);
|
|
2438
|
+
return newest;
|
|
2439
|
+
}
|
|
2440
|
+
case "ctr": {
|
|
2441
|
+
return maxStamp(state.base?.s ?? null, state.d[state.d.length - 1]?.s ?? null);
|
|
2442
|
+
}
|
|
2443
|
+
case "max":
|
|
2444
|
+
case "min":
|
|
2445
|
+
return maxStamp(state.best?.s ?? null, state.reg?.s ?? null);
|
|
2446
|
+
case "rt": {
|
|
2447
|
+
let newest = state.reset?.s ?? null;
|
|
2448
|
+
for (const stamp of Object.values(state.u)) newest = maxStamp(newest, stamp);
|
|
2449
|
+
return newest;
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
function laterOf(a, b) {
|
|
2454
|
+
if (a === null) return b;
|
|
2455
|
+
if (b === null) return a;
|
|
2456
|
+
return compareStamps(a.s, b.s) >= 0 ? a : b;
|
|
2457
|
+
}
|
|
2458
|
+
function joinElements(a, b) {
|
|
2459
|
+
const out = { ...a };
|
|
2460
|
+
for (const [key, y] of Object.entries(b)) {
|
|
2461
|
+
const x = out[key];
|
|
2462
|
+
if (x === void 0) {
|
|
2463
|
+
out[key] = y;
|
|
2464
|
+
continue;
|
|
2465
|
+
}
|
|
2466
|
+
out[key] = {
|
|
2467
|
+
v: x.v,
|
|
2468
|
+
n: x.n,
|
|
2469
|
+
a: maxStamp(x.a, y.a),
|
|
2470
|
+
f: y.f !== null && firstAddBefore(x.f, y.f) ? y.f : x.f,
|
|
2471
|
+
r: maxStamp(x.r, y.r)
|
|
2472
|
+
};
|
|
2473
|
+
}
|
|
2474
|
+
return out;
|
|
2475
|
+
}
|
|
2476
|
+
function joinFieldStates(a, b, plan) {
|
|
2477
|
+
if (a.k !== b.k) return a;
|
|
2478
|
+
switch (a.k) {
|
|
2479
|
+
case "reg": {
|
|
2480
|
+
const entries = pruneChain(mergeLogs(a.e, b.e));
|
|
2481
|
+
return { k: "reg", e: entries, val: chainFold(entries) };
|
|
2482
|
+
}
|
|
2483
|
+
case "res": {
|
|
2484
|
+
const entries = pruneResolverLog(mergeLogs(a.e, b.e));
|
|
2485
|
+
return resolverState(entries, resolverFold(plan, entries));
|
|
2486
|
+
}
|
|
2487
|
+
case "set": {
|
|
2488
|
+
const other = b;
|
|
2489
|
+
return {
|
|
2490
|
+
k: "set",
|
|
2491
|
+
ao: a.ao,
|
|
2492
|
+
sh: laterOf(a.sh, other.sh),
|
|
2493
|
+
clr: maxStamp(a.clr, other.clr),
|
|
2494
|
+
el: joinElements(a.el, other.el)
|
|
2495
|
+
};
|
|
2496
|
+
}
|
|
2497
|
+
case "map": {
|
|
2498
|
+
const other = b;
|
|
2499
|
+
const keys = { ...a.keys };
|
|
2500
|
+
for (const [key, entry] of Object.entries(other.keys)) {
|
|
2501
|
+
const current = keys[key];
|
|
2502
|
+
if (current === void 0 || compareStamps(entry.s, current.s) > 0) keys[key] = entry;
|
|
2503
|
+
}
|
|
2504
|
+
return { k: "map", sh: laterOf(a.sh, other.sh), clr: maxStamp(a.clr, other.clr), keys };
|
|
2505
|
+
}
|
|
2506
|
+
case "ctr": {
|
|
2507
|
+
const other = b;
|
|
2508
|
+
const base = laterOf(a.base, other.base);
|
|
2509
|
+
return counterState(base, pruneDeltas(mergeDeltas(a.d, other.d), base));
|
|
2510
|
+
}
|
|
2511
|
+
case "max":
|
|
2512
|
+
case "min": {
|
|
2513
|
+
const other = b;
|
|
2514
|
+
let best = a.best;
|
|
2515
|
+
if (other.best !== null && betterExtremum(a.k, best, other.best)) best = other.best;
|
|
2516
|
+
return { k: a.k, best, reg: laterOf(a.reg, other.reg) };
|
|
2517
|
+
}
|
|
2518
|
+
case "rt": {
|
|
2519
|
+
const other = b;
|
|
2520
|
+
const u = { ...a.u };
|
|
2521
|
+
for (const [update, stamp] of Object.entries(other.u)) {
|
|
2522
|
+
u[update] = maxStamp(u[update] ?? null, stamp);
|
|
2523
|
+
}
|
|
2524
|
+
return { k: "rt", reset: laterOf(a.reset, other.reset), u };
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2529
|
+
// src/fold/types.ts
|
|
2530
|
+
var FOLD_STATE_VERSION = 2;
|
|
2531
|
+
|
|
2532
|
+
// src/fold/serialize.ts
|
|
2533
|
+
var FIELD_KINDS = /* @__PURE__ */ new Set(["reg", "set", "map", "ctr", "max", "min", "res", "rt"]);
|
|
2534
|
+
function serializeFoldState(state) {
|
|
2535
|
+
return canonicalize(state);
|
|
2536
|
+
}
|
|
2537
|
+
function isRecord(value) {
|
|
2538
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2539
|
+
}
|
|
2540
|
+
function deserializeFoldState(json) {
|
|
2541
|
+
let parsed;
|
|
2542
|
+
try {
|
|
2543
|
+
parsed = JSON.parse(json);
|
|
2544
|
+
} catch (error) {
|
|
2545
|
+
throw new FoldStateError("Stored fold state is not valid JSON.", {
|
|
2546
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
2547
|
+
});
|
|
2548
|
+
}
|
|
2549
|
+
if (!isRecord(parsed)) throw new FoldStateError("Stored fold state is not an object.");
|
|
2550
|
+
if (parsed.v !== FOLD_STATE_VERSION) {
|
|
2551
|
+
throw new FoldStateError(
|
|
2552
|
+
`Stored fold state has format version ${String(parsed.v)}; this build reads version ${FOLD_STATE_VERSION}.`,
|
|
2553
|
+
{ found: parsed.v, expected: FOLD_STATE_VERSION }
|
|
2554
|
+
);
|
|
2555
|
+
}
|
|
2556
|
+
if (typeof parsed.c !== "string" || typeof parsed.r !== "string" || !isRecord(parsed.f)) {
|
|
2557
|
+
throw new FoldStateError("Stored fold state is missing its collection, record id or fields.");
|
|
2558
|
+
}
|
|
2559
|
+
for (const [field, state2] of Object.entries(parsed.f)) {
|
|
2560
|
+
if (!isRecord(state2) || typeof state2.k !== "string" || !FIELD_KINDS.has(state2.k)) {
|
|
2561
|
+
throw new FoldStateError(`Stored fold state has an unknown kind for field "${field}".`, {
|
|
2562
|
+
field
|
|
2563
|
+
});
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
const state = parsed;
|
|
2567
|
+
for (const fieldState of Object.values(state.f)) {
|
|
2568
|
+
if ((fieldState.k === "reg" || fieldState.k === "res") && fieldState.e.length === 0) {
|
|
2569
|
+
fieldState.val = void 0;
|
|
2570
|
+
}
|
|
2571
|
+
if (fieldState.k === "ctr" && fieldState.d.length === 0 && fieldState.base === null) {
|
|
2572
|
+
fieldState.val = void 0;
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
return state;
|
|
2576
|
+
}
|
|
2577
|
+
|
|
2578
|
+
// src/fold/fold.ts
|
|
2579
|
+
var FOLD_RECORD_TRACE_FIELD = "*";
|
|
2580
|
+
var SECRET_REDACTED = "[secret]";
|
|
2581
|
+
function createFoldState(collection, recordId) {
|
|
2582
|
+
return {
|
|
2583
|
+
v: FOLD_STATE_VERSION,
|
|
2584
|
+
c: collection,
|
|
2585
|
+
r: recordId,
|
|
2586
|
+
cr: null,
|
|
2587
|
+
w: null,
|
|
2588
|
+
d: null,
|
|
2589
|
+
u: null,
|
|
2590
|
+
f: {}
|
|
2591
|
+
};
|
|
2592
|
+
}
|
|
2593
|
+
function isExcluded(op2, exclude) {
|
|
2594
|
+
if (exclude === void 0) return false;
|
|
2595
|
+
return typeof exclude === "function" ? exclude(op2) : exclude.has(op2.id);
|
|
2596
|
+
}
|
|
2597
|
+
function samePlanKind(state, plan) {
|
|
2598
|
+
if (state.k !== plan.kind) return false;
|
|
2599
|
+
return state.k !== "set" || state.ao === plan.appendOnly;
|
|
2600
|
+
}
|
|
2601
|
+
function fieldPlanFor(collection, field, existing, state) {
|
|
2602
|
+
const plan = planField(collection, field);
|
|
2603
|
+
if (existing !== void 0 && !samePlanKind(existing, plan)) {
|
|
2604
|
+
throw new FoldStateError(
|
|
2605
|
+
`Field "${field}" of ${state.c}/${state.r} is stored as fold kind "${existing.k}" but the schema now folds it as "${plan.kind}".`,
|
|
2606
|
+
{ collection: state.c, recordId: state.r, field, stored: existing.k, expected: plan.kind }
|
|
2607
|
+
);
|
|
2608
|
+
}
|
|
2609
|
+
return plan;
|
|
2610
|
+
}
|
|
2611
|
+
function fieldWriteStamp(op2, field, opStamp) {
|
|
2612
|
+
if (op2.type !== "insert" || op2.fieldVersions === void 0) return opStamp;
|
|
2613
|
+
const version = op2.fieldVersions[field];
|
|
2614
|
+
if (version !== void 0) return stampOf(version, op2.id);
|
|
2615
|
+
let oldest = opStamp;
|
|
2616
|
+
for (const candidate of Object.values(op2.fieldVersions)) {
|
|
2617
|
+
const stamp = stampOf(candidate, op2.id);
|
|
2618
|
+
if (compareStamps(stamp, oldest) < 0) oldest = stamp;
|
|
2619
|
+
}
|
|
2620
|
+
return oldest;
|
|
2621
|
+
}
|
|
2622
|
+
function classify(stamp, op2, field, plan, options) {
|
|
2623
|
+
if (!plan.authoritative) return stamp;
|
|
2624
|
+
const author = op2.type === "insert" ? op2.fieldVersions?.[field]?.nodeId ?? op2.nodeId : op2.nodeId;
|
|
2625
|
+
if (isAuthoritativeNodeId(author, options.authoritativeNodeIds)) {
|
|
2626
|
+
return { ...stamp, c: 1 };
|
|
2627
|
+
}
|
|
2628
|
+
return stamp;
|
|
2629
|
+
}
|
|
2630
|
+
function mergeCarriedState(state, op2, schema, options) {
|
|
2631
|
+
let carried;
|
|
2632
|
+
try {
|
|
2633
|
+
carried = deserializeFoldState(op2.foldState);
|
|
2634
|
+
} catch (error) {
|
|
2635
|
+
if (error instanceof FoldStateError) return null;
|
|
2636
|
+
throw error;
|
|
2637
|
+
}
|
|
2638
|
+
if (carried.c !== state.c || carried.r !== state.r) {
|
|
2639
|
+
throw new FoldStateError(
|
|
2640
|
+
`Operation ${op2.id} carries the fold state of ${carried.c}/${carried.r} but targets ${state.c}/${state.r}.`,
|
|
2641
|
+
{ operationId: op2.id, collection: op2.collection, recordId: op2.recordId }
|
|
2642
|
+
);
|
|
2643
|
+
}
|
|
2644
|
+
let joined;
|
|
2645
|
+
try {
|
|
2646
|
+
joined = joinStates(state, carried, schema);
|
|
2647
|
+
} catch (error) {
|
|
2648
|
+
if (error instanceof FoldStateError) return null;
|
|
2649
|
+
throw error;
|
|
2650
|
+
}
|
|
2651
|
+
const changed = serializeFoldState(joined) !== serializeFoldState(state);
|
|
2652
|
+
const traces = [];
|
|
2653
|
+
if ((options.traces ?? "conflicts") !== "none") {
|
|
2654
|
+
const collection = schema.collections[state.c];
|
|
2655
|
+
for (const [field, incomingState] of Object.entries(carried.f)) {
|
|
2656
|
+
const mine = state.f[field];
|
|
2657
|
+
if (mine === void 0) continue;
|
|
2658
|
+
const prior = safeMaterializeField(mine, field, options);
|
|
2659
|
+
const incoming = safeMaterializeField(incomingState, field, options);
|
|
2660
|
+
if (canonicalKey(prior) === canonicalKey(incoming)) continue;
|
|
2661
|
+
const plan = planField(collection, field);
|
|
2662
|
+
const output = safeMaterializeField(joined.f[field], field, options);
|
|
2663
|
+
const redact = (value) => plan.secret ? SECRET_REDACTED : value;
|
|
2664
|
+
traces.push({
|
|
2665
|
+
field,
|
|
2666
|
+
strategy: `scope-entry-${plan.strategy}`,
|
|
2667
|
+
inputA: redact(prior),
|
|
2668
|
+
inputB: redact(incoming),
|
|
2669
|
+
base: null,
|
|
2670
|
+
output: redact(output),
|
|
2671
|
+
tier: plan.tier,
|
|
2672
|
+
constraintViolated: null,
|
|
2673
|
+
duration: 0,
|
|
2674
|
+
operation: plan.secret ? redactOperation(op2, field) : op2,
|
|
2675
|
+
priorOperationId: fieldStamp(mine)?.o ?? null,
|
|
2676
|
+
conflict: true
|
|
2677
|
+
});
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
return { state: changed ? joined : state, traces, changed };
|
|
2681
|
+
}
|
|
2682
|
+
function buildWrite(op2, field, raw, stamp) {
|
|
2683
|
+
if (raw === void 0) return null;
|
|
2684
|
+
const insert = op2.type === "insert";
|
|
2685
|
+
const v = normalizeValue(raw);
|
|
2686
|
+
const atomic = op2.atomicOps?.[field];
|
|
2687
|
+
const a = atomic ? { type: atomic.type, value: normalizeValue(atomic.value) } : void 0;
|
|
2688
|
+
const prevRaw = insert ? void 0 : op2.previousData?.[field];
|
|
2689
|
+
const hasPrev = !insert && op2.previousData !== null && field in op2.previousData && prevRaw !== void 0;
|
|
2690
|
+
const prev = hasPrev ? normalizeValue(prevRaw) : void 0;
|
|
2691
|
+
if (!insert && a === void 0 && hasPrev && canonicalKey(prev) === canonicalKey(v)) return null;
|
|
2692
|
+
return a ? { s: stamp, insert, v, prev, hasPrev, a } : { s: stamp, insert, v, prev, hasPrev };
|
|
2693
|
+
}
|
|
2694
|
+
function safeMaterializeField(state, field, options) {
|
|
2695
|
+
if (state === void 0) return void 0;
|
|
2696
|
+
try {
|
|
2697
|
+
return materializeField(state, field, options.richtext);
|
|
2698
|
+
} catch (error) {
|
|
2699
|
+
if (error instanceof FoldConfigurationError) return void 0;
|
|
2700
|
+
throw error;
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
function redactOperation(op2, field) {
|
|
2704
|
+
const redact = (data) => data !== null && field in data ? { ...data, [field]: SECRET_REDACTED } : data;
|
|
2705
|
+
return { ...op2, data: redact(op2.data), previousData: redact(op2.previousData) };
|
|
2706
|
+
}
|
|
2707
|
+
function fieldTrace(op2, field, plan, write, prior, priorStamp, output, newest, error) {
|
|
2708
|
+
const lost = newest !== null && compareStamps(newest, write.s) > 0;
|
|
2709
|
+
const baseMismatch = prior !== void 0 && !write.insert && canonicalKey(prior) !== canonicalKey(write.prev);
|
|
2710
|
+
const insertOnto = write.insert && prior !== void 0;
|
|
2711
|
+
const strategy = plan.kind === "reg" && write.a ? `atomic-${write.a.type}` : plan.strategy;
|
|
2712
|
+
const trace = {
|
|
2713
|
+
field,
|
|
2714
|
+
strategy,
|
|
2715
|
+
inputA: plan.secret ? SECRET_REDACTED : prior,
|
|
2716
|
+
inputB: plan.secret ? SECRET_REDACTED : write.v,
|
|
2717
|
+
base: write.hasPrev ? plan.secret ? SECRET_REDACTED : write.prev : null,
|
|
2718
|
+
output: plan.secret ? SECRET_REDACTED : output,
|
|
2719
|
+
tier: plan.tier,
|
|
2720
|
+
constraintViolated: null,
|
|
2721
|
+
// The fold is pure: callers time the whole mergeOp if they need a duration.
|
|
2722
|
+
duration: 0,
|
|
2723
|
+
operation: plan.secret ? redactOperation(op2, field) : op2,
|
|
2724
|
+
priorOperationId: priorStamp?.o ?? null,
|
|
2725
|
+
conflict: lost || baseMismatch || insertOnto
|
|
2726
|
+
};
|
|
2727
|
+
if (error !== void 0) trace.error = error;
|
|
2728
|
+
return trace;
|
|
2729
|
+
}
|
|
2730
|
+
function isAlive(state) {
|
|
2731
|
+
return state.cr !== null && isAfter(state.w, state.d);
|
|
2732
|
+
}
|
|
2733
|
+
function mergeOp(state, input, schema, options = {}) {
|
|
2734
|
+
if (isExcluded(input, options.exclude)) return { state, traces: [], changed: false };
|
|
2735
|
+
const op2 = operationSchemaView(input, schema.version, options.transforms);
|
|
2736
|
+
if (op2 === null) return { state, traces: [], changed: false };
|
|
2737
|
+
if (op2.collection !== state.c || op2.recordId !== state.r) {
|
|
2738
|
+
throw new FoldStateError(
|
|
2739
|
+
`Operation ${op2.id} targets ${op2.collection}/${op2.recordId} but the fold state is for ${state.c}/${state.r}.`,
|
|
2740
|
+
{ operationId: op2.id, collection: op2.collection, recordId: op2.recordId }
|
|
2741
|
+
);
|
|
2742
|
+
}
|
|
2743
|
+
if (op2.foldState !== void 0 && op2.type === "insert") {
|
|
2744
|
+
const carried = mergeCarriedState(state, op2, schema, options);
|
|
2745
|
+
if (carried !== null) return carried;
|
|
2746
|
+
}
|
|
2747
|
+
const mode = options.traces ?? "conflicts";
|
|
2748
|
+
const traces = [];
|
|
2749
|
+
const stamp = stampOf(op2.timestamp, op2.id);
|
|
2750
|
+
const next = { ...state };
|
|
2751
|
+
let changed = false;
|
|
2752
|
+
const setRecordStamp = (key, value) => {
|
|
2753
|
+
if (value !== next[key]) {
|
|
2754
|
+
next[key] = value;
|
|
2755
|
+
changed = true;
|
|
2756
|
+
}
|
|
2757
|
+
};
|
|
2758
|
+
setRecordStamp("u", maxStamp(state.u, stamp));
|
|
2759
|
+
if (op2.type === "delete") {
|
|
2760
|
+
setRecordStamp("d", maxStamp(state.d, stamp));
|
|
2761
|
+
if (mode !== "none" && state.cr !== null && isAfter(state.w, stamp)) {
|
|
2762
|
+
traces.push(recordTrace(op2, true, true));
|
|
2763
|
+
}
|
|
2764
|
+
return { state: changed ? next : state, traces, changed };
|
|
2765
|
+
}
|
|
2766
|
+
if (op2.data === null) {
|
|
2767
|
+
if (op2.encrypted !== void 0) {
|
|
2768
|
+
if (op2.type === "insert") setRecordStamp("cr", minStamp(state.cr, stamp));
|
|
2769
|
+
setRecordStamp("w", maxStamp(state.w, stamp));
|
|
2770
|
+
}
|
|
2771
|
+
return { state: changed ? next : state, traces, changed };
|
|
2772
|
+
}
|
|
2773
|
+
if (op2.type === "insert") setRecordStamp("cr", minStamp(state.cr, stamp));
|
|
2774
|
+
setRecordStamp("w", maxStamp(state.w, stamp));
|
|
2775
|
+
if (mode !== "none" && isAfter(state.d, stamp)) traces.push(recordTrace(op2, false, false));
|
|
2776
|
+
const collection = schema.collections[op2.collection];
|
|
2777
|
+
let fields = state.f;
|
|
2778
|
+
for (const [field, raw] of Object.entries(op2.data)) {
|
|
2779
|
+
if (raw === void 0) continue;
|
|
2780
|
+
const existing = fields[field];
|
|
2781
|
+
const plan = fieldPlanFor(collection, field, existing, state);
|
|
2782
|
+
const write = buildWrite(
|
|
2783
|
+
op2,
|
|
2784
|
+
field,
|
|
2785
|
+
raw,
|
|
2786
|
+
classify(fieldWriteStamp(op2, field, stamp), op2, field, plan, options)
|
|
2787
|
+
);
|
|
2788
|
+
if (write === null) continue;
|
|
2789
|
+
const tracing = mode !== "none";
|
|
2790
|
+
const prior = tracing ? safeMaterializeField(existing, field, options) : void 0;
|
|
2791
|
+
const priorStamp = existing ? fieldStamp(existing) : null;
|
|
2792
|
+
const result = applyFieldWrite(
|
|
2793
|
+
existing ?? emptyField(plan),
|
|
2794
|
+
write,
|
|
2795
|
+
plan,
|
|
2796
|
+
options.richtextSubsumes
|
|
2797
|
+
);
|
|
2798
|
+
if (result.changed) {
|
|
2799
|
+
if (fields === state.f) fields = { ...state.f };
|
|
2800
|
+
fields[field] = result.state;
|
|
2801
|
+
changed = true;
|
|
2802
|
+
}
|
|
2803
|
+
if (tracing) {
|
|
2804
|
+
const trace = fieldTrace(
|
|
2805
|
+
op2,
|
|
2806
|
+
field,
|
|
2807
|
+
plan,
|
|
2808
|
+
write,
|
|
2809
|
+
prior,
|
|
2810
|
+
priorStamp,
|
|
2811
|
+
safeMaterializeField(result.state, field, options),
|
|
2812
|
+
fieldStamp(result.state),
|
|
2813
|
+
result.error
|
|
2814
|
+
);
|
|
2815
|
+
if (mode === "all" || trace.conflict || trace.error !== void 0) traces.push(trace);
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
next.f = fields;
|
|
2819
|
+
return { state: changed ? next : state, traces, changed };
|
|
2820
|
+
}
|
|
2821
|
+
function recordTrace(op2, isDelete, lost) {
|
|
2822
|
+
return {
|
|
2823
|
+
field: FOLD_RECORD_TRACE_FIELD,
|
|
2824
|
+
strategy: "delete-tombstone-lww",
|
|
2825
|
+
inputA: isDelete ? "live" : "deleted",
|
|
2826
|
+
inputB: isDelete ? "deleted" : "live",
|
|
2827
|
+
base: null,
|
|
2828
|
+
// A delete that lost keeps the record live; a write older than the newest
|
|
2829
|
+
// delete stays hidden.
|
|
2830
|
+
output: isDelete ? lost ? "live" : "deleted" : "deleted",
|
|
2831
|
+
tier: 1,
|
|
2832
|
+
constraintViolated: null,
|
|
2833
|
+
duration: 0,
|
|
2834
|
+
operation: op2,
|
|
2835
|
+
priorOperationId: null,
|
|
2836
|
+
conflict: true
|
|
2837
|
+
};
|
|
2838
|
+
}
|
|
2839
|
+
function foldRecord(ops, schema, options = {}) {
|
|
2840
|
+
const kept = ops.filter((op2) => !isExcluded(op2, options.exclude));
|
|
2841
|
+
const first = kept[0];
|
|
2842
|
+
if (first === void 0) return { state: null, traces: [] };
|
|
2843
|
+
const ordered = kept.map((op2) => ({ op: op2, stamp: stampOf(op2.timestamp, op2.id) })).sort((a, b) => compareStamps(a.stamp, b.stamp));
|
|
2844
|
+
let state = createFoldState(first.collection, first.recordId);
|
|
2845
|
+
const traces = [];
|
|
2846
|
+
const mergeOptions = {
|
|
2847
|
+
...options,
|
|
2848
|
+
exclude: void 0,
|
|
2849
|
+
traces: options.traces ?? "none"
|
|
2850
|
+
};
|
|
2851
|
+
for (const { op: op2 } of ordered) {
|
|
2852
|
+
const result = mergeOp(state, op2, schema, mergeOptions);
|
|
2853
|
+
state = result.state;
|
|
2854
|
+
traces.push(...result.traces);
|
|
2855
|
+
}
|
|
2856
|
+
return { state, traces };
|
|
2857
|
+
}
|
|
2858
|
+
function materialize(state, options = {}) {
|
|
2859
|
+
if (!isAlive(state)) return null;
|
|
2860
|
+
const record = {};
|
|
2861
|
+
for (const field of Object.keys(state.f).sort()) {
|
|
2862
|
+
const value = materializeField(state.f[field], field, options.richtext);
|
|
2863
|
+
if (value !== void 0) record[field] = value;
|
|
2864
|
+
}
|
|
2865
|
+
return record;
|
|
2866
|
+
}
|
|
2867
|
+
function isFoldStateLive(state) {
|
|
2868
|
+
return isAlive(state);
|
|
2869
|
+
}
|
|
2870
|
+
function joinStates(a, b, schema) {
|
|
2871
|
+
if (a.c !== b.c || a.r !== b.r) {
|
|
2872
|
+
throw new FoldStateError(`Cannot join fold states of ${a.c}/${a.r} and ${b.c}/${b.r}.`, {
|
|
2873
|
+
left: `${a.c}/${a.r}`,
|
|
2874
|
+
right: `${b.c}/${b.r}`
|
|
2875
|
+
});
|
|
2876
|
+
}
|
|
2877
|
+
const collection = schema.collections[a.c];
|
|
2878
|
+
const fields = { ...a.f };
|
|
2879
|
+
for (const [field, other] of Object.entries(b.f)) {
|
|
2880
|
+
const mine = fields[field];
|
|
2881
|
+
if (mine === void 0) {
|
|
2882
|
+
fieldPlanFor(collection, field, other, b);
|
|
2883
|
+
fields[field] = other;
|
|
2884
|
+
continue;
|
|
2885
|
+
}
|
|
2886
|
+
const plan = fieldPlanFor(collection, field, mine, a);
|
|
2887
|
+
fieldPlanFor(collection, field, other, b);
|
|
2888
|
+
fields[field] = joinFieldStates(mine, other, plan);
|
|
2889
|
+
}
|
|
2890
|
+
return {
|
|
2891
|
+
v: FOLD_STATE_VERSION,
|
|
2892
|
+
c: a.c,
|
|
2893
|
+
r: a.r,
|
|
2894
|
+
cr: minStamp(a.cr, b.cr),
|
|
2895
|
+
w: maxStamp(a.w, b.w),
|
|
2896
|
+
d: maxStamp(a.d, b.d),
|
|
2897
|
+
u: maxStamp(a.u, b.u),
|
|
2898
|
+
f: fields
|
|
2899
|
+
};
|
|
2900
|
+
}
|
|
2901
|
+
function getFoldFieldVersionStrings(state) {
|
|
2902
|
+
const out = {};
|
|
2903
|
+
for (const field of Object.keys(state.f).sort()) {
|
|
2904
|
+
const stamp = fieldStamp(state.f[field]);
|
|
2905
|
+
if (stamp !== null) out[field] = stamp.t;
|
|
2906
|
+
}
|
|
2907
|
+
return out;
|
|
2908
|
+
}
|
|
2909
|
+
function getFoldFieldVersions(state) {
|
|
2910
|
+
if (!isAlive(state) || state.cr === null || state.u === null) return null;
|
|
2911
|
+
const fields = {};
|
|
2912
|
+
for (const [field, fieldState] of Object.entries(state.f)) {
|
|
2913
|
+
const stamp = fieldStamp(fieldState);
|
|
2914
|
+
if (stamp !== null) fields[field] = stampTimestamp(stamp);
|
|
2915
|
+
}
|
|
2916
|
+
return { fields, created: stampTimestamp(state.cr), latest: stampTimestamp(state.u) };
|
|
2917
|
+
}
|
|
2918
|
+
|
|
2919
|
+
// src/fold/plan.ts
|
|
2920
|
+
function hashText2(text) {
|
|
2921
|
+
let hash = 2166136261;
|
|
2922
|
+
for (let i = 0; i < text.length; i++) {
|
|
2923
|
+
hash ^= text.charCodeAt(i);
|
|
2924
|
+
hash = Math.imul(hash, 16777619) >>> 0;
|
|
2925
|
+
}
|
|
2926
|
+
return hash.toString(16).padStart(8, "0");
|
|
2927
|
+
}
|
|
2928
|
+
function collectionFoldPlanFingerprint(name, collection) {
|
|
2929
|
+
const fields = Object.keys(collection.fields).sort().map((field) => {
|
|
2930
|
+
const descriptor = collection.fields[field];
|
|
2931
|
+
const resolver = collection.resolvers?.[field];
|
|
2932
|
+
return `${field}:${descriptor?.kind ?? ""}:${descriptor?.mergeStrategy ?? ""}:${resolver ? hashText2(String(resolver)) : ""}`;
|
|
2933
|
+
});
|
|
2934
|
+
const resolverOnly = Object.keys(collection.resolvers ?? {}).filter((field) => !(field in collection.fields)).sort().map((field) => `${field}:resolver:${hashText2(String(collection.resolvers?.[field]))}`);
|
|
2935
|
+
return `${name}(${[...fields, ...resolverOnly].join(",")})`;
|
|
2936
|
+
}
|
|
2937
|
+
function foldPlanFingerprint(schema, transforms) {
|
|
2938
|
+
const parts = [`fold-v${FOLD_STATE_VERSION}`];
|
|
2939
|
+
for (const name of Object.keys(schema.collections).sort()) {
|
|
2940
|
+
const collection = schema.collections[name];
|
|
2941
|
+
if (!collection) continue;
|
|
2942
|
+
parts.push(collectionFoldPlanFingerprint(name, collection));
|
|
2943
|
+
}
|
|
2944
|
+
const xf = operationTransformsFingerprint(schema.version, transforms);
|
|
2945
|
+
if (xf !== "") parts.push(xf);
|
|
2946
|
+
return parts.join("|");
|
|
2947
|
+
}
|
|
2948
|
+
function foldPlanFingerprints(schema, transforms) {
|
|
2949
|
+
const out = {};
|
|
2950
|
+
const xf = operationTransformsFingerprint(schema.version, transforms);
|
|
2951
|
+
for (const name of Object.keys(schema.collections).sort()) {
|
|
2952
|
+
const collection = schema.collections[name];
|
|
2953
|
+
if (!collection) continue;
|
|
2954
|
+
out[name] = `fold-v${FOLD_STATE_VERSION}|${collectionFoldPlanFingerprint(name, collection)}${xf !== "" ? `|${xf}` : ""}`;
|
|
2955
|
+
}
|
|
2956
|
+
return out;
|
|
2957
|
+
}
|
|
2958
|
+
function fieldStateMatchesPlan(collection, field, state) {
|
|
2959
|
+
const plan = planField(collection, field);
|
|
2960
|
+
if (state.k !== plan.kind) return false;
|
|
2961
|
+
return state.k !== "set" || state.ao === plan.appendOnly;
|
|
2962
|
+
}
|
|
2963
|
+
function mismatchedFoldFields(state, schema) {
|
|
2964
|
+
const collection = schema.collections[state.c];
|
|
2965
|
+
return Object.keys(state.f).filter((field) => !fieldStateMatchesPlan(collection, field, state.f[field])).sort();
|
|
2966
|
+
}
|
|
2967
|
+
|
|
2968
|
+
// src/fold/snapshot.ts
|
|
2969
|
+
var SNAPSHOT_CLEAR_ID = "\uFFFFkora-snapshot:0";
|
|
2970
|
+
var SNAPSHOT_WRITE_ID = "\uFFFFkora-snapshot:1";
|
|
2971
|
+
function snapshotField(plan, raw, clear, write) {
|
|
2972
|
+
const v = normalizeValue(raw);
|
|
2973
|
+
switch (plan.kind) {
|
|
2974
|
+
case "reg":
|
|
2975
|
+
return { k: "reg", e: [{ s: write, v }], val: v };
|
|
2976
|
+
case "res":
|
|
2977
|
+
return { k: "res", e: [{ s: write, v, z: 1 }], val: v };
|
|
2978
|
+
case "ctr":
|
|
2979
|
+
return { k: "ctr", base: { s: write, v }, d: [], val: v };
|
|
2980
|
+
case "max":
|
|
2981
|
+
case "min":
|
|
2982
|
+
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 } };
|
|
2983
|
+
case "set": {
|
|
2984
|
+
if (!Array.isArray(v)) {
|
|
2985
|
+
return {
|
|
2986
|
+
k: "set",
|
|
2987
|
+
ao: plan.appendOnly,
|
|
2988
|
+
sh: { s: write, arr: false, v },
|
|
2989
|
+
clr: write,
|
|
2990
|
+
el: {}
|
|
2991
|
+
};
|
|
2992
|
+
}
|
|
2993
|
+
const el = {};
|
|
2994
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2995
|
+
v.forEach((item, index) => {
|
|
2996
|
+
const canonical2 = canonicalKey(item);
|
|
2997
|
+
const n = counts.get(canonical2) ?? 0;
|
|
2998
|
+
counts.set(canonical2, n + 1);
|
|
2999
|
+
el[elementKey(canonical2, n)] = {
|
|
3000
|
+
v: JSON.parse(canonical2),
|
|
3001
|
+
n,
|
|
3002
|
+
a: write,
|
|
3003
|
+
f: { s: write, i: index },
|
|
3004
|
+
r: null
|
|
3005
|
+
};
|
|
3006
|
+
});
|
|
3007
|
+
return { k: "set", ao: plan.appendOnly, sh: { s: write, arr: true }, clr: clear, el };
|
|
3008
|
+
}
|
|
3009
|
+
case "map": {
|
|
3010
|
+
if (!isPlainObject2(v)) {
|
|
3011
|
+
return { k: "map", sh: { s: write, obj: false, v }, clr: write, keys: {} };
|
|
3012
|
+
}
|
|
3013
|
+
const keys = {};
|
|
3014
|
+
for (const [key, member] of Object.entries(v)) keys[key] = { s: write, del: false, v: member };
|
|
3015
|
+
return { k: "map", sh: { s: write, obj: true }, clr: clear, keys };
|
|
3016
|
+
}
|
|
3017
|
+
case "rt": {
|
|
3018
|
+
if (v === null || typeof v === "string" || !isPlainObject2(v) || !("$koraBytes" in v)) {
|
|
3019
|
+
return { k: "rt", reset: { s: write, v }, u: {} };
|
|
3020
|
+
}
|
|
3021
|
+
const update = String(v.$koraBytes);
|
|
3022
|
+
return { k: "rt", reset: { s: clear, v: null }, u: { [update]: write } };
|
|
3023
|
+
}
|
|
3024
|
+
}
|
|
3025
|
+
}
|
|
3026
|
+
function createSnapshotState(input, schema, options = {}) {
|
|
3027
|
+
const state = createFoldState(input.collection, input.recordId);
|
|
3028
|
+
const collection = schema.collections[input.collection];
|
|
3029
|
+
const seed = options.seed && options.seed.c === input.collection && options.seed.r === input.recordId ? options.seed : null;
|
|
3030
|
+
const fields = {};
|
|
3031
|
+
for (const [field, raw] of Object.entries(input.values)) {
|
|
3032
|
+
if (raw === void 0) continue;
|
|
3033
|
+
const seeded = seed?.f[field];
|
|
3034
|
+
if (seeded !== void 0 && fieldStateMatchesPlan(collection, field, seeded)) {
|
|
3035
|
+
fields[field] = seeded;
|
|
3036
|
+
continue;
|
|
3037
|
+
}
|
|
3038
|
+
const version = input.fieldVersions[field] ?? input.latest;
|
|
3039
|
+
const plan = planField(collection, field);
|
|
3040
|
+
const authoritative = plan.authoritative === true && isAuthoritativeNodeId(version.nodeId, options.authoritativeNodeIds);
|
|
3041
|
+
fields[field] = snapshotField(
|
|
3042
|
+
plan,
|
|
3043
|
+
raw,
|
|
3044
|
+
classed(stampOf(version, SNAPSHOT_CLEAR_ID), authoritative),
|
|
3045
|
+
classed(stampOf(version, SNAPSHOT_WRITE_ID), authoritative)
|
|
3046
|
+
);
|
|
1126
3047
|
}
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
3048
|
+
if (seed !== null && seed.cr !== null && seed.u !== null) {
|
|
3049
|
+
return { ...state, cr: seed.cr, w: seed.w, d: seed.d, u: seed.u, f: fields };
|
|
3050
|
+
}
|
|
3051
|
+
const latestClear = stampOf(input.latest, SNAPSHOT_CLEAR_ID);
|
|
3052
|
+
const latestWrite = stampOf(input.latest, SNAPSHOT_WRITE_ID);
|
|
3053
|
+
return {
|
|
3054
|
+
...state,
|
|
3055
|
+
cr: stampOf(input.created, SNAPSHOT_WRITE_ID),
|
|
3056
|
+
w: input.deleted ? latestClear : latestWrite,
|
|
3057
|
+
d: input.deleted ? latestWrite : null,
|
|
3058
|
+
u: latestWrite,
|
|
3059
|
+
f: fields
|
|
3060
|
+
};
|
|
3061
|
+
}
|
|
3062
|
+
function classed(stamp, authoritative) {
|
|
3063
|
+
return authoritative ? { ...stamp, c: 1 } : stamp;
|
|
3064
|
+
}
|
|
3065
|
+
function adaptFoldState(state, schema, options = {}) {
|
|
3066
|
+
const mismatched = mismatchedFoldFields(state, schema);
|
|
3067
|
+
if (mismatched.length === 0) return { state, adapted: [] };
|
|
3068
|
+
const collection = schema.collections[state.c];
|
|
3069
|
+
const fields = { ...state.f };
|
|
3070
|
+
for (const field of mismatched) {
|
|
3071
|
+
const old = fields[field];
|
|
3072
|
+
delete fields[field];
|
|
3073
|
+
const stamp = fieldStamp(old);
|
|
3074
|
+
if (stamp === null) continue;
|
|
3075
|
+
let value;
|
|
3076
|
+
try {
|
|
3077
|
+
value = materializeField(old, field, options.richtext);
|
|
3078
|
+
} catch (error) {
|
|
3079
|
+
if (error instanceof FoldConfigurationError) continue;
|
|
3080
|
+
throw error;
|
|
1131
3081
|
}
|
|
3082
|
+
if (value === void 0) continue;
|
|
3083
|
+
const plan = planField(collection, field);
|
|
3084
|
+
const authoritative = stamp.c === 1 && plan.authoritative === true;
|
|
3085
|
+
fields[field] = snapshotField(
|
|
3086
|
+
plan,
|
|
3087
|
+
value,
|
|
3088
|
+
classed({ t: stamp.t, o: SNAPSHOT_CLEAR_ID }, authoritative),
|
|
3089
|
+
classed({ t: stamp.t, o: SNAPSHOT_WRITE_ID }, authoritative)
|
|
3090
|
+
);
|
|
1132
3091
|
}
|
|
1133
|
-
return
|
|
3092
|
+
return { state: { ...state, f: fields }, adapted: mismatched };
|
|
1134
3093
|
}
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
3094
|
+
|
|
3095
|
+
// src/fold/trace.ts
|
|
3096
|
+
function toMergeTrace(trace, operationA) {
|
|
3097
|
+
return {
|
|
3098
|
+
operationA: operationA ?? trace.operation,
|
|
3099
|
+
operationB: trace.operation,
|
|
3100
|
+
field: trace.field,
|
|
3101
|
+
strategy: trace.strategy,
|
|
3102
|
+
inputA: trace.inputA,
|
|
3103
|
+
inputB: trace.inputB,
|
|
3104
|
+
base: trace.base,
|
|
3105
|
+
output: trace.output,
|
|
3106
|
+
tier: trace.tier,
|
|
3107
|
+
constraintViolated: trace.constraintViolated,
|
|
3108
|
+
duration: trace.duration
|
|
3109
|
+
};
|
|
3110
|
+
}
|
|
3111
|
+
|
|
3112
|
+
// src/text/stored-text.ts
|
|
3113
|
+
var STORED_TEXT_ESCAPE = "\uFFFF";
|
|
3114
|
+
var NEEDS_ENCODING = /[\u0000\ud800-\udfff]/;
|
|
3115
|
+
function needsStoredTextEncoding(s) {
|
|
3116
|
+
return NEEDS_ENCODING.test(s);
|
|
3117
|
+
}
|
|
3118
|
+
function encodeStoredText(s) {
|
|
3119
|
+
if (!NEEDS_ENCODING.test(s)) return s;
|
|
1140
3120
|
let out = "";
|
|
1141
|
-
for (let i = 0; i <
|
|
1142
|
-
const
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
3121
|
+
for (let i = 0; i < s.length; i++) {
|
|
3122
|
+
const code = s.charCodeAt(i);
|
|
3123
|
+
if (code === 0) {
|
|
3124
|
+
out += `${STORED_TEXT_ESCAPE}0`;
|
|
3125
|
+
} else if (code === 65535) {
|
|
3126
|
+
out += `${STORED_TEXT_ESCAPE}F`;
|
|
3127
|
+
} else if (code >= 55296 && code <= 56319) {
|
|
3128
|
+
const next = s.charCodeAt(i + 1);
|
|
3129
|
+
if (next >= 56320 && next <= 57343) {
|
|
3130
|
+
out += s.slice(i, i + 2);
|
|
3131
|
+
i++;
|
|
3132
|
+
} else {
|
|
3133
|
+
out += `${STORED_TEXT_ESCAPE}s${code.toString(16)}`;
|
|
3134
|
+
}
|
|
3135
|
+
} else if (code >= 56320 && code <= 57343) {
|
|
3136
|
+
out += `${STORED_TEXT_ESCAPE}s${code.toString(16)}`;
|
|
3137
|
+
} else {
|
|
3138
|
+
out += s.charAt(i);
|
|
3139
|
+
}
|
|
1150
3140
|
}
|
|
1151
3141
|
return out;
|
|
1152
3142
|
}
|
|
1153
|
-
function
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
let
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
if (value === void 0) {
|
|
1162
|
-
throw new OperationError(`Invalid base64 character "${char}" in tagged binary value.`, {
|
|
1163
|
-
char
|
|
1164
|
-
});
|
|
3143
|
+
function decodeStoredText(s) {
|
|
3144
|
+
if (!s.includes(STORED_TEXT_ESCAPE)) return s;
|
|
3145
|
+
let out = "";
|
|
3146
|
+
for (let i = 0; i < s.length; i++) {
|
|
3147
|
+
const char = s[i];
|
|
3148
|
+
if (char !== STORED_TEXT_ESCAPE) {
|
|
3149
|
+
out += char;
|
|
3150
|
+
continue;
|
|
1165
3151
|
}
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
3152
|
+
const tag = s[i + 1];
|
|
3153
|
+
if (tag === "0") {
|
|
3154
|
+
out += "\0";
|
|
3155
|
+
i += 1;
|
|
3156
|
+
} else if (tag === "F") {
|
|
3157
|
+
out += STORED_TEXT_ESCAPE;
|
|
3158
|
+
i += 1;
|
|
3159
|
+
} else if (tag === "s") {
|
|
3160
|
+
const hex = s.slice(i + 2, i + 6);
|
|
3161
|
+
const code = Number.parseInt(hex, 16);
|
|
3162
|
+
if (hex.length === 4 && Number.isInteger(code) && code >= 55296 && code <= 57343) {
|
|
3163
|
+
out += String.fromCharCode(code);
|
|
3164
|
+
i += 5;
|
|
3165
|
+
} else {
|
|
3166
|
+
out += char;
|
|
3167
|
+
}
|
|
3168
|
+
} else {
|
|
3169
|
+
out += char;
|
|
1172
3170
|
}
|
|
1173
3171
|
}
|
|
1174
3172
|
return out;
|
|
1175
3173
|
}
|
|
1176
|
-
function
|
|
1177
|
-
|
|
1178
|
-
return value;
|
|
1179
|
-
}
|
|
1180
|
-
const bytes = value instanceof Uint8Array ? value : new Uint8Array(value);
|
|
1181
|
-
return { $koraBytes: bytesToBase64(bytes) };
|
|
3174
|
+
function encodeStoredJsonValue(value) {
|
|
3175
|
+
return mapStrings(value, encodeStoredText);
|
|
1182
3176
|
}
|
|
1183
|
-
function
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
if (value
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
3177
|
+
function decodeStoredJsonValue(value) {
|
|
3178
|
+
return mapStrings(value, decodeStoredText);
|
|
3179
|
+
}
|
|
3180
|
+
function mapStrings(value, map) {
|
|
3181
|
+
if (typeof value === "string") return map(value);
|
|
3182
|
+
if (Array.isArray(value)) return value.map((member) => mapStrings(member, map));
|
|
3183
|
+
if (value !== null && typeof value === "object" && !ArrayBuffer.isView(value)) {
|
|
3184
|
+
const out = {};
|
|
3185
|
+
for (const [key, member] of Object.entries(value)) out[map(key)] = mapStrings(member, map);
|
|
3186
|
+
return out;
|
|
3187
|
+
}
|
|
3188
|
+
return value;
|
|
3189
|
+
}
|
|
3190
|
+
|
|
3191
|
+
// src/fold/side-effect-id.ts
|
|
3192
|
+
var SIDE_EFFECT_DOMAIN = "kora/side-effect/v1";
|
|
3193
|
+
async function deriveSideEffectOpId(parentOpId, ruleId, targetRecordId) {
|
|
3194
|
+
for (const [name, value] of [
|
|
3195
|
+
["parentOpId", parentOpId],
|
|
3196
|
+
["ruleId", ruleId],
|
|
3197
|
+
["targetRecordId", targetRecordId]
|
|
3198
|
+
]) {
|
|
3199
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
3200
|
+
throw new OperationError(`deriveSideEffectOpId: ${name} must be a non-empty string`, {
|
|
3201
|
+
[name]: value
|
|
3202
|
+
});
|
|
1201
3203
|
}
|
|
1202
|
-
return bytes;
|
|
1203
3204
|
}
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
3205
|
+
const canonical2 = canonicalize({
|
|
3206
|
+
domain: SIDE_EFFECT_DOMAIN,
|
|
3207
|
+
parent: parentOpId,
|
|
3208
|
+
rule: ruleId,
|
|
3209
|
+
target: targetRecordId
|
|
3210
|
+
});
|
|
3211
|
+
const digest = await globalThis.crypto.subtle.digest(
|
|
3212
|
+
"SHA-256",
|
|
3213
|
+
new TextEncoder().encode(canonical2)
|
|
1207
3214
|
);
|
|
3215
|
+
return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
1208
3216
|
}
|
|
1209
3217
|
|
|
1210
3218
|
// src/scopes/sync-scope-bindings.ts
|
|
@@ -1489,6 +3497,21 @@ function buildCollection(name, input) {
|
|
|
1489
3497
|
validateStateMachineDefinition(name, input.stateMachine, fields);
|
|
1490
3498
|
stateMachine = { ...input.stateMachine };
|
|
1491
3499
|
}
|
|
3500
|
+
if (!stateMachine) {
|
|
3501
|
+
const transitionFields = Object.entries(fields).filter(
|
|
3502
|
+
([, descriptor]) => descriptor.kind === "enum" && descriptor.transitions
|
|
3503
|
+
);
|
|
3504
|
+
const only = transitionFields.length === 1 ? transitionFields[0] : void 0;
|
|
3505
|
+
if (only?.[1].transitions) {
|
|
3506
|
+
const derived = {
|
|
3507
|
+
field: only[0],
|
|
3508
|
+
transitions: { ...only[1].transitions },
|
|
3509
|
+
onInvalidTransition: "reject"
|
|
3510
|
+
};
|
|
3511
|
+
validateStateMachineDefinition(name, derived, fields);
|
|
3512
|
+
stateMachine = derived;
|
|
3513
|
+
}
|
|
3514
|
+
}
|
|
1492
3515
|
return { fields, indexes, constraints, resolvers, scope, stateMachine };
|
|
1493
3516
|
}
|
|
1494
3517
|
function buildSyncRules(syncInput, collections) {
|
|
@@ -1665,13 +3688,13 @@ ALTER TABLE ${quoteIdent(collectionName)} ADD COLUMN _field_versions TEXT NOT NU
|
|
|
1665
3688
|
);
|
|
1666
3689
|
for (const indexField of collection.indexes) {
|
|
1667
3690
|
statements.push(
|
|
1668
|
-
`CREATE INDEX IF NOT EXISTS
|
|
3691
|
+
`CREATE INDEX IF NOT EXISTS ${quoteIdent(collectionIndexName(collectionName, indexField))} ON ${quoteIdent(collectionName)} (${quoteIdent(indexField)})`
|
|
1669
3692
|
);
|
|
1670
3693
|
}
|
|
1671
3694
|
for (const fkField of fkFields) {
|
|
1672
3695
|
if (!indexedFields.has(fkField)) {
|
|
1673
3696
|
statements.push(
|
|
1674
|
-
`CREATE INDEX IF NOT EXISTS
|
|
3697
|
+
`CREATE INDEX IF NOT EXISTS ${quoteIdent(collectionIndexName(collectionName, fkField))} ON ${quoteIdent(collectionName)} (${quoteIdent(fkField)})`
|
|
1675
3698
|
);
|
|
1676
3699
|
}
|
|
1677
3700
|
}
|
|
@@ -1694,11 +3717,16 @@ ALTER TABLE ${quoteIdent(collectionName)} ADD COLUMN _field_versions TEXT NOT NU
|
|
|
1694
3717
|
);
|
|
1695
3718
|
return statements;
|
|
1696
3719
|
}
|
|
3720
|
+
function collectionIndexName(collection, field) {
|
|
3721
|
+
return `idx_${collection.length}_${collection}_${field}`;
|
|
3722
|
+
}
|
|
3723
|
+
var SCHEMA_CEILING_DIRECTIVE = "--kora:schema-ceiling";
|
|
1697
3724
|
function generateFullDDL(schema) {
|
|
1698
3725
|
const statements = [];
|
|
1699
3726
|
statements.push(
|
|
1700
3727
|
"CREATE TABLE IF NOT EXISTS _kora_meta (\n key TEXT PRIMARY KEY NOT NULL,\n value TEXT NOT NULL\n)"
|
|
1701
3728
|
);
|
|
3729
|
+
statements.push(`${SCHEMA_CEILING_DIRECTIVE} ${schema.version}`);
|
|
1702
3730
|
statements.push(
|
|
1703
3731
|
"CREATE TABLE IF NOT EXISTS _kora_version_vector (\n node_id TEXT PRIMARY KEY NOT NULL,\n sequence_number INTEGER NOT NULL\n)"
|
|
1704
3732
|
);
|
|
@@ -1728,18 +3756,23 @@ function generateFullDDL(schema) {
|
|
|
1728
3756
|
function columnDefinition(fieldName, descriptor) {
|
|
1729
3757
|
const sqlType = mapFieldType(descriptor);
|
|
1730
3758
|
const parts = [quoteIdent(fieldName), sqlType];
|
|
1731
|
-
if (descriptor.required && descriptor.defaultValue === void 0 && !descriptor.auto) {
|
|
1732
|
-
parts.push("NOT NULL");
|
|
1733
|
-
}
|
|
1734
3759
|
if (descriptor.defaultValue !== void 0) {
|
|
1735
|
-
parts.push(`DEFAULT ${
|
|
1736
|
-
}
|
|
1737
|
-
if (descriptor.kind === "enum" && descriptor.enumValues) {
|
|
1738
|
-
const values = descriptor.enumValues.map((v) => `'${v}'`).join(", ");
|
|
1739
|
-
parts.push(`CHECK (${quoteIdent(fieldName)} IN (${values}))`);
|
|
3760
|
+
parts.push(`DEFAULT ${sqlDefaultLiteral(descriptor.defaultValue)}`);
|
|
1740
3761
|
}
|
|
1741
3762
|
return parts.join(" ");
|
|
1742
3763
|
}
|
|
3764
|
+
function sqlStringLiteral(value) {
|
|
3765
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
3766
|
+
}
|
|
3767
|
+
function sqlDefaultLiteral(value) {
|
|
3768
|
+
if (value === null) return "NULL";
|
|
3769
|
+
if (typeof value === "string") return sqlStringLiteral(value);
|
|
3770
|
+
if (typeof value === "number") {
|
|
3771
|
+
return Number.isFinite(value) ? String(value) : "NULL";
|
|
3772
|
+
}
|
|
3773
|
+
if (typeof value === "boolean") return value ? "1" : "0";
|
|
3774
|
+
return sqlStringLiteral(JSON.stringify(value) ?? "null");
|
|
3775
|
+
}
|
|
1743
3776
|
function mapFieldType(descriptor) {
|
|
1744
3777
|
switch (descriptor.kind) {
|
|
1745
3778
|
case "string":
|
|
@@ -1770,13 +3803,6 @@ function mapFieldType(descriptor) {
|
|
|
1770
3803
|
return "BLOB";
|
|
1771
3804
|
}
|
|
1772
3805
|
}
|
|
1773
|
-
function sqlDefault(value) {
|
|
1774
|
-
if (value === null) return "NULL";
|
|
1775
|
-
if (typeof value === "string") return `'${value}'`;
|
|
1776
|
-
if (typeof value === "number") return String(value);
|
|
1777
|
-
if (typeof value === "boolean") return value ? "1" : "0";
|
|
1778
|
-
return `'${JSON.stringify(value)}'`;
|
|
1779
|
-
}
|
|
1780
3806
|
|
|
1781
3807
|
// src/schema/types.ts
|
|
1782
3808
|
var FieldBuilder = class _FieldBuilder {
|
|
@@ -1792,15 +3818,21 @@ var FieldBuilder = class _FieldBuilder {
|
|
|
1792
3818
|
this._auto = auto;
|
|
1793
3819
|
this._mergeStrategy = mergeStrategy;
|
|
1794
3820
|
}
|
|
1795
|
-
/** Mark this field as optional (not required on insert) */
|
|
3821
|
+
/** Mark this field as optional (not required on insert; reads may return `null`). */
|
|
1796
3822
|
optional() {
|
|
1797
3823
|
return new _FieldBuilder(this._kind, false, this._defaultValue, this._auto, this._mergeStrategy);
|
|
1798
3824
|
}
|
|
1799
|
-
/**
|
|
3825
|
+
/**
|
|
3826
|
+
* Set a default value for this field. Implicitly makes the field optional on insert.
|
|
3827
|
+
* The value must be of the field's type (`t.number().default('x')` is a type error).
|
|
3828
|
+
*/
|
|
1800
3829
|
default(value) {
|
|
1801
3830
|
return new _FieldBuilder(this._kind, false, value, this._auto, this._mergeStrategy);
|
|
1802
3831
|
}
|
|
1803
|
-
/**
|
|
3832
|
+
/**
|
|
3833
|
+
* Mark this field as auto-populated (e.g., createdAt timestamps). Developers cannot
|
|
3834
|
+
* set auto fields; the framework fills `t.timestamp().auto()` with the insert time.
|
|
3835
|
+
*/
|
|
1804
3836
|
auto() {
|
|
1805
3837
|
return new _FieldBuilder(this._kind, false, void 0, true, this._mergeStrategy);
|
|
1806
3838
|
}
|
|
@@ -1818,13 +3850,7 @@ var FieldBuilder = class _FieldBuilder {
|
|
|
1818
3850
|
* - `'server-authoritative'`: Always prefer the remote/server value
|
|
1819
3851
|
*/
|
|
1820
3852
|
merge(strategy) {
|
|
1821
|
-
return new _FieldBuilder(
|
|
1822
|
-
this._kind,
|
|
1823
|
-
this._required,
|
|
1824
|
-
this._defaultValue,
|
|
1825
|
-
this._auto,
|
|
1826
|
-
strategy
|
|
1827
|
-
);
|
|
3853
|
+
return new _FieldBuilder(this._kind, this._required, this._defaultValue, this._auto, strategy);
|
|
1828
3854
|
}
|
|
1829
3855
|
/** @internal Build the final FieldDescriptor. Used by defineSchema(). */
|
|
1830
3856
|
_build() {
|
|
@@ -1840,6 +3866,16 @@ var FieldBuilder = class _FieldBuilder {
|
|
|
1840
3866
|
};
|
|
1841
3867
|
}
|
|
1842
3868
|
};
|
|
3869
|
+
function assertStorableEnumValues(values) {
|
|
3870
|
+
for (const value of values) {
|
|
3871
|
+
if (typeof value !== "string" || encodeStoredText(value) !== value) {
|
|
3872
|
+
throw new SchemaValidationError(
|
|
3873
|
+
`Enum value ${JSON.stringify(value)} cannot be stored: enum values must be well-formed strings without U+0000 or U+FFFF.`,
|
|
3874
|
+
{ value: String(value) }
|
|
3875
|
+
);
|
|
3876
|
+
}
|
|
3877
|
+
}
|
|
3878
|
+
}
|
|
1843
3879
|
var EnumFieldBuilder = class _EnumFieldBuilder extends FieldBuilder {
|
|
1844
3880
|
_enumValues;
|
|
1845
3881
|
_transitions;
|
|
@@ -1945,14 +3981,14 @@ var EnumFieldBuilder = class _EnumFieldBuilder extends FieldBuilder {
|
|
|
1945
3981
|
}
|
|
1946
3982
|
};
|
|
1947
3983
|
var ArrayFieldBuilder = class _ArrayFieldBuilder extends FieldBuilder {
|
|
1948
|
-
|
|
3984
|
+
_item;
|
|
1949
3985
|
constructor(itemBuilder, required = true, defaultValue = void 0, auto = false, mergeStrategy = null) {
|
|
1950
3986
|
super("array", required, defaultValue, auto, mergeStrategy);
|
|
1951
|
-
this.
|
|
3987
|
+
this._item = itemBuilder;
|
|
1952
3988
|
}
|
|
1953
3989
|
optional() {
|
|
1954
3990
|
return new _ArrayFieldBuilder(
|
|
1955
|
-
|
|
3991
|
+
this._item,
|
|
1956
3992
|
false,
|
|
1957
3993
|
this._defaultValue,
|
|
1958
3994
|
this._auto,
|
|
@@ -1960,26 +3996,14 @@ var ArrayFieldBuilder = class _ArrayFieldBuilder extends FieldBuilder {
|
|
|
1960
3996
|
);
|
|
1961
3997
|
}
|
|
1962
3998
|
default(value) {
|
|
1963
|
-
return new _ArrayFieldBuilder(
|
|
1964
|
-
new FieldBuilder(this._itemKind),
|
|
1965
|
-
false,
|
|
1966
|
-
value,
|
|
1967
|
-
this._auto,
|
|
1968
|
-
this._mergeStrategy
|
|
1969
|
-
);
|
|
3999
|
+
return new _ArrayFieldBuilder(this._item, false, value, this._auto, this._mergeStrategy);
|
|
1970
4000
|
}
|
|
1971
4001
|
auto() {
|
|
1972
|
-
return new _ArrayFieldBuilder(
|
|
1973
|
-
new FieldBuilder(this._itemKind),
|
|
1974
|
-
false,
|
|
1975
|
-
void 0,
|
|
1976
|
-
true,
|
|
1977
|
-
this._mergeStrategy
|
|
1978
|
-
);
|
|
4002
|
+
return new _ArrayFieldBuilder(this._item, false, void 0, true, this._mergeStrategy);
|
|
1979
4003
|
}
|
|
1980
4004
|
merge(strategy) {
|
|
1981
4005
|
return new _ArrayFieldBuilder(
|
|
1982
|
-
|
|
4006
|
+
this._item,
|
|
1983
4007
|
this._required,
|
|
1984
4008
|
this._defaultValue,
|
|
1985
4009
|
this._auto,
|
|
@@ -1993,7 +4017,7 @@ var ArrayFieldBuilder = class _ArrayFieldBuilder extends FieldBuilder {
|
|
|
1993
4017
|
defaultValue: this._defaultValue,
|
|
1994
4018
|
auto: this._auto,
|
|
1995
4019
|
enumValues: null,
|
|
1996
|
-
itemKind: this.
|
|
4020
|
+
itemKind: this._item._build().kind,
|
|
1997
4021
|
mergeStrategy: this._mergeStrategy,
|
|
1998
4022
|
transitions: null
|
|
1999
4023
|
};
|
|
@@ -2127,6 +4151,9 @@ var SecretFieldBuilder = class _SecretFieldBuilder extends FieldBuilder {
|
|
|
2127
4151
|
this._mergeStrategy
|
|
2128
4152
|
);
|
|
2129
4153
|
}
|
|
4154
|
+
default(value) {
|
|
4155
|
+
return new _SecretFieldBuilder(this._secretMode, false, value, this._auto, this._mergeStrategy);
|
|
4156
|
+
}
|
|
2130
4157
|
auto() {
|
|
2131
4158
|
return new _SecretFieldBuilder(this._secretMode, false, void 0, true, this._mergeStrategy);
|
|
2132
4159
|
}
|
|
@@ -2153,42 +4180,220 @@ var SecretFieldBuilder = class _SecretFieldBuilder extends FieldBuilder {
|
|
|
2153
4180
|
};
|
|
2154
4181
|
}
|
|
2155
4182
|
};
|
|
4183
|
+
function scalar(kind) {
|
|
4184
|
+
return new FieldBuilder(kind, true, void 0, false);
|
|
4185
|
+
}
|
|
2156
4186
|
var t = {
|
|
4187
|
+
/** A string field. */
|
|
2157
4188
|
string() {
|
|
2158
|
-
return
|
|
4189
|
+
return scalar("string");
|
|
2159
4190
|
},
|
|
4191
|
+
/** A finite-number field. */
|
|
2160
4192
|
number() {
|
|
2161
|
-
return
|
|
4193
|
+
return scalar("number");
|
|
2162
4194
|
},
|
|
4195
|
+
/** A boolean field. */
|
|
2163
4196
|
boolean() {
|
|
2164
|
-
return
|
|
4197
|
+
return scalar("boolean");
|
|
2165
4198
|
},
|
|
4199
|
+
/** A timestamp field: integer milliseconds since the epoch (not a `Date`). */
|
|
2166
4200
|
timestamp() {
|
|
2167
|
-
return
|
|
4201
|
+
return scalar("timestamp");
|
|
2168
4202
|
},
|
|
4203
|
+
/** A collaborative rich-text field (Yjs). Reads return the Yjs update bytes. */
|
|
2169
4204
|
richtext() {
|
|
2170
|
-
return
|
|
4205
|
+
return scalar("richtext");
|
|
2171
4206
|
},
|
|
4207
|
+
/** An enum field whose value is one of `values`. */
|
|
2172
4208
|
enum(values) {
|
|
4209
|
+
assertStorableEnumValues(values);
|
|
2173
4210
|
return new EnumFieldBuilder(values, true, void 0, false);
|
|
2174
4211
|
},
|
|
4212
|
+
/** An array field whose items have the type of `itemBuilder`. */
|
|
2175
4213
|
array(itemBuilder) {
|
|
2176
4214
|
return new ArrayFieldBuilder(itemBuilder, true, void 0, false);
|
|
2177
4215
|
},
|
|
4216
|
+
/** A structured object field whose keys are declared by nested builders. */
|
|
2178
4217
|
object(fields) {
|
|
2179
4218
|
return new ObjectFieldBuilder(fields, true, void 0, false);
|
|
2180
4219
|
},
|
|
4220
|
+
/** A dynamic JSON field typed as `T` (compile-time only; not validated against `T`). */
|
|
2181
4221
|
json() {
|
|
2182
4222
|
return new JsonFieldBuilder("json", true, void 0, false);
|
|
2183
4223
|
},
|
|
4224
|
+
/** A blob reference field (`BlobRef`); the bytes live in the blob store. */
|
|
2184
4225
|
blob() {
|
|
2185
|
-
return
|
|
4226
|
+
return scalar("blob");
|
|
2186
4227
|
},
|
|
4228
|
+
/** A secret field (password, token): redacted from traces, hashed or encrypted at rest. */
|
|
2187
4229
|
secret() {
|
|
2188
4230
|
return new SecretFieldBuilder("encrypted", true, void 0, false);
|
|
2189
4231
|
}
|
|
2190
4232
|
};
|
|
2191
4233
|
|
|
4234
|
+
// src/schema/value-domain.ts
|
|
4235
|
+
var TIMESTAMP_MIN_MS = -864e13;
|
|
4236
|
+
var TIMESTAMP_MAX_MS = 864e13;
|
|
4237
|
+
var MAX_VALUE_DEPTH = 64;
|
|
4238
|
+
var DEFAULT_MAX_OPERATION_BYTES = 256 * 1024;
|
|
4239
|
+
function timestampDomainViolation(value) {
|
|
4240
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
4241
|
+
return `must be a timestamp (integer milliseconds), got ${describe2(value)}`;
|
|
4242
|
+
}
|
|
4243
|
+
if (!Number.isInteger(value)) {
|
|
4244
|
+
return `must be a timestamp in whole milliseconds, got ${String(value)}. Round it (Math.round) or use Date.getTime()`;
|
|
4245
|
+
}
|
|
4246
|
+
if (value < TIMESTAMP_MIN_MS || value > TIMESTAMP_MAX_MS) {
|
|
4247
|
+
return `must be a timestamp between ${TIMESTAMP_MIN_MS} and ${TIMESTAMP_MAX_MS} (the range of a JavaScript Date), got ${String(value)}`;
|
|
4248
|
+
}
|
|
4249
|
+
return null;
|
|
4250
|
+
}
|
|
4251
|
+
function valueDepthViolation(value, maxDepth = MAX_VALUE_DEPTH) {
|
|
4252
|
+
const walk = (current, depth, path) => {
|
|
4253
|
+
if (current === null || typeof current !== "object") return null;
|
|
4254
|
+
if (depth > maxDepth) return path;
|
|
4255
|
+
if (current instanceof Uint8Array || current instanceof ArrayBuffer) return null;
|
|
4256
|
+
const entries = Array.isArray(current) ? current.map((item, index) => [`[${index}]`, item]) : Object.entries(current).map(
|
|
4257
|
+
([key, item]) => [`.${key}`, item]
|
|
4258
|
+
);
|
|
4259
|
+
for (const [suffix, item] of entries) {
|
|
4260
|
+
const found = walk(item, depth + 1, `${path}${suffix}`);
|
|
4261
|
+
if (found !== null) return found;
|
|
4262
|
+
}
|
|
4263
|
+
return null;
|
|
4264
|
+
};
|
|
4265
|
+
return walk(value, 1, "");
|
|
4266
|
+
}
|
|
4267
|
+
function protoKeyViolation(value) {
|
|
4268
|
+
const walk = (current, path) => {
|
|
4269
|
+
if (current === null || typeof current !== "object") return null;
|
|
4270
|
+
if (current instanceof Uint8Array || current instanceof ArrayBuffer) return null;
|
|
4271
|
+
if (Array.isArray(current)) {
|
|
4272
|
+
for (let index = 0; index < current.length; index++) {
|
|
4273
|
+
const found = walk(current[index], `${path}[${index}]`);
|
|
4274
|
+
if (found !== null) return found;
|
|
4275
|
+
}
|
|
4276
|
+
return null;
|
|
4277
|
+
}
|
|
4278
|
+
if (Object.prototype.hasOwnProperty.call(current, "__proto__")) return `${path}.__proto__`;
|
|
4279
|
+
for (const [key, item] of Object.entries(current)) {
|
|
4280
|
+
const found = walk(item, `${path}.${key}`);
|
|
4281
|
+
if (found !== null) return found;
|
|
4282
|
+
}
|
|
4283
|
+
return null;
|
|
4284
|
+
};
|
|
4285
|
+
return walk(value, "");
|
|
4286
|
+
}
|
|
4287
|
+
function operationValueViolation(descriptor, value) {
|
|
4288
|
+
if (value === null || value === void 0) return null;
|
|
4289
|
+
switch (descriptor.kind) {
|
|
4290
|
+
case "string":
|
|
4291
|
+
case "secret":
|
|
4292
|
+
return typeof value === "string" ? null : `must be a string, got ${describe2(value)}`;
|
|
4293
|
+
case "enum":
|
|
4294
|
+
if (typeof value !== "string") return `must be a string (enum), got ${describe2(value)}`;
|
|
4295
|
+
if (descriptor.enumValues && !descriptor.enumValues.includes(value)) {
|
|
4296
|
+
return `must be one of: ${descriptor.enumValues.join(", ")}. Got "${value}"`;
|
|
4297
|
+
}
|
|
4298
|
+
return null;
|
|
4299
|
+
case "number":
|
|
4300
|
+
return typeof value === "number" && Number.isFinite(value) ? null : `must be a finite number, got ${describe2(value)}`;
|
|
4301
|
+
case "boolean":
|
|
4302
|
+
return typeof value === "boolean" ? null : `must be a boolean, got ${describe2(value)}`;
|
|
4303
|
+
case "timestamp":
|
|
4304
|
+
return timestampDomainViolation(value);
|
|
4305
|
+
case "richtext":
|
|
4306
|
+
return typeof value === "string" || isKoraBytesValue(value) || value instanceof Uint8Array ? null : `must be richtext (Yjs bytes or a string), got ${describe2(value)}`;
|
|
4307
|
+
case "blob":
|
|
4308
|
+
return isBlobRef(value) ? null : `must be a BlobRef, got ${describe2(value)}`;
|
|
4309
|
+
case "array": {
|
|
4310
|
+
if (!Array.isArray(value)) return `must be an array, got ${describe2(value)}`;
|
|
4311
|
+
const item = descriptor.itemKind;
|
|
4312
|
+
for (let index = 0; index < value.length; index++) {
|
|
4313
|
+
const element = value[index];
|
|
4314
|
+
if (item === "timestamp") {
|
|
4315
|
+
const violation = timestampDomainViolation(element);
|
|
4316
|
+
if (violation) return `[${index}] ${violation}`;
|
|
4317
|
+
} else if (item === "number" && (typeof element !== "number" || !Number.isFinite(element))) {
|
|
4318
|
+
return `[${index}] must be a finite number, got ${describe2(element)}`;
|
|
4319
|
+
} else if ((item === "string" || item === "enum") && typeof element !== "string") {
|
|
4320
|
+
return `[${index}] must be a string, got ${describe2(element)}`;
|
|
4321
|
+
} else if (item === "boolean" && typeof element !== "boolean") {
|
|
4322
|
+
return `[${index}] must be a boolean, got ${describe2(element)}`;
|
|
4323
|
+
}
|
|
4324
|
+
}
|
|
4325
|
+
return structureViolation(value);
|
|
4326
|
+
}
|
|
4327
|
+
case "object": {
|
|
4328
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
4329
|
+
return `must be a plain object, got ${describe2(value)}`;
|
|
4330
|
+
}
|
|
4331
|
+
for (const [name, nested] of Object.entries(descriptor.nestedFields ?? {})) {
|
|
4332
|
+
const violation = operationValueViolation(nested, value[name]);
|
|
4333
|
+
if (violation) return `.${name} ${violation}`;
|
|
4334
|
+
}
|
|
4335
|
+
return structureViolation(value);
|
|
4336
|
+
}
|
|
4337
|
+
case "json":
|
|
4338
|
+
return structureViolation(value);
|
|
4339
|
+
}
|
|
4340
|
+
}
|
|
4341
|
+
var WIRE_BYTES_KEY = "__kora_bytes__";
|
|
4342
|
+
function reservedWireShapeViolation(value) {
|
|
4343
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return null;
|
|
4344
|
+
const keys = Object.keys(value);
|
|
4345
|
+
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;
|
|
4346
|
+
}
|
|
4347
|
+
function structureViolation(value) {
|
|
4348
|
+
const reserved = reservedWireShapeViolation(value);
|
|
4349
|
+
if (reserved !== null) return reserved;
|
|
4350
|
+
const deep = valueDepthViolation(value);
|
|
4351
|
+
if (deep !== null) return `nests deeper than ${MAX_VALUE_DEPTH} levels at "${deep}"`;
|
|
4352
|
+
const proto = protoKeyViolation(value);
|
|
4353
|
+
if (proto !== null) return `holds a "__proto__" key at "${proto}", which is not supported`;
|
|
4354
|
+
const walk = (current, path) => {
|
|
4355
|
+
if (typeof current === "number") {
|
|
4356
|
+
return Number.isFinite(current) ? null : `holds ${String(current)} at "${path}"`;
|
|
4357
|
+
}
|
|
4358
|
+
if (current === null || typeof current !== "object") return null;
|
|
4359
|
+
const entries = Array.isArray(current) ? current.map((item, index) => [`${path}[${index}]`, item]) : Object.entries(current).map(
|
|
4360
|
+
([key, item]) => [`${path}.${key}`, item]
|
|
4361
|
+
);
|
|
4362
|
+
for (const [next, item] of entries) {
|
|
4363
|
+
const found = walk(item, next);
|
|
4364
|
+
if (found !== null) return found;
|
|
4365
|
+
}
|
|
4366
|
+
return null;
|
|
4367
|
+
};
|
|
4368
|
+
return walk(value, "");
|
|
4369
|
+
}
|
|
4370
|
+
function measureOperationBytes(op2) {
|
|
4371
|
+
return utf8Length(JSON.stringify(op2));
|
|
4372
|
+
}
|
|
4373
|
+
function utf8Length(text) {
|
|
4374
|
+
let bytes = 0;
|
|
4375
|
+
for (let i = 0; i < text.length; i++) {
|
|
4376
|
+
const code = text.charCodeAt(i);
|
|
4377
|
+
if (code < 128) bytes += 1;
|
|
4378
|
+
else if (code < 2048) bytes += 2;
|
|
4379
|
+
else if (code >= 55296 && code <= 56319 && i + 1 < text.length) {
|
|
4380
|
+
const next = text.charCodeAt(i + 1);
|
|
4381
|
+
if (next >= 56320 && next <= 57343) {
|
|
4382
|
+
bytes += 4;
|
|
4383
|
+
i++;
|
|
4384
|
+
} else bytes += 3;
|
|
4385
|
+
} else bytes += 3;
|
|
4386
|
+
}
|
|
4387
|
+
return bytes;
|
|
4388
|
+
}
|
|
4389
|
+
function describe2(value) {
|
|
4390
|
+
if (value === null) return "null";
|
|
4391
|
+
if (Array.isArray(value)) return "array";
|
|
4392
|
+
if (typeof value === "number") return Number.isFinite(value) ? "number" : String(value);
|
|
4393
|
+
if (value instanceof Date) return "Date";
|
|
4394
|
+
return typeof value;
|
|
4395
|
+
}
|
|
4396
|
+
|
|
2192
4397
|
// src/schema/validation.ts
|
|
2193
4398
|
function validateRecord(collection, collectionDef, data, operationType) {
|
|
2194
4399
|
if (operationType === "delete") {
|
|
@@ -2219,7 +4424,7 @@ function validateRecord(collection, collectionDef, data, operationType) {
|
|
|
2219
4424
|
result[fieldName] = value;
|
|
2220
4425
|
} else if (value !== void 0 && value !== null) {
|
|
2221
4426
|
validateFieldValue(collection, fieldName, descriptor, value);
|
|
2222
|
-
result[fieldName] = value;
|
|
4427
|
+
result[fieldName] = canonicalFieldValue(collection, fieldName, descriptor, value);
|
|
2223
4428
|
} else {
|
|
2224
4429
|
result[fieldName] = value;
|
|
2225
4430
|
}
|
|
@@ -2243,10 +4448,29 @@ function validateRecord(collection, collectionDef, data, operationType) {
|
|
|
2243
4448
|
continue;
|
|
2244
4449
|
}
|
|
2245
4450
|
validateFieldValue(collection, fieldName, descriptor, value);
|
|
2246
|
-
result[fieldName] = value;
|
|
4451
|
+
result[fieldName] = canonicalFieldValue(collection, fieldName, descriptor, value);
|
|
2247
4452
|
}
|
|
2248
4453
|
return result;
|
|
2249
4454
|
}
|
|
4455
|
+
function canonicalFieldValue(collection, fieldName, descriptor, value) {
|
|
4456
|
+
if (descriptor.kind === "richtext") return value;
|
|
4457
|
+
try {
|
|
4458
|
+
return canonicalValue(value, `${collection}.${fieldName}`);
|
|
4459
|
+
} catch (error) {
|
|
4460
|
+
if (error instanceof NonCanonicalValueError) {
|
|
4461
|
+
throw new SchemaValidationError(
|
|
4462
|
+
`Field "${fieldName}" in collection "${collection}": ${error.message}`,
|
|
4463
|
+
{
|
|
4464
|
+
collection,
|
|
4465
|
+
field: fieldName,
|
|
4466
|
+
path: error.path,
|
|
4467
|
+
receivedType: error.receivedType
|
|
4468
|
+
}
|
|
4469
|
+
);
|
|
4470
|
+
}
|
|
4471
|
+
throw error;
|
|
4472
|
+
}
|
|
4473
|
+
}
|
|
2250
4474
|
function validateFieldValue(collection, fieldName, descriptor, value) {
|
|
2251
4475
|
switch (descriptor.kind) {
|
|
2252
4476
|
case "string": {
|
|
@@ -2259,9 +4483,9 @@ function validateFieldValue(collection, fieldName, descriptor, value) {
|
|
|
2259
4483
|
break;
|
|
2260
4484
|
}
|
|
2261
4485
|
case "number": {
|
|
2262
|
-
if (typeof value !== "number" || Number.
|
|
4486
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
2263
4487
|
throw new SchemaValidationError(
|
|
2264
|
-
`Field "${fieldName}" in collection "${collection}" must be a number, got ${typeof value}`,
|
|
4488
|
+
`Field "${fieldName}" in collection "${collection}" must be a finite number, got ${typeof value === "number" ? String(value) : typeof value}`,
|
|
2265
4489
|
{ collection, field: fieldName, expectedType: "number", receivedType: typeof value }
|
|
2266
4490
|
);
|
|
2267
4491
|
}
|
|
@@ -2277,14 +4501,16 @@ function validateFieldValue(collection, fieldName, descriptor, value) {
|
|
|
2277
4501
|
break;
|
|
2278
4502
|
}
|
|
2279
4503
|
case "timestamp": {
|
|
2280
|
-
|
|
4504
|
+
const violation = timestampDomainViolation(value);
|
|
4505
|
+
if (violation !== null) {
|
|
2281
4506
|
throw new SchemaValidationError(
|
|
2282
|
-
`Field "${fieldName}" in collection "${collection}"
|
|
4507
|
+
`Field "${fieldName}" in collection "${collection}" ${violation}`,
|
|
2283
4508
|
{
|
|
2284
4509
|
collection,
|
|
2285
4510
|
field: fieldName,
|
|
2286
4511
|
expectedType: "timestamp",
|
|
2287
|
-
receivedType: typeof value
|
|
4512
|
+
receivedType: typeof value,
|
|
4513
|
+
received: value
|
|
2288
4514
|
}
|
|
2289
4515
|
);
|
|
2290
4516
|
}
|
|
@@ -2321,6 +4547,14 @@ function validateFieldValue(collection, fieldName, descriptor, value) {
|
|
|
2321
4547
|
const expectedType = jsTypeForKind(descriptor.itemKind);
|
|
2322
4548
|
for (let i = 0; i < value.length; i++) {
|
|
2323
4549
|
const item = value[i];
|
|
4550
|
+
if ((descriptor.itemKind === "timestamp" || descriptor.itemKind === "number") && item !== null && item !== void 0) {
|
|
4551
|
+
validateFieldValue(
|
|
4552
|
+
collection,
|
|
4553
|
+
`${fieldName}[${i}]`,
|
|
4554
|
+
{ ...descriptor, kind: descriptor.itemKind, itemKind: null },
|
|
4555
|
+
item
|
|
4556
|
+
);
|
|
4557
|
+
}
|
|
2324
4558
|
if (!matchesJsType(item, expectedType)) {
|
|
2325
4559
|
throw new SchemaValidationError(
|
|
2326
4560
|
`Field "${fieldName}[${i}]" in collection "${collection}" must be a ${descriptor.itemKind}, got ${typeof item}`,
|
|
@@ -2334,6 +4568,7 @@ function validateFieldValue(collection, fieldName, descriptor, value) {
|
|
|
2334
4568
|
}
|
|
2335
4569
|
}
|
|
2336
4570
|
}
|
|
4571
|
+
assertStructureInDomain(collection, fieldName, value);
|
|
2337
4572
|
break;
|
|
2338
4573
|
}
|
|
2339
4574
|
case "richtext": {
|
|
@@ -2351,7 +4586,7 @@ function validateFieldValue(collection, fieldName, descriptor, value) {
|
|
|
2351
4586
|
break;
|
|
2352
4587
|
}
|
|
2353
4588
|
case "object": {
|
|
2354
|
-
if (!
|
|
4589
|
+
if (!isPlainObject3(value)) {
|
|
2355
4590
|
throw new SchemaValidationError(
|
|
2356
4591
|
`Field "${fieldName}" in collection "${collection}" must be a plain object, got ${describeType(value)}`,
|
|
2357
4592
|
{
|
|
@@ -2375,6 +4610,7 @@ function validateFieldValue(collection, fieldName, descriptor, value) {
|
|
|
2375
4610
|
}
|
|
2376
4611
|
}
|
|
2377
4612
|
}
|
|
4613
|
+
assertStructureInDomain(collection, fieldName, value);
|
|
2378
4614
|
break;
|
|
2379
4615
|
}
|
|
2380
4616
|
case "json": {
|
|
@@ -2391,6 +4627,7 @@ function validateFieldValue(collection, fieldName, descriptor, value) {
|
|
|
2391
4627
|
}
|
|
2392
4628
|
);
|
|
2393
4629
|
}
|
|
4630
|
+
assertStructureInDomain(collection, fieldName, value);
|
|
2394
4631
|
break;
|
|
2395
4632
|
}
|
|
2396
4633
|
case "blob": {
|
|
@@ -2418,7 +4655,34 @@ function validateFieldValue(collection, fieldName, descriptor, value) {
|
|
|
2418
4655
|
}
|
|
2419
4656
|
}
|
|
2420
4657
|
}
|
|
2421
|
-
function
|
|
4658
|
+
function assertStructureInDomain(collection, fieldName, value) {
|
|
4659
|
+
const reserved = reservedWireShapeViolation(value);
|
|
4660
|
+
if (reserved !== null) {
|
|
4661
|
+
throw new SchemaValidationError(
|
|
4662
|
+
`Field "${fieldName}" in collection "${collection}" ${reserved}.`,
|
|
4663
|
+
{ collection, field: fieldName }
|
|
4664
|
+
);
|
|
4665
|
+
}
|
|
4666
|
+
const deep = valueDepthViolation(value);
|
|
4667
|
+
if (deep !== null) {
|
|
4668
|
+
throw new SchemaValidationError(
|
|
4669
|
+
`Field "${fieldName}" in collection "${collection}" nests deeper than ${MAX_VALUE_DEPTH} levels (at "${fieldName}${deep}"). Flatten the value.`,
|
|
4670
|
+
{ collection, field: fieldName, path: `${fieldName}${deep}`, maxDepth: MAX_VALUE_DEPTH }
|
|
4671
|
+
);
|
|
4672
|
+
}
|
|
4673
|
+
const proto = protoKeyViolation(value);
|
|
4674
|
+
if (proto !== null) {
|
|
4675
|
+
throw new SchemaValidationError(
|
|
4676
|
+
`Field "${fieldName}" in collection "${collection}" holds a "__proto__" key (at "${fieldName}${proto}"), which cannot be stored and synced unchanged. Rename the key.`,
|
|
4677
|
+
{ collection, field: fieldName, path: `${fieldName}${proto}` }
|
|
4678
|
+
);
|
|
4679
|
+
}
|
|
4680
|
+
}
|
|
4681
|
+
function assertResolvedFieldValue(collection, fieldName, descriptor, value) {
|
|
4682
|
+
if (value === null || value === void 0 || descriptor.kind === "richtext") return;
|
|
4683
|
+
validateFieldValue(collection, fieldName, descriptor, value);
|
|
4684
|
+
}
|
|
4685
|
+
function isPlainObject3(value) {
|
|
2422
4686
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
2423
4687
|
return false;
|
|
2424
4688
|
}
|
|
@@ -2690,6 +4954,17 @@ function buildScopeMap(schema, scopeValues) {
|
|
|
2690
4954
|
return result;
|
|
2691
4955
|
}
|
|
2692
4956
|
|
|
4957
|
+
// src/scopes/scope-grant.ts
|
|
4958
|
+
var SCOPE_CLAIMS_KEY = "$claims";
|
|
4959
|
+
function claimScopes(claims, explicit) {
|
|
4960
|
+
return { ...explicit ?? {}, [SCOPE_CLAIMS_KEY]: { ...claims } };
|
|
4961
|
+
}
|
|
4962
|
+
function getScopeClaims(grant) {
|
|
4963
|
+
if (!grant) return null;
|
|
4964
|
+
const claims = grant[SCOPE_CLAIMS_KEY];
|
|
4965
|
+
return claims && typeof claims === "object" ? claims : null;
|
|
4966
|
+
}
|
|
4967
|
+
|
|
2693
4968
|
// src/scopes/extract-scope-from-claims.ts
|
|
2694
4969
|
function collectSchemaScopeFields(schema) {
|
|
2695
4970
|
return collectSchemaScopeValueKeys(schema);
|
|
@@ -2744,29 +5019,6 @@ function defaultSequenceFormat(name) {
|
|
|
2744
5019
|
return `${name}-{seq:4}`;
|
|
2745
5020
|
}
|
|
2746
5021
|
|
|
2747
|
-
// src/migration/apply-operation-transforms.ts
|
|
2748
|
-
var MAX_TRANSFORM_STEPS = 32;
|
|
2749
|
-
function applyOperationTransforms(operation, targetSchemaVersion, transforms) {
|
|
2750
|
-
let current = operation;
|
|
2751
|
-
for (let step = 0; step < MAX_TRANSFORM_STEPS; step++) {
|
|
2752
|
-
if (current.schemaVersion === targetSchemaVersion) {
|
|
2753
|
-
return current;
|
|
2754
|
-
}
|
|
2755
|
-
const transform = transforms.find(
|
|
2756
|
-
(candidate) => candidate.fromVersion === current.schemaVersion
|
|
2757
|
-
);
|
|
2758
|
-
if (!transform) {
|
|
2759
|
-
return null;
|
|
2760
|
-
}
|
|
2761
|
-
const next = transform.transform(current);
|
|
2762
|
-
if (next === null) {
|
|
2763
|
-
return null;
|
|
2764
|
-
}
|
|
2765
|
-
current = next;
|
|
2766
|
-
}
|
|
2767
|
-
return null;
|
|
2768
|
-
}
|
|
2769
|
-
|
|
2770
5022
|
// src/migrations/migration-builder.ts
|
|
2771
5023
|
var RollbackBuilder = class {
|
|
2772
5024
|
_steps = [];
|
|
@@ -2872,17 +5124,29 @@ var MigrationBuilder = class _MigrationBuilder {
|
|
|
2872
5124
|
}
|
|
2873
5125
|
/**
|
|
2874
5126
|
* Backfill records in a collection using a transform function.
|
|
2875
|
-
* The transform receives each record
|
|
2876
|
-
* Runs after structural changes (addField, renameField,
|
|
5127
|
+
* The transform receives each live record (typed values, as the app reads them) and
|
|
5128
|
+
* returns the fields to change. Runs after structural changes (addField, renameField,
|
|
5129
|
+
* etc.), in the same transaction as them and the schema version write.
|
|
5130
|
+
*
|
|
5131
|
+
* Changed records are written as update operations (mutation name `migration:v<N>`)
|
|
5132
|
+
* through the normal local write path, so backfilled values sync. Pass
|
|
5133
|
+
* `{ localOnly: true }` for device-local data that must not sync.
|
|
2877
5134
|
*
|
|
2878
5135
|
* @param collection - The collection name
|
|
2879
5136
|
* @param transform - Forward transform function
|
|
2880
|
-
* @param
|
|
5137
|
+
* @param reverseOrOptions - Reverse transform for rollback support, or options
|
|
2881
5138
|
*/
|
|
2882
|
-
backfill(collection, transform,
|
|
5139
|
+
backfill(collection, transform, reverseOrOptions) {
|
|
5140
|
+
const options = typeof reverseOrOptions === "function" ? { reverseTransform: reverseOrOptions } : reverseOrOptions ?? {};
|
|
2883
5141
|
return new _MigrationBuilder([
|
|
2884
5142
|
...this.steps,
|
|
2885
|
-
{
|
|
5143
|
+
{
|
|
5144
|
+
type: "backfill",
|
|
5145
|
+
collection,
|
|
5146
|
+
transform,
|
|
5147
|
+
...options.reverseTransform ? { reverseTransform: options.reverseTransform } : {},
|
|
5148
|
+
...options.localOnly ? { localOnly: true } : {}
|
|
5149
|
+
}
|
|
2886
5150
|
]);
|
|
2887
5151
|
}
|
|
2888
5152
|
/**
|
|
@@ -3029,11 +5293,13 @@ function migrationStepsToSQL(steps) {
|
|
|
3029
5293
|
break;
|
|
3030
5294
|
case "addIndex":
|
|
3031
5295
|
statements.push(
|
|
3032
|
-
`CREATE INDEX IF NOT EXISTS
|
|
5296
|
+
`CREATE INDEX IF NOT EXISTS ${quoteIdent(collectionIndexName(step.collection, step.field))} ON ${quoteIdent(step.collection)} (${quoteIdent(step.field)})`
|
|
3033
5297
|
);
|
|
3034
5298
|
break;
|
|
3035
5299
|
case "removeIndex":
|
|
3036
|
-
statements.push(
|
|
5300
|
+
statements.push(
|
|
5301
|
+
`DROP INDEX IF EXISTS ${quoteIdent(collectionIndexName(step.collection, step.field))}`
|
|
5302
|
+
);
|
|
3037
5303
|
break;
|
|
3038
5304
|
case "backfill":
|
|
3039
5305
|
break;
|
|
@@ -3049,11 +5315,7 @@ function addFieldSQL(collection, field, descriptor) {
|
|
|
3049
5315
|
const sqlType = mapFieldType2(descriptor);
|
|
3050
5316
|
const parts = [`ALTER TABLE ${quoteIdent(collection)} ADD COLUMN ${quoteIdent(field)}`, sqlType];
|
|
3051
5317
|
if (descriptor.defaultValue !== void 0) {
|
|
3052
|
-
parts.push(`DEFAULT ${
|
|
3053
|
-
}
|
|
3054
|
-
if (descriptor.kind === "enum" && descriptor.enumValues) {
|
|
3055
|
-
const values = descriptor.enumValues.map((v) => `'${v}'`).join(", ");
|
|
3056
|
-
parts.push(`CHECK (${quoteIdent(field)} IN (${values}))`);
|
|
5318
|
+
parts.push(`DEFAULT ${sqlDefaultLiteral(descriptor.defaultValue)}`);
|
|
3057
5319
|
}
|
|
3058
5320
|
return parts.join(" ");
|
|
3059
5321
|
}
|
|
@@ -3083,13 +5345,6 @@ function mapFieldType2(descriptor) {
|
|
|
3083
5345
|
return "BLOB";
|
|
3084
5346
|
}
|
|
3085
5347
|
}
|
|
3086
|
-
function sqlDefault2(value) {
|
|
3087
|
-
if (value === null) return "NULL";
|
|
3088
|
-
if (typeof value === "string") return `'${value}'`;
|
|
3089
|
-
if (typeof value === "number") return String(value);
|
|
3090
|
-
if (typeof value === "boolean") return value ? "1" : "0";
|
|
3091
|
-
return `'${JSON.stringify(value)}'`;
|
|
3092
|
-
}
|
|
3093
5348
|
|
|
3094
5349
|
// src/codegen/proto-generator.ts
|
|
3095
5350
|
var SCALAR_TYPE_MAP = {
|
|
@@ -3194,15 +5449,20 @@ var KORA_OPERATION_MESSAGE = `message KoraOperation {
|
|
|
3194
5449
|
var OPERATION_BATCH_MESSAGE = `message OperationBatch {
|
|
3195
5450
|
repeated KoraOperation operations = 1;
|
|
3196
5451
|
bool is_final = 2;
|
|
5452
|
+
bytes retractions_json = 3;
|
|
3197
5453
|
}`;
|
|
3198
5454
|
var HANDSHAKE_MESSAGE = `message HandshakeMessage {
|
|
3199
5455
|
map<string, int64> version_vector = 1;
|
|
3200
5456
|
int32 schema_version = 2;
|
|
3201
5457
|
string node_id = 3;
|
|
5458
|
+
string scope_exit_policy = 4;
|
|
3202
5459
|
}`;
|
|
3203
5460
|
var HANDSHAKE_RESPONSE_MESSAGE = `message HandshakeResponse {
|
|
3204
5461
|
map<string, int64> version_vector = 1;
|
|
3205
5462
|
int32 schema_version = 2;
|
|
5463
|
+
bytes accepted_scope_json = 3;
|
|
5464
|
+
bytes accepted_downlink_scopes_json = 4;
|
|
5465
|
+
bytes accepted_uplink_scopes_json = 5;
|
|
3206
5466
|
}`;
|
|
3207
5467
|
var ACKNOWLEDGMENT_MESSAGE = `message Acknowledgment {
|
|
3208
5468
|
int64 sequence_number = 1;
|
|
@@ -3268,20 +5528,25 @@ function buildJsonDescriptor(schema) {
|
|
|
3268
5528
|
nested.OperationBatch = {
|
|
3269
5529
|
fields: {
|
|
3270
5530
|
operations: { type: "KoraOperation", id: 1, rule: "repeated" },
|
|
3271
|
-
is_final: { type: "bool", id: 2 }
|
|
5531
|
+
is_final: { type: "bool", id: 2 },
|
|
5532
|
+
retractions_json: { type: "bytes", id: 3 }
|
|
3272
5533
|
}
|
|
3273
5534
|
};
|
|
3274
5535
|
nested.HandshakeMessage = {
|
|
3275
5536
|
fields: {
|
|
3276
5537
|
version_vector: { keyType: "string", type: "int64", id: 1 },
|
|
3277
5538
|
schema_version: { type: "int32", id: 2 },
|
|
3278
|
-
node_id: { type: "string", id: 3 }
|
|
5539
|
+
node_id: { type: "string", id: 3 },
|
|
5540
|
+
scope_exit_policy: { type: "string", id: 4 }
|
|
3279
5541
|
}
|
|
3280
5542
|
};
|
|
3281
5543
|
nested.HandshakeResponse = {
|
|
3282
5544
|
fields: {
|
|
3283
5545
|
version_vector: { keyType: "string", type: "int64", id: 1 },
|
|
3284
|
-
schema_version: { type: "int32", id: 2 }
|
|
5546
|
+
schema_version: { type: "int32", id: 2 },
|
|
5547
|
+
accepted_scope_json: { type: "bytes", id: 3 },
|
|
5548
|
+
accepted_downlink_scopes_json: { type: "bytes", id: 4 },
|
|
5549
|
+
accepted_uplink_scopes_json: { type: "bytes", id: 5 }
|
|
3285
5550
|
}
|
|
3286
5551
|
};
|
|
3287
5552
|
nested.Acknowledgment = {
|
|
@@ -3337,50 +5602,91 @@ function generateProtoDefinitions(schema) {
|
|
|
3337
5602
|
CONNECTION_QUALITIES,
|
|
3338
5603
|
CausalTracker,
|
|
3339
5604
|
ClockDriftError,
|
|
5605
|
+
DEFAULT_MAX_OPERATION_BYTES,
|
|
5606
|
+
DEFAULT_OPERATION_HASH_VERSION,
|
|
3340
5607
|
EnumFieldBuilder,
|
|
5608
|
+
FOLD_RECORD_TRACE_FIELD,
|
|
5609
|
+
FOLD_STATE_VERSION,
|
|
3341
5610
|
FieldBuilder,
|
|
5611
|
+
FoldConfigurationError,
|
|
5612
|
+
FoldStateError,
|
|
3342
5613
|
HybridLogicalClock,
|
|
3343
5614
|
InvalidTimestampError,
|
|
5615
|
+
JsonFieldBuilder,
|
|
3344
5616
|
KORA_ERROR_FIX_SUGGESTIONS,
|
|
3345
5617
|
KoraError,
|
|
3346
5618
|
MAX_LOGICAL,
|
|
5619
|
+
MAX_VALUE_DEPTH,
|
|
3347
5620
|
MERGE_STRATEGIES,
|
|
3348
5621
|
MergeConflictError,
|
|
3349
5622
|
MigrationBuilder,
|
|
3350
5623
|
MigrationRollbackError,
|
|
3351
5624
|
MissingSecretKeyError,
|
|
5625
|
+
NonCanonicalValueError,
|
|
5626
|
+
ObjectFieldBuilder,
|
|
3352
5627
|
OperationError,
|
|
5628
|
+
OperationTooLargeError,
|
|
5629
|
+
OperationTransformCoverageError,
|
|
5630
|
+
OperationTransformError,
|
|
5631
|
+
RESERVED_NODE_ID_PREFIX,
|
|
3353
5632
|
RemoteClockDriftError,
|
|
3354
5633
|
RollbackBuilder,
|
|
5634
|
+
SCOPE_CLAIMS_KEY,
|
|
5635
|
+
SERVER_NODE_ID_PREFIX,
|
|
5636
|
+
STORED_TEXT_ESCAPE,
|
|
3355
5637
|
SchemaValidationError,
|
|
5638
|
+
SecretFieldBuilder,
|
|
3356
5639
|
StorageError,
|
|
3357
5640
|
SyncError,
|
|
5641
|
+
TIMESTAMP_MAX_MS,
|
|
5642
|
+
TIMESTAMP_MIN_MS,
|
|
5643
|
+
adaptFoldState,
|
|
3358
5644
|
advanceVector,
|
|
3359
5645
|
applyAtomicOp,
|
|
3360
5646
|
applyOperationTransforms,
|
|
5647
|
+
assertOperationTransformCoverage,
|
|
5648
|
+
assertResolvedFieldValue,
|
|
3361
5649
|
base64ToBytes,
|
|
3362
5650
|
buildScopeMap,
|
|
3363
5651
|
buildStateMachineConstraints,
|
|
3364
5652
|
bytesToBase64,
|
|
3365
5653
|
canAutoRollback,
|
|
5654
|
+
canonicalValue,
|
|
5655
|
+
canonicalizeLegacyOperation,
|
|
5656
|
+
canonicalizeOperationBody,
|
|
5657
|
+
canonicalizeProvenLegacyClear,
|
|
5658
|
+
claimScopes,
|
|
3366
5659
|
collectSchemaScopeFields,
|
|
3367
5660
|
collectSchemaScopeValueKeys,
|
|
3368
5661
|
computeDelta,
|
|
5662
|
+
computeOperationId,
|
|
3369
5663
|
createBlobRef,
|
|
5664
|
+
createFoldState,
|
|
3370
5665
|
createOperation,
|
|
3371
5666
|
createReversibleMigration,
|
|
5667
|
+
createSnapshotState,
|
|
3372
5668
|
createVersionVector,
|
|
3373
5669
|
decodeBytesFromOpData,
|
|
5670
|
+
decodeStoredJsonValue,
|
|
5671
|
+
decodeStoredText,
|
|
3374
5672
|
decryptSecret,
|
|
3375
5673
|
defaultApplyFailureReason,
|
|
3376
5674
|
defaultSequenceFormat,
|
|
3377
5675
|
defineSchema,
|
|
5676
|
+
deriveSideEffectOpId,
|
|
5677
|
+
deserializeFoldState,
|
|
3378
5678
|
deserializeVector,
|
|
3379
5679
|
dominates,
|
|
3380
5680
|
encodeBytesForOpData,
|
|
5681
|
+
encodeStoredJsonValue,
|
|
5682
|
+
encodeStoredText,
|
|
3381
5683
|
encryptSecret,
|
|
5684
|
+
expandFieldVersionedOperations,
|
|
3382
5685
|
extractScopeValuesFromClaims,
|
|
3383
5686
|
extractTimestamp,
|
|
5687
|
+
foldPlanFingerprint,
|
|
5688
|
+
foldPlanFingerprints,
|
|
5689
|
+
foldRecord,
|
|
3384
5690
|
formatSequenceValue,
|
|
3385
5691
|
generateFullDDL,
|
|
3386
5692
|
generateProtoDefinitions,
|
|
@@ -3388,35 +5694,61 @@ function generateProtoDefinitions(schema) {
|
|
|
3388
5694
|
generateSQL,
|
|
3389
5695
|
generateUUIDv7,
|
|
3390
5696
|
getCollectionScopeBindings,
|
|
5697
|
+
getFoldFieldVersionStrings,
|
|
5698
|
+
getFoldFieldVersions,
|
|
3391
5699
|
getKoraErrorFix,
|
|
5700
|
+
getScopeClaims,
|
|
3392
5701
|
getTransitionMap,
|
|
3393
5702
|
hasSchemaSyncRules,
|
|
3394
5703
|
hashBlob,
|
|
3395
5704
|
hashSecret,
|
|
3396
5705
|
isApplyFailure,
|
|
3397
5706
|
isAtomicOp,
|
|
5707
|
+
isAuthoritativeNodeId,
|
|
3398
5708
|
isBlobRef,
|
|
3399
5709
|
isCollectionSyncScoped,
|
|
5710
|
+
isFoldStateLive,
|
|
3400
5711
|
isKoraBytesValue,
|
|
3401
5712
|
isLegacyNumericByteObject,
|
|
5713
|
+
isReservedNodeId,
|
|
5714
|
+
isServerNodeId,
|
|
3402
5715
|
isValidOperation,
|
|
3403
5716
|
isValidUUIDv7,
|
|
5717
|
+
joinStates,
|
|
5718
|
+
materialize,
|
|
5719
|
+
measureOperationBytes,
|
|
5720
|
+
mergeArraySet,
|
|
5721
|
+
mergeOp,
|
|
3404
5722
|
mergeVectors,
|
|
3405
5723
|
migrate,
|
|
3406
5724
|
migrationStepsToSQL,
|
|
5725
|
+
mismatchedFoldFields,
|
|
5726
|
+
missingTransformPaths,
|
|
5727
|
+
needsStoredTextEncoding,
|
|
3407
5728
|
op,
|
|
5729
|
+
operationSchemaView,
|
|
5730
|
+
operationTransformsFingerprint,
|
|
5731
|
+
operationValueViolation,
|
|
5732
|
+
protoKeyViolation,
|
|
3408
5733
|
quoteIdent,
|
|
5734
|
+
replayFieldVersionsForRecord,
|
|
3409
5735
|
replayOperationsForRecord,
|
|
5736
|
+
reservedWireShapeViolation,
|
|
3410
5737
|
resolveAtomicOp,
|
|
3411
5738
|
revealSecret,
|
|
3412
5739
|
rollbackStepsToSQL,
|
|
5740
|
+
serializeFoldState,
|
|
3413
5741
|
serializeVector,
|
|
3414
5742
|
t,
|
|
5743
|
+
timestampDomainViolation,
|
|
3415
5744
|
toAtomicOp,
|
|
5745
|
+
toMergeTrace,
|
|
3416
5746
|
transformSecretFieldsForWrite,
|
|
3417
5747
|
validateRecord,
|
|
3418
5748
|
validateTransition,
|
|
5749
|
+
valueDepthViolation,
|
|
3419
5750
|
vectorsEqual,
|
|
5751
|
+
verifyOperationId,
|
|
3420
5752
|
verifyOperationIntegrity,
|
|
3421
5753
|
verifySecret,
|
|
3422
5754
|
verifySecretValue
|