@korajs/store 0.6.0 → 1.0.0-beta.1
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/LICENSE +21 -0
- package/dist/adapters/better-sqlite3.cjs +94 -40
- package/dist/adapters/better-sqlite3.cjs.map +1 -1
- package/dist/adapters/better-sqlite3.d.cts +9 -1
- package/dist/adapters/better-sqlite3.d.ts +9 -1
- package/dist/adapters/better-sqlite3.js +62 -41
- package/dist/adapters/better-sqlite3.js.map +1 -1
- package/dist/adapters/indexeddb.cjs +24 -13
- package/dist/adapters/indexeddb.cjs.map +1 -1
- package/dist/adapters/indexeddb.d.cts +2 -2
- package/dist/adapters/indexeddb.d.ts +2 -2
- package/dist/adapters/indexeddb.js +4 -3
- package/dist/adapters/indexeddb.js.map +1 -1
- package/dist/adapters/sqlite-wasm.cjs +24 -13
- package/dist/adapters/sqlite-wasm.cjs.map +1 -1
- package/dist/adapters/sqlite-wasm.d.cts +27 -4
- package/dist/adapters/sqlite-wasm.d.ts +27 -4
- package/dist/adapters/sqlite-wasm.js +6 -4
- package/dist/{backup-JN3LB4YT.js → backup-ZV4Y424B.js} +3 -3
- package/dist/blob-fs.cjs +166 -0
- package/dist/blob-fs.cjs.map +1 -0
- package/dist/blob-fs.d.cts +29 -0
- package/dist/blob-fs.d.ts +29 -0
- package/dist/blob-fs.js +128 -0
- package/dist/blob-fs.js.map +1 -0
- package/dist/{chunk-56LL4XP6.js → chunk-6NKLDBTY.js} +52 -1
- package/dist/chunk-6NKLDBTY.js.map +1 -0
- package/dist/{chunk-AGN3CIPB.js → chunk-FFYA7J5H.js} +2 -2
- package/dist/{chunk-XO4LMPAH.js → chunk-IOEFNHHP.js} +2 -35
- package/dist/chunk-IOEFNHHP.js.map +1 -0
- package/dist/{chunk-LAWV6CFH.js → chunk-QL2XVZRN.js} +12 -1
- package/dist/chunk-QL2XVZRN.js.map +1 -0
- package/dist/{chunk-6AHYGQQT.js → chunk-RBPVEAVM.js} +4 -4
- package/dist/chunk-TVJEC5S2.js +40 -0
- package/dist/chunk-TVJEC5S2.js.map +1 -0
- package/dist/chunk-W4MTTNEZ.js +55 -0
- package/dist/chunk-W4MTTNEZ.js.map +1 -0
- package/dist/{chunk-EZSTD2XC.js → chunk-X57MWW35.js} +2 -2
- package/dist/{chunk-7BPETY5Y.js → chunk-ZLWAYQEQ.js} +128 -15
- package/dist/chunk-ZLWAYQEQ.js.map +1 -0
- package/dist/content-addressed-blob-store-Dcj3SB9r.d.cts +60 -0
- package/dist/content-addressed-blob-store-Dcj3SB9r.d.ts +60 -0
- package/dist/{export-audit-5GV6VRDZ.js → export-audit-AJ7RORNK.js} +3 -3
- package/dist/index.cjs +1245 -113
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +468 -7
- package/dist/index.d.ts +468 -7
- package/dist/index.js +975 -61
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs +123 -33
- package/dist/internal.cjs.map +1 -1
- package/dist/internal.d.cts +3 -3
- package/dist/internal.d.ts +3 -3
- package/dist/internal.js +3 -3
- package/dist/{sqlite-wasm-channel-I9y_jObg.d.cts → sqlite-wasm-channel-DUDPclTE.d.cts} +2 -15
- package/dist/{sqlite-wasm-channel-I9y_jObg.d.ts → sqlite-wasm-channel-DUDPclTE.d.ts} +2 -15
- package/dist/sqlite-wasm-channel-H43KEZLM.js +12 -0
- package/dist/{types-CTr00g_d.d.cts → types-5h4Hqcwc.d.ts} +10 -3
- package/dist/{types-BMyHmwvn.d.ts → types-DfPiPwKr.d.cts} +10 -3
- package/dist/{types-DepnRDk_.d.cts → types-k6AZgiH4.d.cts} +48 -2
- package/dist/{types-DepnRDk_.d.ts → types-k6AZgiH4.d.ts} +48 -2
- package/package.json +15 -2
- package/dist/chunk-56LL4XP6.js.map +0 -1
- package/dist/chunk-7BPETY5Y.js.map +0 -1
- package/dist/chunk-LAWV6CFH.js.map +0 -1
- package/dist/chunk-XO4LMPAH.js.map +0 -1
- package/dist/sqlite-wasm-channel-HDDW6UBH.js +0 -10
- /package/dist/{backup-JN3LB4YT.js.map → backup-ZV4Y424B.js.map} +0 -0
- /package/dist/{chunk-AGN3CIPB.js.map → chunk-FFYA7J5H.js.map} +0 -0
- /package/dist/{chunk-6AHYGQQT.js.map → chunk-RBPVEAVM.js.map} +0 -0
- /package/dist/{chunk-EZSTD2XC.js.map → chunk-X57MWW35.js.map} +0 -0
- /package/dist/{export-audit-5GV6VRDZ.js.map → export-audit-AJ7RORNK.js.map} +0 -0
- /package/dist/{sqlite-wasm-channel-HDDW6UBH.js.map → sqlite-wasm-channel-H43KEZLM.js.map} +0 -0
package/dist/index.cjs
CHANGED
|
@@ -30,6 +30,38 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
30
30
|
));
|
|
31
31
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
32
32
|
|
|
33
|
+
// src/serialization/op-data-encoding.ts
|
|
34
|
+
function encodeRichtextFieldsForOpData(data, fields) {
|
|
35
|
+
const result = { ...data };
|
|
36
|
+
for (const [key, value] of Object.entries(data)) {
|
|
37
|
+
if (fields[key]?.kind !== "richtext") {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (typeof value === "string" || value instanceof Uint8Array || value instanceof ArrayBuffer) {
|
|
41
|
+
result[key] = (0, import_core4.encodeBytesForOpData)(value);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
function decodeRichtextFieldsFromOpData(data, fields) {
|
|
47
|
+
const result = { ...data };
|
|
48
|
+
for (const [key, value] of Object.entries(data)) {
|
|
49
|
+
if (fields[key]?.kind !== "richtext" || value === null || value === void 0) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
result[key] = (0, import_core4.decodeBytesFromOpData)(value);
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
var import_core4, decodeRichtextOpDataValue;
|
|
57
|
+
var init_op_data_encoding = __esm({
|
|
58
|
+
"src/serialization/op-data-encoding.ts"() {
|
|
59
|
+
"use strict";
|
|
60
|
+
import_core4 = require("@korajs/core");
|
|
61
|
+
decodeRichtextOpDataValue = import_core4.decodeBytesFromOpData;
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
33
65
|
// src/serialization/richtext-serializer.ts
|
|
34
66
|
function encodeRichtext(value) {
|
|
35
67
|
if (value === null || value === void 0) {
|
|
@@ -46,6 +78,10 @@ function encodeRichtext(value) {
|
|
|
46
78
|
if (value instanceof ArrayBuffer) {
|
|
47
79
|
return new Uint8Array(value);
|
|
48
80
|
}
|
|
81
|
+
if ((0, import_core4.isKoraBytesValue)(value) || (0, import_core4.isLegacyNumericByteObject)(value)) {
|
|
82
|
+
const decoded = decodeRichtextOpDataValue(value);
|
|
83
|
+
return decoded instanceof Uint8Array ? decoded : null;
|
|
84
|
+
}
|
|
49
85
|
throw new Error("Richtext value must be a string, Uint8Array, ArrayBuffer, null, or undefined.");
|
|
50
86
|
}
|
|
51
87
|
function decodeRichtext(value) {
|
|
@@ -84,6 +120,7 @@ var init_richtext_serializer = __esm({
|
|
|
84
120
|
"src/serialization/richtext-serializer.ts"() {
|
|
85
121
|
"use strict";
|
|
86
122
|
Y = __toESM(require("yjs"), 1);
|
|
123
|
+
init_op_data_encoding();
|
|
87
124
|
TEXT_KEY = "content";
|
|
88
125
|
}
|
|
89
126
|
});
|
|
@@ -147,7 +184,7 @@ function serializeOperation(op) {
|
|
|
147
184
|
record_id: op.recordId,
|
|
148
185
|
data: dataPayload ? JSON.stringify(dataPayload) : null,
|
|
149
186
|
previous_data: op.previousData ? JSON.stringify(op.previousData) : null,
|
|
150
|
-
timestamp:
|
|
187
|
+
timestamp: import_core5.HybridLogicalClock.serialize(op.timestamp),
|
|
151
188
|
sequence_number: op.sequenceNumber,
|
|
152
189
|
causal_deps: JSON.stringify(op.causalDeps),
|
|
153
190
|
schema_version: op.schemaVersion
|
|
@@ -181,7 +218,7 @@ function deserializeOperation(row) {
|
|
|
181
218
|
recordId: row.record_id,
|
|
182
219
|
data,
|
|
183
220
|
previousData: row.previous_data ? JSON.parse(row.previous_data) : null,
|
|
184
|
-
timestamp:
|
|
221
|
+
timestamp: import_core5.HybridLogicalClock.deserialize(row.timestamp),
|
|
185
222
|
sequenceNumber: row.sequence_number,
|
|
186
223
|
causalDeps: JSON.parse(row.causal_deps),
|
|
187
224
|
schemaVersion: row.schema_version,
|
|
@@ -202,6 +239,9 @@ function serializeValue(value, descriptor) {
|
|
|
202
239
|
case "boolean":
|
|
203
240
|
return value ? 1 : 0;
|
|
204
241
|
case "array":
|
|
242
|
+
case "object":
|
|
243
|
+
case "json":
|
|
244
|
+
case "blob":
|
|
205
245
|
return JSON.stringify(value);
|
|
206
246
|
case "richtext":
|
|
207
247
|
return encodeRichtext(value);
|
|
@@ -218,17 +258,24 @@ function deserializeValue(value, descriptor) {
|
|
|
218
258
|
return JSON.parse(value);
|
|
219
259
|
}
|
|
220
260
|
return value;
|
|
261
|
+
case "object":
|
|
262
|
+
case "json":
|
|
263
|
+
case "blob":
|
|
264
|
+
if (typeof value === "string") {
|
|
265
|
+
return JSON.parse(value);
|
|
266
|
+
}
|
|
267
|
+
return value;
|
|
221
268
|
case "richtext":
|
|
222
269
|
return decodeRichtext(value);
|
|
223
270
|
default:
|
|
224
271
|
return value;
|
|
225
272
|
}
|
|
226
273
|
}
|
|
227
|
-
var
|
|
274
|
+
var import_core5, ATOMIC_OPS_KEY, TX_ID_KEY, MUTATION_NAME_KEY;
|
|
228
275
|
var init_serializer = __esm({
|
|
229
276
|
"src/serialization/serializer.ts"() {
|
|
230
277
|
"use strict";
|
|
231
|
-
|
|
278
|
+
import_core5 = require("@korajs/core");
|
|
232
279
|
init_richtext_serializer();
|
|
233
280
|
ATOMIC_OPS_KEY = "__kora_atomic_ops__";
|
|
234
281
|
TX_ID_KEY = "__kora_tx_id__";
|
|
@@ -609,11 +656,11 @@ async function computeSha256(data) {
|
|
|
609
656
|
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
610
657
|
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
611
658
|
}
|
|
612
|
-
var
|
|
659
|
+
var import_core6, BACKUP_VERSION;
|
|
613
660
|
var init_backup = __esm({
|
|
614
661
|
"src/backup/backup.ts"() {
|
|
615
662
|
"use strict";
|
|
616
|
-
|
|
663
|
+
import_core6 = require("@korajs/core");
|
|
617
664
|
init_serializer();
|
|
618
665
|
BACKUP_VERSION = 1;
|
|
619
666
|
}
|
|
@@ -670,7 +717,7 @@ __export(audit_trace_store_exports, {
|
|
|
670
717
|
});
|
|
671
718
|
function persistedAuditTraceFromEvent(event) {
|
|
672
719
|
return {
|
|
673
|
-
id: (0,
|
|
720
|
+
id: (0, import_core18.generateUUIDv7)(),
|
|
674
721
|
recordedAt: Date.now(),
|
|
675
722
|
eventType: event.type,
|
|
676
723
|
constraint: event.type === "constraint:violated" ? event.constraint : void 0,
|
|
@@ -736,11 +783,11 @@ function auditTraceIndexFields(trace) {
|
|
|
736
783
|
recordId: trace.operationA.recordId
|
|
737
784
|
};
|
|
738
785
|
}
|
|
739
|
-
var
|
|
786
|
+
var import_core18;
|
|
740
787
|
var init_audit_trace_store = __esm({
|
|
741
788
|
"src/audit/audit-trace-store.ts"() {
|
|
742
789
|
"use strict";
|
|
743
|
-
|
|
790
|
+
import_core18 = require("@korajs/core");
|
|
744
791
|
init_audit_json();
|
|
745
792
|
}
|
|
746
793
|
});
|
|
@@ -936,10 +983,15 @@ var init_export_audit = __esm({
|
|
|
936
983
|
var index_exports = {};
|
|
937
984
|
__export(index_exports, {
|
|
938
985
|
AdapterError: () => AdapterError,
|
|
986
|
+
BlobIntegrityError: () => BlobIntegrityError,
|
|
939
987
|
COMPACTION_BASELINE_META_KEY: () => COMPACTION_BASELINE_META_KEY,
|
|
940
988
|
Collection: () => Collection,
|
|
989
|
+
DEFAULT_CHUNK_SIZE: () => DEFAULT_CHUNK_SIZE,
|
|
941
990
|
InvalidStateTransitionError: () => InvalidStateTransitionError,
|
|
942
991
|
LAST_ACKED_SERVER_VECTOR_META_KEY: () => LAST_ACKED_SERVER_VECTOR_META_KEY,
|
|
992
|
+
MemoryBlobStore: () => MemoryBlobStore,
|
|
993
|
+
OpfsBlobStore: () => OpfsBlobStore,
|
|
994
|
+
OptimisticLockError: () => OptimisticLockError,
|
|
943
995
|
PersistenceError: () => PersistenceError,
|
|
944
996
|
QueryBuilder: () => QueryBuilder,
|
|
945
997
|
QueryError: () => QueryError,
|
|
@@ -956,26 +1008,44 @@ __export(index_exports, {
|
|
|
956
1008
|
WorkerTimeoutError: () => WorkerTimeoutError,
|
|
957
1009
|
asRichTextSyncEngine: () => asRichTextSyncEngine,
|
|
958
1010
|
assertQueryReady: () => assertQueryReady,
|
|
1011
|
+
chunkBlob: () => chunkBlob,
|
|
1012
|
+
collectBlobGarbage: () => collectBlobGarbage,
|
|
959
1013
|
collectOperationsAheadOfServer: () => collectOperationsAheadOfServer,
|
|
960
1014
|
compactOperationLog: () => compactOperationLog,
|
|
961
1015
|
computeAckCompactionWatermark: () => computeAckCompactionWatermark,
|
|
1016
|
+
createChunkPortPair: () => createChunkPortPair,
|
|
1017
|
+
createMemoryServerBlobStore: () => createMemoryServerBlobStore,
|
|
1018
|
+
createOpfsBlobDirectory: () => createOpfsBlobDirectory,
|
|
1019
|
+
createOpfsBlobStore: () => createOpfsBlobStore,
|
|
1020
|
+
createRemoteChunkProvider: () => createRemoteChunkProvider,
|
|
962
1021
|
createRichTextController: () => createRichTextController,
|
|
963
1022
|
decodeAuditExport: () => decodeAuditExport,
|
|
964
1023
|
decodeRichtext: () => decodeRichtext,
|
|
965
1024
|
deserializeVersionVectorFromMeta: () => deserializeVersionVectorFromMeta,
|
|
966
1025
|
encodeRichtext: () => encodeRichtext,
|
|
967
1026
|
exportBackup: () => exportBackup,
|
|
1027
|
+
extractBlobRefs: () => extractBlobRefs,
|
|
1028
|
+
fetchBlobManifest: () => fetchBlobManifest,
|
|
968
1029
|
getSharedQueryStoreCache: () => getSharedQueryStoreCache,
|
|
969
1030
|
mergeVersionVectors: () => mergeVersionVectors,
|
|
1031
|
+
parseBlobManifest: () => parseBlobManifest,
|
|
970
1032
|
persistedAuditTraceFromEvent: () => persistedAuditTraceFromEvent,
|
|
971
1033
|
pluralize: () => pluralize,
|
|
1034
|
+
prepareBlobForSend: () => prepareBlobForSend,
|
|
1035
|
+
putBlobForTransfer: () => putBlobForTransfer,
|
|
972
1036
|
readAuditExportManifest: () => readAuditExportManifest,
|
|
973
1037
|
readBackupManifest: () => readBackupManifest,
|
|
1038
|
+
reassembleBlob: () => reassembleBlob,
|
|
1039
|
+
receiveBlob: () => receiveBlob,
|
|
1040
|
+
resolveBlobManifest: () => resolveBlobManifest,
|
|
974
1041
|
restoreBackup: () => restoreBackup,
|
|
975
1042
|
richtextStatesEqual: () => richtextStatesEqual,
|
|
976
1043
|
richtextToPlainText: () => richtextToPlainText,
|
|
1044
|
+
serializeBlobManifest: () => serializeBlobManifest,
|
|
977
1045
|
serializeVersionVectorToMeta: () => serializeVersionVectorToMeta,
|
|
1046
|
+
serveBlobChunks: () => serveBlobChunks,
|
|
978
1047
|
singularize: () => singularize,
|
|
1048
|
+
toServerBlobCallbacks: () => toServerBlobCallbacks,
|
|
979
1049
|
validateStateTransition: () => validateStateTransition,
|
|
980
1050
|
validateUpdateStateMachine: () => validateUpdateStateMachine,
|
|
981
1051
|
verifyAuditExportChecksum: () => verifyAuditExportChecksum,
|
|
@@ -1000,6 +1070,16 @@ var RecordNotFoundError = class extends import_core.KoraError {
|
|
|
1000
1070
|
this.name = "RecordNotFoundError";
|
|
1001
1071
|
}
|
|
1002
1072
|
};
|
|
1073
|
+
var OptimisticLockError = class extends import_core.KoraError {
|
|
1074
|
+
constructor(collection, recordId) {
|
|
1075
|
+
super(
|
|
1076
|
+
`Row version changed while merging record "${recordId}" in "${collection}"; retry with fresh state`,
|
|
1077
|
+
"OPTIMISTIC_LOCK",
|
|
1078
|
+
{ collection, recordId }
|
|
1079
|
+
);
|
|
1080
|
+
this.name = "OptimisticLockError";
|
|
1081
|
+
}
|
|
1082
|
+
};
|
|
1003
1083
|
var AdapterError = class extends import_core.KoraError {
|
|
1004
1084
|
constructor(message, context) {
|
|
1005
1085
|
super(message, "ADAPTER_ERROR", context);
|
|
@@ -1176,27 +1256,27 @@ async function saveDeltaCursor(adapter, cursor) {
|
|
|
1176
1256
|
}
|
|
1177
1257
|
|
|
1178
1258
|
// src/store/store.ts
|
|
1179
|
-
var
|
|
1259
|
+
var import_core19 = require("@korajs/core");
|
|
1180
1260
|
init_backup();
|
|
1181
1261
|
|
|
1182
1262
|
// src/mutations/execute-delete.ts
|
|
1183
|
-
var
|
|
1263
|
+
var import_core8 = require("@korajs/core");
|
|
1184
1264
|
|
|
1185
1265
|
// src/lww/row-version.ts
|
|
1186
|
-
var
|
|
1266
|
+
var import_core7 = require("@korajs/core");
|
|
1187
1267
|
var EMPTY_ROW_VERSION = "";
|
|
1188
1268
|
function serializeRowVersion(timestamp) {
|
|
1189
|
-
return
|
|
1269
|
+
return import_core7.HybridLogicalClock.serialize(timestamp);
|
|
1190
1270
|
}
|
|
1191
1271
|
function rowVersionFromRecord(row) {
|
|
1192
1272
|
const version = row._version;
|
|
1193
1273
|
if (typeof version === "string" && version.length > 0) {
|
|
1194
|
-
return
|
|
1274
|
+
return import_core7.HybridLogicalClock.deserialize(version);
|
|
1195
1275
|
}
|
|
1196
1276
|
return { wallTime: row._updated_at, logical: 0, nodeId: "" };
|
|
1197
1277
|
}
|
|
1198
1278
|
function isIncomingNewerThanRow(incoming, row) {
|
|
1199
|
-
return
|
|
1279
|
+
return import_core7.HybridLogicalClock.compare(incoming, rowVersionFromRecord(row)) > 0;
|
|
1200
1280
|
}
|
|
1201
1281
|
function lwwVersionWhereClause(remoteVersion) {
|
|
1202
1282
|
return {
|
|
@@ -1256,6 +1336,28 @@ function buildUpdateQuery(collection, id, changes) {
|
|
|
1256
1336
|
const sql = `UPDATE ${collection} SET ${setClauses.join(", ")} WHERE id = ?`;
|
|
1257
1337
|
return { sql, params };
|
|
1258
1338
|
}
|
|
1339
|
+
function buildFieldFastForwardUpdateQuery(collection, id, fieldChanges, remoteVersion, wallTime, options) {
|
|
1340
|
+
const fieldClauses = Object.keys(fieldChanges).map((col) => `${col} = ?`);
|
|
1341
|
+
const setClauses = [
|
|
1342
|
+
...fieldClauses,
|
|
1343
|
+
"_version = CASE WHEN _version >= ? THEN _version ELSE ? END",
|
|
1344
|
+
"_updated_at = CASE WHEN _updated_at >= ? THEN _updated_at ELSE ? END"
|
|
1345
|
+
];
|
|
1346
|
+
const params = [
|
|
1347
|
+
...Object.values(fieldChanges),
|
|
1348
|
+
remoteVersion,
|
|
1349
|
+
remoteVersion,
|
|
1350
|
+
wallTime,
|
|
1351
|
+
wallTime
|
|
1352
|
+
];
|
|
1353
|
+
if (options?.maxCreatedAt !== void 0) {
|
|
1354
|
+
setClauses.push("_created_at = CASE WHEN _created_at >= ? THEN _created_at ELSE ? END");
|
|
1355
|
+
params.push(options.maxCreatedAt, options.maxCreatedAt);
|
|
1356
|
+
}
|
|
1357
|
+
params.push(id);
|
|
1358
|
+
const sql = `UPDATE ${collection} SET ${setClauses.join(", ")} WHERE id = ?`;
|
|
1359
|
+
return { sql, params };
|
|
1360
|
+
}
|
|
1259
1361
|
function buildSoftDeleteQuery(collection, id, updatedAt, version) {
|
|
1260
1362
|
if (version !== void 0) {
|
|
1261
1363
|
return {
|
|
@@ -1268,13 +1370,6 @@ function buildSoftDeleteQuery(collection, id, updatedAt, version) {
|
|
|
1268
1370
|
params: [updatedAt, id]
|
|
1269
1371
|
};
|
|
1270
1372
|
}
|
|
1271
|
-
function buildLwwUpdateQuery(collection, id, changes, remoteVersion) {
|
|
1272
|
-
const setClauses = Object.keys(changes).map((col) => `${col} = ?`);
|
|
1273
|
-
const lww = lwwVersionWhereClause(remoteVersion);
|
|
1274
|
-
const sql = `UPDATE ${collection} SET ${setClauses.join(", ")} WHERE id = ? AND ${lww.sql}`;
|
|
1275
|
-
const params = [...Object.values(changes), id, ...lww.params];
|
|
1276
|
-
return { sql, params };
|
|
1277
|
-
}
|
|
1278
1373
|
function buildLwwSoftDeleteQuery(collection, id, updatedAt, version) {
|
|
1279
1374
|
const lww = lwwVersionWhereClause(version);
|
|
1280
1375
|
return {
|
|
@@ -1439,7 +1534,7 @@ async function executeDelete(ctx, id, options) {
|
|
|
1439
1534
|
await ctx.adapter.transaction(async (tx) => {
|
|
1440
1535
|
if (!operation) {
|
|
1441
1536
|
const sequenceNumber = await allocateNextSequenceInTransaction(tx, ctx.nodeId);
|
|
1442
|
-
operation = await (0,
|
|
1537
|
+
operation = await (0, import_core8.createOperation)(
|
|
1443
1538
|
{
|
|
1444
1539
|
nodeId: ctx.nodeId,
|
|
1445
1540
|
type: "delete",
|
|
@@ -1486,28 +1581,90 @@ async function executeDelete(ctx, id, options) {
|
|
|
1486
1581
|
}
|
|
1487
1582
|
|
|
1488
1583
|
// src/mutations/execute-insert.ts
|
|
1489
|
-
var
|
|
1584
|
+
var import_core9 = require("@korajs/core");
|
|
1585
|
+
|
|
1586
|
+
// src/lww/field-versions.ts
|
|
1587
|
+
function parseFieldVersions(raw) {
|
|
1588
|
+
if (typeof raw !== "string" || raw.length === 0) {
|
|
1589
|
+
return {};
|
|
1590
|
+
}
|
|
1591
|
+
try {
|
|
1592
|
+
const parsed = JSON.parse(raw);
|
|
1593
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1594
|
+
return {};
|
|
1595
|
+
}
|
|
1596
|
+
const result = {};
|
|
1597
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
1598
|
+
if (typeof value === "string") {
|
|
1599
|
+
result[key] = value;
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
return result;
|
|
1603
|
+
} catch {
|
|
1604
|
+
return {};
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
function serializeFieldVersions(versions) {
|
|
1608
|
+
return JSON.stringify(versions);
|
|
1609
|
+
}
|
|
1610
|
+
function fieldVersionsForFields(fieldNames, version) {
|
|
1611
|
+
const versions = {};
|
|
1612
|
+
for (const field of fieldNames) {
|
|
1613
|
+
versions[field] = version;
|
|
1614
|
+
}
|
|
1615
|
+
return versions;
|
|
1616
|
+
}
|
|
1617
|
+
function effectiveFieldVersion(current, field, rowVersion) {
|
|
1618
|
+
const own = current[field];
|
|
1619
|
+
if (own !== void 0) {
|
|
1620
|
+
return own;
|
|
1621
|
+
}
|
|
1622
|
+
return rowVersion ?? EMPTY_ROW_VERSION;
|
|
1623
|
+
}
|
|
1624
|
+
function resolvePerFieldLww(current, incomingFields, incomingVersion, rowVersion) {
|
|
1625
|
+
const winners = [];
|
|
1626
|
+
const merged = { ...current };
|
|
1627
|
+
for (const field of incomingFields) {
|
|
1628
|
+
const stored = effectiveFieldVersion(current, field, rowVersion);
|
|
1629
|
+
if (incomingVersion > stored) {
|
|
1630
|
+
winners.push(field);
|
|
1631
|
+
merged[field] = incomingVersion;
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
return { winners, merged };
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
// src/mutations/execute-insert.ts
|
|
1638
|
+
init_op_data_encoding();
|
|
1490
1639
|
init_serializer();
|
|
1491
1640
|
async function executeInsert(ctx, data) {
|
|
1492
|
-
const validated = (0,
|
|
1493
|
-
const recordId = (0,
|
|
1641
|
+
const validated = (0, import_core9.validateRecord)(ctx.collection, ctx.definition, data, "insert");
|
|
1642
|
+
const recordId = (0, import_core9.generateUUIDv7)();
|
|
1494
1643
|
for (const [fieldName, descriptor] of Object.entries(ctx.definition.fields)) {
|
|
1495
1644
|
if (descriptor.auto && descriptor.kind === "timestamp") {
|
|
1496
1645
|
validated[fieldName] = Date.now();
|
|
1497
1646
|
}
|
|
1498
1647
|
}
|
|
1648
|
+
const writeData = await (0, import_core9.transformSecretFieldsForWrite)(
|
|
1649
|
+
validated,
|
|
1650
|
+
ctx.definition,
|
|
1651
|
+
ctx.secretKeyProvider
|
|
1652
|
+
);
|
|
1499
1653
|
const causalDeps = resolveCausalDeps(ctx);
|
|
1500
1654
|
let operation;
|
|
1501
1655
|
let record;
|
|
1502
1656
|
await ctx.adapter.transaction(async (tx) => {
|
|
1503
1657
|
const sequenceNumber = await allocateNextSequenceInTransaction(tx, ctx.nodeId);
|
|
1504
|
-
operation = await (0,
|
|
1658
|
+
operation = await (0, import_core9.createOperation)(
|
|
1505
1659
|
{
|
|
1506
1660
|
nodeId: ctx.nodeId,
|
|
1507
1661
|
type: "insert",
|
|
1508
1662
|
collection: ctx.collection,
|
|
1509
1663
|
recordId,
|
|
1510
|
-
|
|
1664
|
+
// Binary richtext values are tagged as canonical JSON BEFORE the
|
|
1665
|
+
// operation is content-hashed, so the hash input, persisted JSON,
|
|
1666
|
+
// and wire payload are the identical value.
|
|
1667
|
+
data: encodeRichtextFieldsForOpData(writeData, ctx.definition.fields),
|
|
1511
1668
|
previousData: null,
|
|
1512
1669
|
sequenceNumber,
|
|
1513
1670
|
causalDeps,
|
|
@@ -1516,14 +1673,19 @@ async function executeInsert(ctx, data) {
|
|
|
1516
1673
|
ctx.clock
|
|
1517
1674
|
);
|
|
1518
1675
|
ctx.causalTracker?.afterOperation(ctx.collection, operation.id, ctx.inTransaction);
|
|
1519
|
-
const serializedData = serializeRecord(
|
|
1676
|
+
const serializedData = serializeRecord(writeData, ctx.definition.fields);
|
|
1520
1677
|
const version = serializeRowVersion(operation.timestamp);
|
|
1521
1678
|
record = {
|
|
1522
1679
|
id: recordId,
|
|
1523
1680
|
...serializedData,
|
|
1524
1681
|
_created_at: operation.timestamp.wallTime,
|
|
1525
1682
|
_updated_at: operation.timestamp.wallTime,
|
|
1526
|
-
_version: version
|
|
1683
|
+
_version: version,
|
|
1684
|
+
// Stamp every inserted field with this operation's version so later
|
|
1685
|
+
// per-field LWW compares against a real writer, not the fallback.
|
|
1686
|
+
_field_versions: serializeFieldVersions(
|
|
1687
|
+
fieldVersionsForFields(Object.keys(serializedData), version)
|
|
1688
|
+
)
|
|
1527
1689
|
};
|
|
1528
1690
|
const builtInsert = buildInsertQuery(ctx.collection, record);
|
|
1529
1691
|
const opInsert = buildInsertQuery(
|
|
@@ -1536,19 +1698,20 @@ async function executeInsert(ctx, data) {
|
|
|
1536
1698
|
ctx.onMutation(ctx.collection, operation);
|
|
1537
1699
|
return {
|
|
1538
1700
|
id: recordId,
|
|
1539
|
-
...
|
|
1701
|
+
...writeData,
|
|
1540
1702
|
createdAt: operation.timestamp.wallTime,
|
|
1541
1703
|
updatedAt: operation.timestamp.wallTime
|
|
1542
1704
|
};
|
|
1543
1705
|
}
|
|
1544
1706
|
|
|
1545
1707
|
// src/mutations/execute-update.ts
|
|
1546
|
-
var
|
|
1708
|
+
var import_core11 = require("@korajs/core");
|
|
1709
|
+
init_op_data_encoding();
|
|
1547
1710
|
init_serializer();
|
|
1548
1711
|
|
|
1549
1712
|
// src/state-machine/state-validator.ts
|
|
1550
|
-
var
|
|
1551
|
-
var InvalidStateTransitionError = class extends
|
|
1713
|
+
var import_core10 = require("@korajs/core");
|
|
1714
|
+
var InvalidStateTransitionError = class extends import_core10.KoraError {
|
|
1552
1715
|
constructor(collection, recordId, field, fromState, toState, allowedStates) {
|
|
1553
1716
|
super(
|
|
1554
1717
|
`Invalid state transition in collection "${collection}": cannot transition field "${field}" from "${fromState}" to "${toState}". Allowed transitions from "${fromState}": ${allowedStates.length > 0 ? allowedStates.join(", ") : "(none -- terminal state)"}`,
|
|
@@ -1582,7 +1745,7 @@ function validateStateTransition(collectionName, recordId, stateMachine, current
|
|
|
1582
1745
|
collection: collectionName,
|
|
1583
1746
|
transitions: stateMachine.transitions
|
|
1584
1747
|
};
|
|
1585
|
-
const transitionResult = (0,
|
|
1748
|
+
const transitionResult = (0, import_core10.validateTransition)(constraint, currentState, newState);
|
|
1586
1749
|
if (transitionResult.valid) {
|
|
1587
1750
|
return { valid: true, allowedStates: transitionResult.allowedTargets };
|
|
1588
1751
|
}
|
|
@@ -1638,7 +1801,7 @@ async function executeUpdate(ctx, id, data) {
|
|
|
1638
1801
|
if (!currentRow) {
|
|
1639
1802
|
throw new RecordNotFoundError(ctx.collection, id);
|
|
1640
1803
|
}
|
|
1641
|
-
let validated = (0,
|
|
1804
|
+
let validated = (0, import_core11.validateRecord)(ctx.collection, ctx.definition, data, "update");
|
|
1642
1805
|
const currentRecord = deserializeRecord(currentRow, ctx.definition.fields);
|
|
1643
1806
|
validated = validateUpdateStateMachine(
|
|
1644
1807
|
ctx.collection,
|
|
@@ -1656,26 +1819,34 @@ async function executeUpdate(ctx, id, data) {
|
|
|
1656
1819
|
for (const key of Object.keys(validated)) {
|
|
1657
1820
|
const value = validated[key];
|
|
1658
1821
|
previousData[key] = currentRecord[key];
|
|
1659
|
-
if ((0,
|
|
1660
|
-
resolvedData[key] = (0,
|
|
1661
|
-
atomicOps[key] = (0,
|
|
1822
|
+
if ((0, import_core11.isAtomicOp)(value)) {
|
|
1823
|
+
resolvedData[key] = (0, import_core11.resolveAtomicOp)(currentRecord[key], value);
|
|
1824
|
+
atomicOps[key] = (0, import_core11.toAtomicOp)(value);
|
|
1662
1825
|
} else {
|
|
1663
1826
|
resolvedData[key] = value;
|
|
1664
1827
|
}
|
|
1665
1828
|
}
|
|
1829
|
+
const writeData = await (0, import_core11.transformSecretFieldsForWrite)(
|
|
1830
|
+
resolvedData,
|
|
1831
|
+
ctx.definition,
|
|
1832
|
+
ctx.secretKeyProvider
|
|
1833
|
+
);
|
|
1666
1834
|
const hasAtomicOps = Object.keys(atomicOps).length > 0;
|
|
1667
1835
|
const causalDeps = resolveCausalDeps(ctx);
|
|
1668
1836
|
let operation;
|
|
1669
1837
|
await ctx.adapter.transaction(async (tx) => {
|
|
1670
1838
|
const sequenceNumber = await allocateNextSequenceInTransaction(tx, ctx.nodeId);
|
|
1671
|
-
operation = await (0,
|
|
1839
|
+
operation = await (0, import_core11.createOperation)(
|
|
1672
1840
|
{
|
|
1673
1841
|
nodeId: ctx.nodeId,
|
|
1674
1842
|
type: "update",
|
|
1675
1843
|
collection: ctx.collection,
|
|
1676
1844
|
recordId: id,
|
|
1677
|
-
|
|
1678
|
-
|
|
1845
|
+
// Binary richtext values (new and previous) are tagged as canonical
|
|
1846
|
+
// JSON BEFORE the operation is content-hashed, so the hash input,
|
|
1847
|
+
// persisted JSON, and wire payload are the identical value.
|
|
1848
|
+
data: encodeRichtextFieldsForOpData(writeData, ctx.definition.fields),
|
|
1849
|
+
previousData: encodeRichtextFieldsForOpData(previousData, ctx.definition.fields),
|
|
1679
1850
|
sequenceNumber,
|
|
1680
1851
|
causalDeps,
|
|
1681
1852
|
schemaVersion: ctx.schema.version,
|
|
@@ -1684,12 +1855,17 @@ async function executeUpdate(ctx, id, data) {
|
|
|
1684
1855
|
ctx.clock
|
|
1685
1856
|
);
|
|
1686
1857
|
ctx.causalTracker?.afterOperation(ctx.collection, operation.id, ctx.inTransaction);
|
|
1687
|
-
const serializedChanges = serializeRecord(
|
|
1858
|
+
const serializedChanges = serializeRecord(writeData, ctx.definition.fields);
|
|
1688
1859
|
const version = serializeRowVersion(operation.timestamp);
|
|
1860
|
+
const mergedFieldVersions = parseFieldVersions(currentRow._field_versions);
|
|
1861
|
+
for (const changedField of Object.keys(serializedChanges)) {
|
|
1862
|
+
mergedFieldVersions[changedField] = version;
|
|
1863
|
+
}
|
|
1689
1864
|
const updateQuery = buildUpdateQuery(ctx.collection, id, {
|
|
1690
1865
|
...serializedChanges,
|
|
1691
1866
|
_updated_at: operation.timestamp.wallTime,
|
|
1692
|
-
_version: version
|
|
1867
|
+
_version: version,
|
|
1868
|
+
_field_versions: serializeFieldVersions(mergedFieldVersions)
|
|
1693
1869
|
});
|
|
1694
1870
|
const opInsert = buildInsertQuery(
|
|
1695
1871
|
`_kora_ops_${ctx.collection}`,
|
|
@@ -1713,7 +1889,7 @@ async function executeUpdate(ctx, id, data) {
|
|
|
1713
1889
|
// src/collection/collection.ts
|
|
1714
1890
|
init_serializer();
|
|
1715
1891
|
var Collection = class {
|
|
1716
|
-
constructor(name, definition, schema, adapter, clock, nodeId, allocateSequenceNumber, onMutation, relationEnforcer, mutationHandler, causalTracker) {
|
|
1892
|
+
constructor(name, definition, schema, adapter, clock, nodeId, allocateSequenceNumber, onMutation, relationEnforcer, mutationHandler, causalTracker, secretKeyProvider) {
|
|
1717
1893
|
this.name = name;
|
|
1718
1894
|
this.definition = definition;
|
|
1719
1895
|
this.schema = schema;
|
|
@@ -1725,6 +1901,7 @@ var Collection = class {
|
|
|
1725
1901
|
this.relationEnforcer = relationEnforcer;
|
|
1726
1902
|
this.mutationHandler = mutationHandler;
|
|
1727
1903
|
this.causalTracker = causalTracker;
|
|
1904
|
+
this.secretKeyProvider = secretKeyProvider;
|
|
1728
1905
|
}
|
|
1729
1906
|
name;
|
|
1730
1907
|
definition;
|
|
@@ -1737,6 +1914,7 @@ var Collection = class {
|
|
|
1737
1914
|
relationEnforcer;
|
|
1738
1915
|
mutationHandler;
|
|
1739
1916
|
causalTracker;
|
|
1917
|
+
secretKeyProvider;
|
|
1740
1918
|
mutationContext() {
|
|
1741
1919
|
return {
|
|
1742
1920
|
collection: this.name,
|
|
@@ -1749,7 +1927,8 @@ var Collection = class {
|
|
|
1749
1927
|
onMutation: this.onMutation,
|
|
1750
1928
|
relationEnforcer: this.relationEnforcer,
|
|
1751
1929
|
causalTracker: this.causalTracker,
|
|
1752
|
-
inTransaction: false
|
|
1930
|
+
inTransaction: false,
|
|
1931
|
+
secretKeyProvider: this.secretKeyProvider
|
|
1753
1932
|
};
|
|
1754
1933
|
}
|
|
1755
1934
|
async insert(data) {
|
|
@@ -2089,7 +2268,7 @@ var QueryError2 = class extends Error {
|
|
|
2089
2268
|
};
|
|
2090
2269
|
|
|
2091
2270
|
// src/relations/relation-enforcer.ts
|
|
2092
|
-
var
|
|
2271
|
+
var import_core12 = require("@korajs/core");
|
|
2093
2272
|
init_serializer();
|
|
2094
2273
|
|
|
2095
2274
|
// src/relations/relation-lookup.ts
|
|
@@ -2114,7 +2293,7 @@ function getIncomingRelations(lookup, collection) {
|
|
|
2114
2293
|
}
|
|
2115
2294
|
|
|
2116
2295
|
// src/relations/relation-enforcer.ts
|
|
2117
|
-
var ReferentialIntegrityError = class extends
|
|
2296
|
+
var ReferentialIntegrityError = class extends import_core12.KoraError {
|
|
2118
2297
|
constructor(collection, recordId, referencingCollection, relationName, referencingCount) {
|
|
2119
2298
|
super(
|
|
2120
2299
|
`Cannot delete record "${recordId}" from "${collection}": ${referencingCount} record(s) in "${referencingCollection}" reference it via relation "${relationName}" with onDelete: 'restrict'. Delete or reassign the referencing records first.`,
|
|
@@ -2203,7 +2382,7 @@ var RelationEnforcer = class {
|
|
|
2203
2382
|
for (const row of referencingRows) {
|
|
2204
2383
|
const now = Date.now();
|
|
2205
2384
|
const sequenceNumber = await allocateNextSequenceInTransaction(tx, this.nodeId);
|
|
2206
|
-
const operation = await (0,
|
|
2385
|
+
const operation = await (0, import_core12.createOperation)(
|
|
2207
2386
|
{
|
|
2208
2387
|
nodeId: this.nodeId,
|
|
2209
2388
|
type: "delete",
|
|
@@ -2252,7 +2431,7 @@ var RelationEnforcer = class {
|
|
|
2252
2431
|
const sequenceNumber = await allocateNextSequenceInTransaction(tx, this.nodeId);
|
|
2253
2432
|
const updateData = { [foreignKeyField]: null };
|
|
2254
2433
|
const previousData = { [foreignKeyField]: deletedRecordId };
|
|
2255
|
-
const operation = await (0,
|
|
2434
|
+
const operation = await (0, import_core12.createOperation)(
|
|
2256
2435
|
{
|
|
2257
2436
|
nodeId: this.nodeId,
|
|
2258
2437
|
type: "update",
|
|
@@ -2313,8 +2492,9 @@ var RelationEnforcer = class {
|
|
|
2313
2492
|
};
|
|
2314
2493
|
|
|
2315
2494
|
// src/replay/replay-to.ts
|
|
2316
|
-
var
|
|
2495
|
+
var import_core13 = require("@korajs/core");
|
|
2317
2496
|
var import_internal = require("@korajs/core/internal");
|
|
2497
|
+
init_op_data_encoding();
|
|
2318
2498
|
function collectCausalClosure(allOps, targetOperationId) {
|
|
2319
2499
|
const opMap = /* @__PURE__ */ new Map();
|
|
2320
2500
|
for (const op of allOps) {
|
|
@@ -2322,7 +2502,7 @@ function collectCausalClosure(allOps, targetOperationId) {
|
|
|
2322
2502
|
}
|
|
2323
2503
|
const target = opMap.get(targetOperationId);
|
|
2324
2504
|
if (!target) {
|
|
2325
|
-
throw new
|
|
2505
|
+
throw new import_core13.OperationError(
|
|
2326
2506
|
`Operation "${targetOperationId}" not found in the local operation log`,
|
|
2327
2507
|
{
|
|
2328
2508
|
operationId: targetOperationId
|
|
@@ -2354,7 +2534,7 @@ function buildReplaySnapshot(schema, allOps, targetOperationId) {
|
|
|
2354
2534
|
const operationsApplied = collectCausalClosure(allOps, targetOperationId);
|
|
2355
2535
|
const targetOperation = operationsApplied.find((op) => op.id === targetOperationId);
|
|
2356
2536
|
if (!targetOperation) {
|
|
2357
|
-
throw new
|
|
2537
|
+
throw new import_core13.OperationError(`Operation "${targetOperationId}" not found after causal sort`, {
|
|
2358
2538
|
operationId: targetOperationId
|
|
2359
2539
|
});
|
|
2360
2540
|
}
|
|
@@ -2399,7 +2579,9 @@ function applyOperationToMemory(state, op, schema) {
|
|
|
2399
2579
|
}
|
|
2400
2580
|
colMap.set(op.recordId, {
|
|
2401
2581
|
id: op.recordId,
|
|
2402
|
-
|
|
2582
|
+
// op.data stores binary richtext as tagged JSON; snapshots must
|
|
2583
|
+
// expose record-shaped values (Uint8Array/string).
|
|
2584
|
+
fields: decodeRichtextFieldsFromOpData(op.data, definition.fields),
|
|
2403
2585
|
deleted: false,
|
|
2404
2586
|
createdAt: wallTime,
|
|
2405
2587
|
updatedAt: wallTime
|
|
@@ -2410,9 +2592,10 @@ function applyOperationToMemory(state, op, schema) {
|
|
|
2410
2592
|
if (!op.data) {
|
|
2411
2593
|
return;
|
|
2412
2594
|
}
|
|
2595
|
+
const decodedData = decodeRichtextFieldsFromOpData(op.data, definition.fields);
|
|
2413
2596
|
const existing = colMap.get(op.recordId);
|
|
2414
2597
|
if (existing && !existing.deleted) {
|
|
2415
|
-
existing.fields = { ...existing.fields, ...
|
|
2598
|
+
existing.fields = { ...existing.fields, ...decodedData };
|
|
2416
2599
|
existing.updatedAt = wallTime;
|
|
2417
2600
|
break;
|
|
2418
2601
|
}
|
|
@@ -2421,7 +2604,7 @@ function applyOperationToMemory(state, op, schema) {
|
|
|
2421
2604
|
}
|
|
2422
2605
|
colMap.set(op.recordId, {
|
|
2423
2606
|
id: op.recordId,
|
|
2424
|
-
fields:
|
|
2607
|
+
fields: decodedData,
|
|
2425
2608
|
deleted: false,
|
|
2426
2609
|
createdAt: wallTime,
|
|
2427
2610
|
updatedAt: wallTime
|
|
@@ -2477,7 +2660,7 @@ function toCollectionRecord(record, definition) {
|
|
|
2477
2660
|
}
|
|
2478
2661
|
|
|
2479
2662
|
// src/sequences/sequence-manager.ts
|
|
2480
|
-
var
|
|
2663
|
+
var import_core14 = require("@korajs/core");
|
|
2481
2664
|
var SequenceManager = class {
|
|
2482
2665
|
adapter;
|
|
2483
2666
|
nodeId;
|
|
@@ -2495,7 +2678,7 @@ var SequenceManager = class {
|
|
|
2495
2678
|
async next(name, config) {
|
|
2496
2679
|
const scope = config?.scope ?? "";
|
|
2497
2680
|
const startAt = config?.startAt ?? 1;
|
|
2498
|
-
const format = config?.format ?? (0,
|
|
2681
|
+
const format = config?.format ?? (0, import_core14.defaultSequenceFormat)(name);
|
|
2499
2682
|
let counter = 0;
|
|
2500
2683
|
await this.adapter.transaction(async (tx) => {
|
|
2501
2684
|
const rows = await tx.query(
|
|
@@ -2517,7 +2700,7 @@ var SequenceManager = class {
|
|
|
2517
2700
|
);
|
|
2518
2701
|
}
|
|
2519
2702
|
});
|
|
2520
|
-
return (0,
|
|
2703
|
+
return (0, import_core14.formatSequenceValue)(format, counter, this.nodeId);
|
|
2521
2704
|
}
|
|
2522
2705
|
/**
|
|
2523
2706
|
* Get the current counter value without incrementing.
|
|
@@ -2975,8 +3158,141 @@ var SubscriptionManager = class {
|
|
|
2975
3158
|
}
|
|
2976
3159
|
};
|
|
2977
3160
|
|
|
3161
|
+
// src/sync/rebase-unsynced-operations.ts
|
|
3162
|
+
var import_core15 = require("@korajs/core");
|
|
3163
|
+
var import_internal2 = require("@korajs/core/internal");
|
|
3164
|
+
init_serializer();
|
|
3165
|
+
var LOGICAL_COUNTER_LIMIT = import_core15.MAX_LOGICAL + 1;
|
|
3166
|
+
function emptyResult() {
|
|
3167
|
+
return { operations: [], idMapping: {}, rebasedCount: 0, newMaxTimestamp: null };
|
|
3168
|
+
}
|
|
3169
|
+
async function rebaseUnsyncedOperationsInLog(adapter, schema, unsyncedOpIds, correctedNowMs) {
|
|
3170
|
+
if (unsyncedOpIds.length === 0) {
|
|
3171
|
+
return emptyResult();
|
|
3172
|
+
}
|
|
3173
|
+
const rebaseIds = new Set(unsyncedOpIds);
|
|
3174
|
+
const rebased = [];
|
|
3175
|
+
let maxOtherSerializedTs = null;
|
|
3176
|
+
for (const collectionName of Object.keys(schema.collections)) {
|
|
3177
|
+
const rows = await adapter.query(`SELECT * FROM _kora_ops_${collectionName}`);
|
|
3178
|
+
for (const row of rows) {
|
|
3179
|
+
if (rebaseIds.has(row.id)) {
|
|
3180
|
+
rebased.push(deserializeOperationWithCollection(row, collectionName));
|
|
3181
|
+
} else if (maxOtherSerializedTs === null || row.timestamp > maxOtherSerializedTs) {
|
|
3182
|
+
maxOtherSerializedTs = row.timestamp;
|
|
3183
|
+
}
|
|
3184
|
+
}
|
|
3185
|
+
}
|
|
3186
|
+
if (rebased.length === 0) {
|
|
3187
|
+
return emptyResult();
|
|
3188
|
+
}
|
|
3189
|
+
const maxOtherWallTime = maxOtherSerializedTs === null ? Number.NEGATIVE_INFINITY : import_core15.HybridLogicalClock.deserialize(maxOtherSerializedTs).wallTime;
|
|
3190
|
+
const baseWallTime = Math.max(correctedNowMs, maxOtherWallTime + 1);
|
|
3191
|
+
rebased.sort((a, b) => import_core15.HybridLogicalClock.compare(a.timestamp, b.timestamp));
|
|
3192
|
+
const newTimestamps = rebased.map((op, i) => ({
|
|
3193
|
+
// Spill into wallTime past the 5-digit logical serialization limit so the
|
|
3194
|
+
// serialized order stays correct even for enormous offline queues.
|
|
3195
|
+
wallTime: baseWallTime + Math.floor(i / LOGICAL_COUNTER_LIMIT),
|
|
3196
|
+
logical: i % LOGICAL_COUNTER_LIMIT,
|
|
3197
|
+
nodeId: op.nodeId
|
|
3198
|
+
}));
|
|
3199
|
+
const idMapping = {};
|
|
3200
|
+
const newIds = [];
|
|
3201
|
+
for (let i = 0; i < rebased.length; i++) {
|
|
3202
|
+
const op = rebased[i];
|
|
3203
|
+
const ts = newTimestamps[i];
|
|
3204
|
+
if (!op || !ts) {
|
|
3205
|
+
continue;
|
|
3206
|
+
}
|
|
3207
|
+
const input = {
|
|
3208
|
+
nodeId: op.nodeId,
|
|
3209
|
+
type: op.type,
|
|
3210
|
+
collection: op.collection,
|
|
3211
|
+
recordId: op.recordId,
|
|
3212
|
+
data: op.data,
|
|
3213
|
+
previousData: op.previousData,
|
|
3214
|
+
sequenceNumber: op.sequenceNumber,
|
|
3215
|
+
causalDeps: op.causalDeps,
|
|
3216
|
+
schemaVersion: op.schemaVersion,
|
|
3217
|
+
...op.atomicOps !== void 0 ? { atomicOps: op.atomicOps } : {}
|
|
3218
|
+
};
|
|
3219
|
+
const newId = await (0, import_internal2.computeOperationId)(input, import_core15.HybridLogicalClock.serialize(ts));
|
|
3220
|
+
idMapping[op.id] = newId;
|
|
3221
|
+
newIds.push(newId);
|
|
3222
|
+
}
|
|
3223
|
+
const newOperations = [];
|
|
3224
|
+
for (let i = 0; i < rebased.length; i++) {
|
|
3225
|
+
const op = rebased[i];
|
|
3226
|
+
const ts = newTimestamps[i];
|
|
3227
|
+
const newId = newIds[i];
|
|
3228
|
+
if (!op || !ts || newId === void 0) {
|
|
3229
|
+
continue;
|
|
3230
|
+
}
|
|
3231
|
+
newOperations.push({
|
|
3232
|
+
...op,
|
|
3233
|
+
id: newId,
|
|
3234
|
+
timestamp: ts,
|
|
3235
|
+
causalDeps: op.causalDeps.map((dep) => idMapping[dep] ?? dep)
|
|
3236
|
+
});
|
|
3237
|
+
}
|
|
3238
|
+
await adapter.transaction(async (tx) => {
|
|
3239
|
+
for (let i = 0; i < rebased.length; i++) {
|
|
3240
|
+
const oldOp = rebased[i];
|
|
3241
|
+
const newOp = newOperations[i];
|
|
3242
|
+
if (!oldOp || !newOp) {
|
|
3243
|
+
continue;
|
|
3244
|
+
}
|
|
3245
|
+
const opsTable = `_kora_ops_${oldOp.collection}`;
|
|
3246
|
+
await tx.execute(`DELETE FROM ${opsTable} WHERE id = ?`, [oldOp.id]);
|
|
3247
|
+
const opInsert = buildInsertQuery(
|
|
3248
|
+
opsTable,
|
|
3249
|
+
serializeOperation(newOp)
|
|
3250
|
+
);
|
|
3251
|
+
await tx.execute(opInsert.sql, opInsert.params);
|
|
3252
|
+
const oldVersion = serializeRowVersion(oldOp.timestamp);
|
|
3253
|
+
const newVersion = serializeRowVersion(newOp.timestamp);
|
|
3254
|
+
await tx.execute(
|
|
3255
|
+
`UPDATE ${oldOp.collection} SET _version = ?, _updated_at = ? WHERE id = ? AND _version = ?`,
|
|
3256
|
+
[newVersion, newOp.timestamp.wallTime, oldOp.recordId, oldVersion]
|
|
3257
|
+
);
|
|
3258
|
+
const rows = await tx.query(
|
|
3259
|
+
`SELECT _field_versions FROM ${oldOp.collection} WHERE id = ?`,
|
|
3260
|
+
[oldOp.recordId]
|
|
3261
|
+
);
|
|
3262
|
+
const fieldVersions = parseFieldVersions(rows[0]?._field_versions);
|
|
3263
|
+
let changed = false;
|
|
3264
|
+
for (const [field, version] of Object.entries(fieldVersions)) {
|
|
3265
|
+
if (version === oldVersion) {
|
|
3266
|
+
fieldVersions[field] = newVersion;
|
|
3267
|
+
changed = true;
|
|
3268
|
+
}
|
|
3269
|
+
}
|
|
3270
|
+
if (changed) {
|
|
3271
|
+
await tx.execute(`UPDATE ${oldOp.collection} SET _field_versions = ? WHERE id = ?`, [
|
|
3272
|
+
serializeFieldVersions(fieldVersions),
|
|
3273
|
+
oldOp.recordId
|
|
3274
|
+
]);
|
|
3275
|
+
}
|
|
3276
|
+
if (oldOp.type === "insert") {
|
|
3277
|
+
await tx.execute(
|
|
3278
|
+
`UPDATE ${oldOp.collection} SET _created_at = ? WHERE id = ? AND _created_at = ?`,
|
|
3279
|
+
[newOp.timestamp.wallTime, oldOp.recordId, oldOp.timestamp.wallTime]
|
|
3280
|
+
);
|
|
3281
|
+
}
|
|
3282
|
+
}
|
|
3283
|
+
});
|
|
3284
|
+
const newMaxTimestamp = newTimestamps[newTimestamps.length - 1] ?? null;
|
|
3285
|
+
return {
|
|
3286
|
+
operations: newOperations,
|
|
3287
|
+
idMapping,
|
|
3288
|
+
rebasedCount: newOperations.length,
|
|
3289
|
+
newMaxTimestamp
|
|
3290
|
+
};
|
|
3291
|
+
}
|
|
3292
|
+
|
|
2978
3293
|
// src/transaction/transaction-context.ts
|
|
2979
|
-
var
|
|
3294
|
+
var import_core16 = require("@korajs/core");
|
|
3295
|
+
init_op_data_encoding();
|
|
2980
3296
|
init_serializer();
|
|
2981
3297
|
var TransactionContext = class {
|
|
2982
3298
|
transactionId;
|
|
@@ -2987,7 +3303,7 @@ var TransactionContext = class {
|
|
|
2987
3303
|
config;
|
|
2988
3304
|
constructor(config) {
|
|
2989
3305
|
this.config = config;
|
|
2990
|
-
this.transactionId = (0,
|
|
3306
|
+
this.transactionId = (0, import_core16.generateUUIDv7)();
|
|
2991
3307
|
}
|
|
2992
3308
|
/**
|
|
2993
3309
|
* Set a human-readable mutation name for this transaction.
|
|
@@ -3102,8 +3418,8 @@ var TransactionContext = class {
|
|
|
3102
3418
|
}
|
|
3103
3419
|
async insert(collectionName, definition, data) {
|
|
3104
3420
|
this.ensureActive();
|
|
3105
|
-
const validated = (0,
|
|
3106
|
-
const recordId = (0,
|
|
3421
|
+
const validated = (0, import_core16.validateRecord)(collectionName, definition, data, "insert");
|
|
3422
|
+
const recordId = (0, import_core16.generateUUIDv7)();
|
|
3107
3423
|
const now = Date.now();
|
|
3108
3424
|
for (const [fieldName, descriptor] of Object.entries(definition.fields)) {
|
|
3109
3425
|
if (descriptor.auto && descriptor.kind === "timestamp") {
|
|
@@ -3112,13 +3428,15 @@ var TransactionContext = class {
|
|
|
3112
3428
|
}
|
|
3113
3429
|
const sequenceNumber = await this.config.sequenceAllocator.allocate();
|
|
3114
3430
|
const causalDeps = this.config.causalTracker?.nextCausalDeps(collectionName, true) ?? [];
|
|
3115
|
-
const operation = await (0,
|
|
3431
|
+
const operation = await (0, import_core16.createOperation)(
|
|
3116
3432
|
{
|
|
3117
3433
|
nodeId: this.config.nodeId,
|
|
3118
3434
|
type: "insert",
|
|
3119
3435
|
collection: collectionName,
|
|
3120
3436
|
recordId,
|
|
3121
|
-
|
|
3437
|
+
// Binary richtext values are tagged as canonical JSON before the
|
|
3438
|
+
// operation is content-hashed (see encodeRichtextFieldsForOpData).
|
|
3439
|
+
data: encodeRichtextFieldsForOpData(validated, definition.fields),
|
|
3122
3440
|
previousData: null,
|
|
3123
3441
|
sequenceNumber,
|
|
3124
3442
|
causalDeps,
|
|
@@ -3165,7 +3483,7 @@ var TransactionContext = class {
|
|
|
3165
3483
|
if (!currentRecord) {
|
|
3166
3484
|
throw new RecordNotFoundError(collectionName, id);
|
|
3167
3485
|
}
|
|
3168
|
-
const validated = (0,
|
|
3486
|
+
const validated = (0, import_core16.validateRecord)(collectionName, definition, data, "update");
|
|
3169
3487
|
const now = Date.now();
|
|
3170
3488
|
const previousData = {};
|
|
3171
3489
|
const resolvedData = {};
|
|
@@ -3173,9 +3491,9 @@ var TransactionContext = class {
|
|
|
3173
3491
|
for (const key of Object.keys(validated)) {
|
|
3174
3492
|
const value = validated[key];
|
|
3175
3493
|
previousData[key] = currentRecord[key];
|
|
3176
|
-
if ((0,
|
|
3177
|
-
resolvedData[key] = (0,
|
|
3178
|
-
atomicOps[key] = (0,
|
|
3494
|
+
if ((0, import_core16.isAtomicOp)(value)) {
|
|
3495
|
+
resolvedData[key] = (0, import_core16.resolveAtomicOp)(currentRecord[key], value);
|
|
3496
|
+
atomicOps[key] = (0, import_core16.toAtomicOp)(value);
|
|
3179
3497
|
} else {
|
|
3180
3498
|
resolvedData[key] = value;
|
|
3181
3499
|
}
|
|
@@ -3183,14 +3501,16 @@ var TransactionContext = class {
|
|
|
3183
3501
|
const hasAtomicOps = Object.keys(atomicOps).length > 0;
|
|
3184
3502
|
const sequenceNumber = await this.config.sequenceAllocator.allocate();
|
|
3185
3503
|
const causalDeps = this.config.causalTracker?.nextCausalDeps(collectionName, true) ?? [];
|
|
3186
|
-
const operation = await (0,
|
|
3504
|
+
const operation = await (0, import_core16.createOperation)(
|
|
3187
3505
|
{
|
|
3188
3506
|
nodeId: this.config.nodeId,
|
|
3189
3507
|
type: "update",
|
|
3190
3508
|
collection: collectionName,
|
|
3191
3509
|
recordId: id,
|
|
3192
|
-
|
|
3193
|
-
|
|
3510
|
+
// Binary richtext values (new and previous) are tagged as canonical
|
|
3511
|
+
// JSON before the operation is content-hashed.
|
|
3512
|
+
data: encodeRichtextFieldsForOpData(resolvedData, definition.fields),
|
|
3513
|
+
previousData: encodeRichtextFieldsForOpData(previousData, definition.fields),
|
|
3194
3514
|
sequenceNumber,
|
|
3195
3515
|
causalDeps,
|
|
3196
3516
|
schemaVersion: this.config.schema.version,
|
|
@@ -3236,7 +3556,7 @@ var TransactionContext = class {
|
|
|
3236
3556
|
const now = Date.now();
|
|
3237
3557
|
const sequenceNumber = await this.config.sequenceAllocator.allocate();
|
|
3238
3558
|
const causalDeps = this.config.causalTracker?.nextCausalDeps(collectionName, true) ?? [];
|
|
3239
|
-
const operation = await (0,
|
|
3559
|
+
const operation = await (0, import_core16.createOperation)(
|
|
3240
3560
|
{
|
|
3241
3561
|
nodeId: this.config.nodeId,
|
|
3242
3562
|
type: "delete",
|
|
@@ -3297,14 +3617,16 @@ var TransactionContext = class {
|
|
|
3297
3617
|
if (entry.operation.type === "insert" && entry.operation.data) {
|
|
3298
3618
|
record = {
|
|
3299
3619
|
id,
|
|
3300
|
-
|
|
3620
|
+
// op.data stores binary richtext as tagged JSON; the effective
|
|
3621
|
+
// record must expose record-shaped values (Uint8Array/string).
|
|
3622
|
+
...decodeRichtextFieldsFromOpData(entry.operation.data, definition.fields),
|
|
3301
3623
|
createdAt: entry.operation.timestamp.wallTime,
|
|
3302
3624
|
updatedAt: entry.operation.timestamp.wallTime
|
|
3303
3625
|
};
|
|
3304
3626
|
} else if (entry.operation.type === "update" && entry.operation.data && record) {
|
|
3305
3627
|
record = {
|
|
3306
3628
|
...record,
|
|
3307
|
-
...entry.operation.data,
|
|
3629
|
+
...decodeRichtextFieldsFromOpData(entry.operation.data, definition.fields),
|
|
3308
3630
|
updatedAt: entry.operation.timestamp.wallTime
|
|
3309
3631
|
};
|
|
3310
3632
|
} else if (entry.operation.type === "delete") {
|
|
@@ -3339,7 +3661,7 @@ var TransactionSequenceAllocator = class {
|
|
|
3339
3661
|
};
|
|
3340
3662
|
|
|
3341
3663
|
// src/store/tab-node-id.ts
|
|
3342
|
-
var
|
|
3664
|
+
var import_core17 = require("@korajs/core");
|
|
3343
3665
|
function perTabStorageKey(dbName) {
|
|
3344
3666
|
return `kora-tab-node-${dbName}`;
|
|
3345
3667
|
}
|
|
@@ -3360,7 +3682,7 @@ function resolvePerTabNodeId(dbName) {
|
|
|
3360
3682
|
if (existing) {
|
|
3361
3683
|
return existing;
|
|
3362
3684
|
}
|
|
3363
|
-
const nodeId = (0,
|
|
3685
|
+
const nodeId = (0, import_core17.generateUUIDv7)();
|
|
3364
3686
|
savePerTabNodeId(dbName, nodeId);
|
|
3365
3687
|
return nodeId;
|
|
3366
3688
|
}
|
|
@@ -3370,7 +3692,7 @@ var Store = class {
|
|
|
3370
3692
|
opened = false;
|
|
3371
3693
|
nodeId = "";
|
|
3372
3694
|
sequenceNumber = 0;
|
|
3373
|
-
versionVector = (0,
|
|
3695
|
+
versionVector = (0, import_core19.createVersionVector)();
|
|
3374
3696
|
clock = null;
|
|
3375
3697
|
collections = /* @__PURE__ */ new Map();
|
|
3376
3698
|
subscriptionManager;
|
|
@@ -3384,6 +3706,7 @@ var Store = class {
|
|
|
3384
3706
|
localMutationHandler;
|
|
3385
3707
|
relationEnforcer = null;
|
|
3386
3708
|
causalTracker = null;
|
|
3709
|
+
secretKeyProvider;
|
|
3387
3710
|
constructor(config) {
|
|
3388
3711
|
this.schema = config.schema;
|
|
3389
3712
|
this.adapter = config.adapter;
|
|
@@ -3392,6 +3715,7 @@ var Store = class {
|
|
|
3392
3715
|
this.isolation = config.isolation ?? "shared";
|
|
3393
3716
|
this.emitter = config.emitter ?? null;
|
|
3394
3717
|
this.localMutationHandler = config.localMutationHandler ?? null;
|
|
3718
|
+
this.secretKeyProvider = config.secretKeyProvider;
|
|
3395
3719
|
this.subscriptionManager = new SubscriptionManager({
|
|
3396
3720
|
onQuerySubscribed: config.onQuerySubscribed
|
|
3397
3721
|
});
|
|
@@ -3404,8 +3728,8 @@ var Store = class {
|
|
|
3404
3728
|
await this.adapter.open(this.schema);
|
|
3405
3729
|
await this.runMigrationsIfNeeded();
|
|
3406
3730
|
this.nodeId = await this.loadOrGenerateNodeId();
|
|
3407
|
-
this.clock = new
|
|
3408
|
-
this.causalTracker = new
|
|
3731
|
+
this.clock = new import_core19.HybridLogicalClock(this.nodeId);
|
|
3732
|
+
this.causalTracker = new import_core19.CausalTracker();
|
|
3409
3733
|
this.sequenceManager = new SequenceManager(this.adapter, this.nodeId);
|
|
3410
3734
|
this.sequenceNumber = await this.loadSequenceNumber();
|
|
3411
3735
|
this.versionVector = await this.loadVersionVector();
|
|
@@ -3434,7 +3758,8 @@ var Store = class {
|
|
|
3434
3758
|
},
|
|
3435
3759
|
this.relationEnforcer,
|
|
3436
3760
|
this.localMutationHandler,
|
|
3437
|
-
this.causalTracker
|
|
3761
|
+
this.causalTracker,
|
|
3762
|
+
this.secretKeyProvider
|
|
3438
3763
|
);
|
|
3439
3764
|
this.collections.set(name, col);
|
|
3440
3765
|
}
|
|
@@ -3491,6 +3816,14 @@ var Store = class {
|
|
|
3491
3816
|
/**
|
|
3492
3817
|
* Get the node ID for this store instance.
|
|
3493
3818
|
*/
|
|
3819
|
+
/**
|
|
3820
|
+
* Records the offset between server time and this device's clock
|
|
3821
|
+
* (serverTime - localTime), learned at sync handshake. Lets the store's HLC
|
|
3822
|
+
* validate remote timestamps correctly even when the local clock is wrong.
|
|
3823
|
+
*/
|
|
3824
|
+
setClockReferenceOffset(offsetMs) {
|
|
3825
|
+
this.clock?.setReferenceOffset(offsetMs);
|
|
3826
|
+
}
|
|
3494
3827
|
getNodeId() {
|
|
3495
3828
|
this.ensureOpen();
|
|
3496
3829
|
return this.nodeId;
|
|
@@ -3517,45 +3850,158 @@ var Store = class {
|
|
|
3517
3850
|
if (this.clock) {
|
|
3518
3851
|
this.clock.receive(op.timestamp);
|
|
3519
3852
|
}
|
|
3520
|
-
const
|
|
3521
|
-
const
|
|
3853
|
+
const materializeTimestamp = options?.materializeTimestamp ?? op.timestamp;
|
|
3854
|
+
const remoteVersion = serializeRowVersion(materializeTimestamp);
|
|
3855
|
+
const wallTime = materializeTimestamp.wallTime;
|
|
3856
|
+
const materializeSource = options?.materializeData ?? op.data;
|
|
3522
3857
|
await this.adapter.transaction(async (tx) => {
|
|
3523
|
-
|
|
3858
|
+
const checkGuard = (row) => {
|
|
3859
|
+
const guard = options?.guardRowState;
|
|
3860
|
+
if (!guard) {
|
|
3861
|
+
return;
|
|
3862
|
+
}
|
|
3863
|
+
const version = typeof row?._version === "string" ? row._version : null;
|
|
3864
|
+
const fieldVersions = typeof row?._field_versions === "string" ? row._field_versions : null;
|
|
3865
|
+
if (version !== guard.version || fieldVersions !== guard.fieldVersions) {
|
|
3866
|
+
throw new OptimisticLockError(collection, op.recordId);
|
|
3867
|
+
}
|
|
3868
|
+
};
|
|
3869
|
+
if (op.type === "insert" && materializeSource) {
|
|
3870
|
+
const serializedData = serializeRecord(materializeSource, definition.fields);
|
|
3524
3871
|
const existing2 = await tx.query(
|
|
3525
|
-
`SELECT _updated_at, _version FROM ${collection} WHERE id = ?`,
|
|
3872
|
+
`SELECT _updated_at, _version, _field_versions, _deleted FROM ${collection} WHERE id = ?`,
|
|
3526
3873
|
[op.recordId]
|
|
3527
3874
|
);
|
|
3528
3875
|
const row = existing2[0];
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
const serializedData = serializeRecord(op.data, definition.fields);
|
|
3876
|
+
checkGuard(row);
|
|
3877
|
+
if (!row) {
|
|
3532
3878
|
const record = {
|
|
3533
3879
|
id: op.recordId,
|
|
3534
3880
|
...serializedData,
|
|
3535
3881
|
_created_at: wallTime,
|
|
3536
3882
|
_updated_at: wallTime,
|
|
3537
|
-
_version: remoteVersion
|
|
3883
|
+
_version: remoteVersion,
|
|
3884
|
+
// Stamp every inserted field so per-field LWW has a real baseline.
|
|
3885
|
+
_field_versions: serializeFieldVersions(
|
|
3886
|
+
fieldVersionsForFields(Object.keys(serializedData), remoteVersion)
|
|
3887
|
+
)
|
|
3538
3888
|
};
|
|
3539
3889
|
const insertQuery = buildInsertQuery(collection, record);
|
|
3540
3890
|
await tx.execute(insertQuery.sql, insertQuery.params);
|
|
3891
|
+
await this.foldOrphanedOperations(tx, collection, definition, op.recordId);
|
|
3892
|
+
} else if (row._deleted === 1) {
|
|
3893
|
+
if (isIncomingNewerThanRow(materializeTimestamp, row)) {
|
|
3894
|
+
const fieldChanges = {
|
|
3895
|
+
...serializedData,
|
|
3896
|
+
_deleted: 0,
|
|
3897
|
+
_field_versions: serializeFieldVersions(
|
|
3898
|
+
fieldVersionsForFields(Object.keys(serializedData), remoteVersion)
|
|
3899
|
+
)
|
|
3900
|
+
};
|
|
3901
|
+
const upsert = buildFieldFastForwardUpdateQuery(
|
|
3902
|
+
collection,
|
|
3903
|
+
op.recordId,
|
|
3904
|
+
fieldChanges,
|
|
3905
|
+
remoteVersion,
|
|
3906
|
+
wallTime,
|
|
3907
|
+
{ maxCreatedAt: wallTime }
|
|
3908
|
+
);
|
|
3909
|
+
await tx.execute(upsert.sql, upsert.params);
|
|
3910
|
+
}
|
|
3911
|
+
} else {
|
|
3912
|
+
const currentVersions = parseFieldVersions(row._field_versions);
|
|
3913
|
+
const rowVersion = typeof row._version === "string" ? row._version : void 0;
|
|
3914
|
+
const fieldChanges = {};
|
|
3915
|
+
let fieldVersionsJson;
|
|
3916
|
+
if (options?.forceMaterialize) {
|
|
3917
|
+
const merged = { ...currentVersions };
|
|
3918
|
+
for (const field of Object.keys(serializedData)) {
|
|
3919
|
+
fieldChanges[field] = serializedData[field];
|
|
3920
|
+
merged[field] = remoteVersion;
|
|
3921
|
+
}
|
|
3922
|
+
fieldVersionsJson = serializeFieldVersions(merged);
|
|
3923
|
+
} else {
|
|
3924
|
+
const { winners, merged } = resolvePerFieldLww(
|
|
3925
|
+
currentVersions,
|
|
3926
|
+
Object.keys(serializedData),
|
|
3927
|
+
remoteVersion,
|
|
3928
|
+
rowVersion
|
|
3929
|
+
);
|
|
3930
|
+
for (const field of winners) {
|
|
3931
|
+
fieldChanges[field] = serializedData[field];
|
|
3932
|
+
}
|
|
3933
|
+
fieldVersionsJson = serializeFieldVersions(merged);
|
|
3934
|
+
}
|
|
3935
|
+
const upsert = buildFieldFastForwardUpdateQuery(
|
|
3936
|
+
collection,
|
|
3937
|
+
op.recordId,
|
|
3938
|
+
{ ...fieldChanges, _field_versions: fieldVersionsJson },
|
|
3939
|
+
remoteVersion,
|
|
3940
|
+
wallTime,
|
|
3941
|
+
{ maxCreatedAt: wallTime }
|
|
3942
|
+
);
|
|
3943
|
+
await tx.execute(upsert.sql, upsert.params);
|
|
3541
3944
|
}
|
|
3542
|
-
} else if (op.type === "update" &&
|
|
3543
|
-
const serializedChanges = serializeRecord(
|
|
3544
|
-
const
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
};
|
|
3549
|
-
if (options?.reactivateIfDeleted) {
|
|
3550
|
-
updatePayload._deleted = 0;
|
|
3551
|
-
}
|
|
3552
|
-
const updateQuery = buildLwwUpdateQuery(
|
|
3553
|
-
collection,
|
|
3554
|
-
op.recordId,
|
|
3555
|
-
updatePayload,
|
|
3556
|
-
remoteVersion
|
|
3945
|
+
} else if (op.type === "update" && materializeSource) {
|
|
3946
|
+
const serializedChanges = serializeRecord(materializeSource, definition.fields);
|
|
3947
|
+
const changedFields = Object.keys(serializedChanges);
|
|
3948
|
+
const currentRows = await tx.query(
|
|
3949
|
+
`SELECT _version, _field_versions FROM ${collection} WHERE id = ?`,
|
|
3950
|
+
[op.recordId]
|
|
3557
3951
|
);
|
|
3558
|
-
|
|
3952
|
+
const currentRow = currentRows[0];
|
|
3953
|
+
checkGuard(currentRow);
|
|
3954
|
+
if (currentRow) {
|
|
3955
|
+
const currentVersions = parseFieldVersions(currentRow._field_versions);
|
|
3956
|
+
const rowVersion = typeof currentRow._version === "string" ? currentRow._version : void 0;
|
|
3957
|
+
if (options?.forceMaterialize) {
|
|
3958
|
+
const merged = { ...currentVersions };
|
|
3959
|
+
for (const field of changedFields) {
|
|
3960
|
+
merged[field] = remoteVersion;
|
|
3961
|
+
}
|
|
3962
|
+
const fieldChanges = {
|
|
3963
|
+
...serializedChanges,
|
|
3964
|
+
_field_versions: serializeFieldVersions(merged)
|
|
3965
|
+
};
|
|
3966
|
+
if (options?.reactivateIfDeleted) {
|
|
3967
|
+
fieldChanges._deleted = 0;
|
|
3968
|
+
}
|
|
3969
|
+
const forceQuery = buildFieldFastForwardUpdateQuery(
|
|
3970
|
+
collection,
|
|
3971
|
+
op.recordId,
|
|
3972
|
+
fieldChanges,
|
|
3973
|
+
remoteVersion,
|
|
3974
|
+
wallTime
|
|
3975
|
+
);
|
|
3976
|
+
await tx.execute(forceQuery.sql, forceQuery.params);
|
|
3977
|
+
} else {
|
|
3978
|
+
const { winners, merged } = resolvePerFieldLww(
|
|
3979
|
+
currentVersions,
|
|
3980
|
+
changedFields,
|
|
3981
|
+
remoteVersion,
|
|
3982
|
+
rowVersion
|
|
3983
|
+
);
|
|
3984
|
+
if (winners.length > 0 || options?.reactivateIfDeleted) {
|
|
3985
|
+
const winningChanges = {
|
|
3986
|
+
_field_versions: serializeFieldVersions(merged)
|
|
3987
|
+
};
|
|
3988
|
+
for (const field of winners) {
|
|
3989
|
+
winningChanges[field] = serializedChanges[field];
|
|
3990
|
+
}
|
|
3991
|
+
if (options?.reactivateIfDeleted) {
|
|
3992
|
+
winningChanges._deleted = 0;
|
|
3993
|
+
}
|
|
3994
|
+
const forceQuery = buildFieldFastForwardUpdateQuery(
|
|
3995
|
+
collection,
|
|
3996
|
+
op.recordId,
|
|
3997
|
+
winningChanges,
|
|
3998
|
+
remoteVersion,
|
|
3999
|
+
wallTime
|
|
4000
|
+
);
|
|
4001
|
+
await tx.execute(forceQuery.sql, forceQuery.params);
|
|
4002
|
+
}
|
|
4003
|
+
}
|
|
4004
|
+
}
|
|
3559
4005
|
} else if (op.type === "delete") {
|
|
3560
4006
|
const deleteQuery = buildLwwSoftDeleteQuery(
|
|
3561
4007
|
collection,
|
|
@@ -3583,6 +4029,67 @@ var Store = class {
|
|
|
3583
4029
|
this.subscriptionManager.notify(collection, op);
|
|
3584
4030
|
return "applied";
|
|
3585
4031
|
}
|
|
4032
|
+
/**
|
|
4033
|
+
* Materialize update/delete operations that were logged for a record BEFORE
|
|
4034
|
+
* its insert arrived (reordered delivery). Runs inside the insert's write
|
|
4035
|
+
* transaction, folding each orphan in timestamp order through the exact
|
|
4036
|
+
* per-field LWW / LWW-delete rules the normal apply paths use, so the final
|
|
4037
|
+
* row state equals what a device that received the operations in causal
|
|
4038
|
+
* order computed. Called only when the insert created a previously-absent
|
|
4039
|
+
* row — in that case every logged update/delete for the record is
|
|
4040
|
+
* necessarily an orphan (rows are soft-deleted, never removed, so a row
|
|
4041
|
+
* that is absent now was never materialized).
|
|
4042
|
+
*/
|
|
4043
|
+
async foldOrphanedOperations(tx, collection, definition, recordId) {
|
|
4044
|
+
const orphanRows = await tx.query(
|
|
4045
|
+
`SELECT * FROM _kora_ops_${collection} WHERE record_id = ? AND type IN ('update', 'delete')`,
|
|
4046
|
+
[recordId]
|
|
4047
|
+
);
|
|
4048
|
+
if (orphanRows.length === 0) {
|
|
4049
|
+
return;
|
|
4050
|
+
}
|
|
4051
|
+
const orphans = orphanRows.map((r) => deserializeOperationWithCollection(r, collection)).sort((a, b) => import_core19.HybridLogicalClock.compare(a.timestamp, b.timestamp));
|
|
4052
|
+
for (const orphan of orphans) {
|
|
4053
|
+
const orphanVersion = serializeRowVersion(orphan.timestamp);
|
|
4054
|
+
const orphanWall = orphan.timestamp.wallTime;
|
|
4055
|
+
if (orphan.type === "update" && orphan.data) {
|
|
4056
|
+
const orphanChanges = serializeRecord(orphan.data, definition.fields);
|
|
4057
|
+
const rows = await tx.query(
|
|
4058
|
+
`SELECT _version, _field_versions FROM ${collection} WHERE id = ?`,
|
|
4059
|
+
[recordId]
|
|
4060
|
+
);
|
|
4061
|
+
const current = rows[0];
|
|
4062
|
+
if (!current) {
|
|
4063
|
+
continue;
|
|
4064
|
+
}
|
|
4065
|
+
const { winners, merged } = resolvePerFieldLww(
|
|
4066
|
+
parseFieldVersions(current._field_versions),
|
|
4067
|
+
Object.keys(orphanChanges),
|
|
4068
|
+
orphanVersion,
|
|
4069
|
+
typeof current._version === "string" ? current._version : void 0
|
|
4070
|
+
);
|
|
4071
|
+
if (winners.length > 0) {
|
|
4072
|
+
const winningChanges = {
|
|
4073
|
+
_field_versions: serializeFieldVersions(merged)
|
|
4074
|
+
};
|
|
4075
|
+
for (const field of winners) {
|
|
4076
|
+
winningChanges[field] = orphanChanges[field];
|
|
4077
|
+
}
|
|
4078
|
+
const query = buildFieldFastForwardUpdateQuery(
|
|
4079
|
+
collection,
|
|
4080
|
+
recordId,
|
|
4081
|
+
winningChanges,
|
|
4082
|
+
orphanVersion,
|
|
4083
|
+
orphanWall
|
|
4084
|
+
);
|
|
4085
|
+
await tx.execute(query.sql, query.params);
|
|
4086
|
+
}
|
|
4087
|
+
} else if (orphan.type === "delete") {
|
|
4088
|
+
const query = buildLwwSoftDeleteQuery(collection, recordId, orphanWall, orphanVersion);
|
|
4089
|
+
await tx.execute(query.sql, query.params);
|
|
4090
|
+
}
|
|
4091
|
+
}
|
|
4092
|
+
}
|
|
3586
4093
|
/**
|
|
3587
4094
|
* Get operations from a node within a sequence number range.
|
|
3588
4095
|
* Implements the OperationLog interface for computeDelta.
|
|
@@ -3713,7 +4220,8 @@ var Store = class {
|
|
|
3713
4220
|
relationEnforcer: this.relationEnforcer,
|
|
3714
4221
|
causalTracker: this.causalTracker,
|
|
3715
4222
|
inTransaction: options?.inTransaction ?? false,
|
|
3716
|
-
extraCausalDeps: options?.extraCausalDeps
|
|
4223
|
+
extraCausalDeps: options?.extraCausalDeps,
|
|
4224
|
+
secretKeyProvider: this.secretKeyProvider
|
|
3717
4225
|
};
|
|
3718
4226
|
}
|
|
3719
4227
|
/**
|
|
@@ -3738,6 +4246,31 @@ var Store = class {
|
|
|
3738
4246
|
deleted: row._deleted === 1
|
|
3739
4247
|
};
|
|
3740
4248
|
}
|
|
4249
|
+
/**
|
|
4250
|
+
* Raw version state of a materialized row, for optimistic-concurrency guarded
|
|
4251
|
+
* applies: capture this snapshot before computing a merge, pass it as
|
|
4252
|
+
* `guardRowState`, and the apply refuses to write if the row changed since.
|
|
4253
|
+
* Returns null when no row exists (which is itself a valid guard state — the
|
|
4254
|
+
* apply then requires the row to still be absent).
|
|
4255
|
+
*/
|
|
4256
|
+
async getRowVersionState(collection, recordId) {
|
|
4257
|
+
this.ensureOpen();
|
|
4258
|
+
if (!this.schema.collections[collection]) {
|
|
4259
|
+
return null;
|
|
4260
|
+
}
|
|
4261
|
+
const rows = await this.adapter.query(
|
|
4262
|
+
`SELECT _version, _field_versions FROM ${collection} WHERE id = ?`,
|
|
4263
|
+
[recordId]
|
|
4264
|
+
);
|
|
4265
|
+
const row = rows[0];
|
|
4266
|
+
if (!row) {
|
|
4267
|
+
return null;
|
|
4268
|
+
}
|
|
4269
|
+
return {
|
|
4270
|
+
version: typeof row._version === "string" ? row._version : null,
|
|
4271
|
+
fieldVersions: typeof row._field_versions === "string" ? row._field_versions : null
|
|
4272
|
+
};
|
|
4273
|
+
}
|
|
3741
4274
|
/**
|
|
3742
4275
|
* Latest operation from this device for a record (used for delete-vs-update merge on sync).
|
|
3743
4276
|
*/
|
|
@@ -3780,6 +4313,30 @@ var Store = class {
|
|
|
3780
4313
|
(nodeId, fromSeq, toSeq) => this.getOperationRange(nodeId, fromSeq, toSeq)
|
|
3781
4314
|
);
|
|
3782
4315
|
}
|
|
4316
|
+
/**
|
|
4317
|
+
* Re-stamp never-acknowledged local operations after a fast device clock was
|
|
4318
|
+
* corrected, so sync can resume immediately. Safe because unacknowledged
|
|
4319
|
+
* operations are private to this device (like unpushed git commits);
|
|
4320
|
+
* acknowledged/shared operations are immutable and never touched.
|
|
4321
|
+
*
|
|
4322
|
+
* After the rewrite the store's HLC is advanced past the highest new
|
|
4323
|
+
* timestamp so subsequent local writes keep sorting after the rebased ops.
|
|
4324
|
+
* No data-change notifications are emitted: materialized values are
|
|
4325
|
+
* unchanged, only version stamps move.
|
|
4326
|
+
*/
|
|
4327
|
+
async rebaseUnsyncedOperations(unsyncedOpIds, correctedNowMs) {
|
|
4328
|
+
this.ensureOpen();
|
|
4329
|
+
const result = await rebaseUnsyncedOperationsInLog(
|
|
4330
|
+
this.adapter,
|
|
4331
|
+
this.schema,
|
|
4332
|
+
unsyncedOpIds,
|
|
4333
|
+
correctedNowMs
|
|
4334
|
+
);
|
|
4335
|
+
if (result.newMaxTimestamp && this.clock) {
|
|
4336
|
+
this.clock.advanceTo(result.newMaxTimestamp);
|
|
4337
|
+
}
|
|
4338
|
+
return result;
|
|
4339
|
+
}
|
|
3783
4340
|
/**
|
|
3784
4341
|
* Count of local operations ahead of the server version vector.
|
|
3785
4342
|
*/
|
|
@@ -3794,7 +4351,7 @@ var Store = class {
|
|
|
3794
4351
|
async compact(strategy) {
|
|
3795
4352
|
this.ensureOpen();
|
|
3796
4353
|
if (strategy.mode === "never") {
|
|
3797
|
-
return compactOperationLog(this.adapter, this.schema, strategy, (0,
|
|
4354
|
+
return compactOperationLog(this.adapter, this.schema, strategy, (0, import_core19.createVersionVector)());
|
|
3798
4355
|
}
|
|
3799
4356
|
const serverVector = strategy.serverVector ?? await loadLastAckedServerVector(this.adapter);
|
|
3800
4357
|
return compactOperationLog(this.adapter, this.schema, strategy, serverVector);
|
|
@@ -3829,7 +4386,7 @@ var Store = class {
|
|
|
3829
4386
|
let latest = null;
|
|
3830
4387
|
for (const row of rows) {
|
|
3831
4388
|
const op = deserializeOperationWithCollection(row, collection);
|
|
3832
|
-
if (!latest ||
|
|
4389
|
+
if (!latest || import_core19.HybridLogicalClock.compare(op.timestamp, latest.timestamp) > 0) {
|
|
3833
4390
|
latest = op;
|
|
3834
4391
|
}
|
|
3835
4392
|
}
|
|
@@ -3972,7 +4529,7 @@ var Store = class {
|
|
|
3972
4529
|
for (let v = storedVersion + 1; v <= targetVersion; v++) {
|
|
3973
4530
|
const migration = migrations[v];
|
|
3974
4531
|
if (!migration) continue;
|
|
3975
|
-
const sqlStatements = (0,
|
|
4532
|
+
const sqlStatements = (0, import_core19.migrationStepsToSQL)(migration.steps);
|
|
3976
4533
|
for (const sql of sqlStatements) {
|
|
3977
4534
|
try {
|
|
3978
4535
|
await this.adapter.execute(sql);
|
|
@@ -4050,7 +4607,7 @@ var Store = class {
|
|
|
4050
4607
|
if (rows[0]) {
|
|
4051
4608
|
return rows[0].value;
|
|
4052
4609
|
}
|
|
4053
|
-
const newNodeId = (0,
|
|
4610
|
+
const newNodeId = (0, import_core19.generateUUIDv7)();
|
|
4054
4611
|
await this.adapter.execute("INSERT INTO _kora_meta (key, value) VALUES ('node_id', ?)", [
|
|
4055
4612
|
newNodeId
|
|
4056
4613
|
]);
|
|
@@ -4067,7 +4624,7 @@ var Store = class {
|
|
|
4067
4624
|
const rows = await this.adapter.query(
|
|
4068
4625
|
"SELECT node_id, sequence_number FROM _kora_version_vector"
|
|
4069
4626
|
);
|
|
4070
|
-
const vector = (0,
|
|
4627
|
+
const vector = (0, import_core19.createVersionVector)();
|
|
4071
4628
|
for (const row of rows) {
|
|
4072
4629
|
vector.set(row.node_id, row.sequence_number);
|
|
4073
4630
|
}
|
|
@@ -4141,12 +4698,12 @@ var QueryStore = class {
|
|
|
4141
4698
|
};
|
|
4142
4699
|
|
|
4143
4700
|
// src/reactivity/assert-query-ready.ts
|
|
4144
|
-
var
|
|
4701
|
+
var import_core20 = require("@korajs/core");
|
|
4145
4702
|
var LEGACY_PENDING_COLLECTION = "__pending__";
|
|
4146
4703
|
function assertQueryReady(query) {
|
|
4147
4704
|
const descriptor = query.getDescriptor();
|
|
4148
4705
|
if (descriptor.collection === LEGACY_PENDING_COLLECTION) {
|
|
4149
|
-
throw new
|
|
4706
|
+
throw new import_core20.AppNotReadyError(
|
|
4150
4707
|
"Cannot use useQuery() before app.ready. Await app.ready or wrap your UI in <KoraProvider app={app}>."
|
|
4151
4708
|
);
|
|
4152
4709
|
}
|
|
@@ -4567,13 +5124,570 @@ init_backup();
|
|
|
4567
5124
|
init_audit_trace_store();
|
|
4568
5125
|
init_audit_json();
|
|
4569
5126
|
init_export_audit();
|
|
5127
|
+
|
|
5128
|
+
// src/blob/content-addressed-blob-store.ts
|
|
5129
|
+
var import_core21 = require("@korajs/core");
|
|
5130
|
+
var BlobIntegrityError = class extends Error {
|
|
5131
|
+
constructor(expectedHash, actualHash) {
|
|
5132
|
+
super(`Blob integrity check failed: content under hash ${expectedHash} hashes to ${actualHash}`);
|
|
5133
|
+
this.expectedHash = expectedHash;
|
|
5134
|
+
this.actualHash = actualHash;
|
|
5135
|
+
this.name = "BlobIntegrityError";
|
|
5136
|
+
}
|
|
5137
|
+
expectedHash;
|
|
5138
|
+
actualHash;
|
|
5139
|
+
};
|
|
5140
|
+
var MemoryBlobStore = class {
|
|
5141
|
+
blobs = /* @__PURE__ */ new Map();
|
|
5142
|
+
async put(bytes, metadata = {}) {
|
|
5143
|
+
const ref = await (0, import_core21.createBlobRef)(bytes, metadata);
|
|
5144
|
+
if (!this.blobs.has(ref.hash)) {
|
|
5145
|
+
const stored = new Uint8Array(bytes.byteLength);
|
|
5146
|
+
stored.set(bytes);
|
|
5147
|
+
this.blobs.set(ref.hash, stored);
|
|
5148
|
+
}
|
|
5149
|
+
return ref;
|
|
5150
|
+
}
|
|
5151
|
+
async get(hash) {
|
|
5152
|
+
const stored = this.blobs.get(hash);
|
|
5153
|
+
if (stored === void 0) {
|
|
5154
|
+
return null;
|
|
5155
|
+
}
|
|
5156
|
+
const actualHash = await (0, import_core21.hashBlob)(stored);
|
|
5157
|
+
if (actualHash !== hash) {
|
|
5158
|
+
throw new BlobIntegrityError(hash, actualHash);
|
|
5159
|
+
}
|
|
5160
|
+
const out = new Uint8Array(stored.byteLength);
|
|
5161
|
+
out.set(stored);
|
|
5162
|
+
return out;
|
|
5163
|
+
}
|
|
5164
|
+
async has(hash) {
|
|
5165
|
+
return this.blobs.has(hash);
|
|
5166
|
+
}
|
|
5167
|
+
async delete(hash) {
|
|
5168
|
+
return this.blobs.delete(hash);
|
|
5169
|
+
}
|
|
5170
|
+
async size() {
|
|
5171
|
+
return this.blobs.size;
|
|
5172
|
+
}
|
|
5173
|
+
async list() {
|
|
5174
|
+
return [...this.blobs.keys()];
|
|
5175
|
+
}
|
|
5176
|
+
};
|
|
5177
|
+
|
|
5178
|
+
// src/blob/opfs-blob-store.ts
|
|
5179
|
+
var import_core22 = require("@korajs/core");
|
|
5180
|
+
var OpfsBlobStore = class {
|
|
5181
|
+
constructor(dir) {
|
|
5182
|
+
this.dir = dir;
|
|
5183
|
+
}
|
|
5184
|
+
dir;
|
|
5185
|
+
shardFor(hash) {
|
|
5186
|
+
return hash.slice(0, 2);
|
|
5187
|
+
}
|
|
5188
|
+
async put(bytes, metadata = {}) {
|
|
5189
|
+
const ref = await (0, import_core22.createBlobRef)(bytes, metadata);
|
|
5190
|
+
const shard = this.shardFor(ref.hash);
|
|
5191
|
+
if (await this.dir.has(shard, ref.hash)) {
|
|
5192
|
+
return ref;
|
|
5193
|
+
}
|
|
5194
|
+
const out = new Uint8Array(bytes.byteLength);
|
|
5195
|
+
out.set(bytes);
|
|
5196
|
+
await this.dir.write(shard, ref.hash, out);
|
|
5197
|
+
return ref;
|
|
5198
|
+
}
|
|
5199
|
+
async get(hash) {
|
|
5200
|
+
const bytes = await this.dir.read(this.shardFor(hash), hash);
|
|
5201
|
+
if (bytes === null) {
|
|
5202
|
+
return null;
|
|
5203
|
+
}
|
|
5204
|
+
const actual = await (0, import_core22.hashBlob)(bytes);
|
|
5205
|
+
if (actual !== hash) {
|
|
5206
|
+
throw new BlobIntegrityError(hash, actual);
|
|
5207
|
+
}
|
|
5208
|
+
return bytes;
|
|
5209
|
+
}
|
|
5210
|
+
async has(hash) {
|
|
5211
|
+
return this.dir.has(this.shardFor(hash), hash);
|
|
5212
|
+
}
|
|
5213
|
+
async delete(hash) {
|
|
5214
|
+
return this.dir.remove(this.shardFor(hash), hash);
|
|
5215
|
+
}
|
|
5216
|
+
async size() {
|
|
5217
|
+
return this.dir.count();
|
|
5218
|
+
}
|
|
5219
|
+
async list() {
|
|
5220
|
+
return this.dir.keys();
|
|
5221
|
+
}
|
|
5222
|
+
};
|
|
5223
|
+
function getStorageManager() {
|
|
5224
|
+
const nav = globalThis.navigator;
|
|
5225
|
+
if (!nav?.storage || typeof nav.storage.getDirectory !== "function") {
|
|
5226
|
+
throw new Error(
|
|
5227
|
+
"OPFS is unavailable in this environment. The OPFS blob store requires a browser with Origin Private File System support; use MemoryBlobStore or a server-side store elsewhere."
|
|
5228
|
+
);
|
|
5229
|
+
}
|
|
5230
|
+
return nav.storage;
|
|
5231
|
+
}
|
|
5232
|
+
function isNotFoundError(error) {
|
|
5233
|
+
return typeof error === "object" && error !== null && error.name === "NotFoundError";
|
|
5234
|
+
}
|
|
5235
|
+
async function createOpfsBlobDirectory(rootDirName = "kora-blobs") {
|
|
5236
|
+
const storage = getStorageManager();
|
|
5237
|
+
if (typeof storage.persist === "function") {
|
|
5238
|
+
try {
|
|
5239
|
+
await storage.persist();
|
|
5240
|
+
} catch {
|
|
5241
|
+
}
|
|
5242
|
+
}
|
|
5243
|
+
const opfsRoot = await storage.getDirectory();
|
|
5244
|
+
const root = await opfsRoot.getDirectoryHandle(rootDirName, { create: true });
|
|
5245
|
+
async function shardHandle(shard, create) {
|
|
5246
|
+
try {
|
|
5247
|
+
return await root.getDirectoryHandle(shard, { create });
|
|
5248
|
+
} catch (error) {
|
|
5249
|
+
if (isNotFoundError(error)) {
|
|
5250
|
+
return null;
|
|
5251
|
+
}
|
|
5252
|
+
throw error;
|
|
5253
|
+
}
|
|
5254
|
+
}
|
|
5255
|
+
return {
|
|
5256
|
+
async read(shard, name) {
|
|
5257
|
+
const dir = await shardHandle(shard, false);
|
|
5258
|
+
if (!dir) {
|
|
5259
|
+
return null;
|
|
5260
|
+
}
|
|
5261
|
+
let handle;
|
|
5262
|
+
try {
|
|
5263
|
+
handle = await dir.getFileHandle(name, { create: false });
|
|
5264
|
+
} catch (error) {
|
|
5265
|
+
if (isNotFoundError(error)) {
|
|
5266
|
+
return null;
|
|
5267
|
+
}
|
|
5268
|
+
throw error;
|
|
5269
|
+
}
|
|
5270
|
+
const file = await handle.getFile();
|
|
5271
|
+
return new Uint8Array(await file.arrayBuffer());
|
|
5272
|
+
},
|
|
5273
|
+
async write(shard, name, bytes) {
|
|
5274
|
+
const dir = await root.getDirectoryHandle(shard, { create: true });
|
|
5275
|
+
const handle = await dir.getFileHandle(name, { create: true });
|
|
5276
|
+
const writable = await handle.createWritable({ keepExistingData: false });
|
|
5277
|
+
try {
|
|
5278
|
+
await writable.write(bytes);
|
|
5279
|
+
} finally {
|
|
5280
|
+
await writable.close();
|
|
5281
|
+
}
|
|
5282
|
+
},
|
|
5283
|
+
async remove(shard, name) {
|
|
5284
|
+
const dir = await shardHandle(shard, false);
|
|
5285
|
+
if (!dir) {
|
|
5286
|
+
return false;
|
|
5287
|
+
}
|
|
5288
|
+
try {
|
|
5289
|
+
await dir.removeEntry(name);
|
|
5290
|
+
return true;
|
|
5291
|
+
} catch (error) {
|
|
5292
|
+
if (isNotFoundError(error)) {
|
|
5293
|
+
return false;
|
|
5294
|
+
}
|
|
5295
|
+
throw error;
|
|
5296
|
+
}
|
|
5297
|
+
},
|
|
5298
|
+
async has(shard, name) {
|
|
5299
|
+
const dir = await shardHandle(shard, false);
|
|
5300
|
+
if (!dir) {
|
|
5301
|
+
return false;
|
|
5302
|
+
}
|
|
5303
|
+
try {
|
|
5304
|
+
await dir.getFileHandle(name, { create: false });
|
|
5305
|
+
return true;
|
|
5306
|
+
} catch (error) {
|
|
5307
|
+
if (isNotFoundError(error)) {
|
|
5308
|
+
return false;
|
|
5309
|
+
}
|
|
5310
|
+
throw error;
|
|
5311
|
+
}
|
|
5312
|
+
},
|
|
5313
|
+
async count() {
|
|
5314
|
+
let total = 0;
|
|
5315
|
+
for await (const shard of root.keys()) {
|
|
5316
|
+
const dir = await shardHandle(shard, false);
|
|
5317
|
+
if (!dir) {
|
|
5318
|
+
continue;
|
|
5319
|
+
}
|
|
5320
|
+
for await (const name of dir.keys()) {
|
|
5321
|
+
if (!name.endsWith(".tmp")) {
|
|
5322
|
+
total++;
|
|
5323
|
+
}
|
|
5324
|
+
}
|
|
5325
|
+
}
|
|
5326
|
+
return total;
|
|
5327
|
+
},
|
|
5328
|
+
async keys() {
|
|
5329
|
+
const names = [];
|
|
5330
|
+
for await (const shard of root.keys()) {
|
|
5331
|
+
const dir = await shardHandle(shard, false);
|
|
5332
|
+
if (!dir) {
|
|
5333
|
+
continue;
|
|
5334
|
+
}
|
|
5335
|
+
for await (const name of dir.keys()) {
|
|
5336
|
+
if (!name.endsWith(".tmp")) {
|
|
5337
|
+
names.push(name);
|
|
5338
|
+
}
|
|
5339
|
+
}
|
|
5340
|
+
}
|
|
5341
|
+
return names;
|
|
5342
|
+
}
|
|
5343
|
+
};
|
|
5344
|
+
}
|
|
5345
|
+
async function createOpfsBlobStore(rootDirName = "kora-blobs") {
|
|
5346
|
+
return new OpfsBlobStore(await createOpfsBlobDirectory(rootDirName));
|
|
5347
|
+
}
|
|
5348
|
+
|
|
5349
|
+
// src/blob/blob-chunking.ts
|
|
5350
|
+
var import_core23 = require("@korajs/core");
|
|
5351
|
+
var DEFAULT_CHUNK_SIZE = 256 * 1024;
|
|
5352
|
+
async function chunkBlob(bytes, chunkSize = DEFAULT_CHUNK_SIZE, metadata = {}) {
|
|
5353
|
+
if (chunkSize <= 0) {
|
|
5354
|
+
throw new Error(`chunkBlob requires a positive chunkSize, got ${chunkSize}`);
|
|
5355
|
+
}
|
|
5356
|
+
const chunkHashes = [];
|
|
5357
|
+
const chunks = /* @__PURE__ */ new Map();
|
|
5358
|
+
for (let offset = 0; offset < bytes.byteLength; offset += chunkSize) {
|
|
5359
|
+
const end = Math.min(offset + chunkSize, bytes.byteLength);
|
|
5360
|
+
const chunk = new Uint8Array(end - offset);
|
|
5361
|
+
chunk.set(bytes.subarray(offset, end));
|
|
5362
|
+
const hash = await (0, import_core23.hashBlob)(chunk);
|
|
5363
|
+
chunkHashes.push(hash);
|
|
5364
|
+
if (!chunks.has(hash)) {
|
|
5365
|
+
chunks.set(hash, chunk);
|
|
5366
|
+
}
|
|
5367
|
+
}
|
|
5368
|
+
const blobHash = await (0, import_core23.hashBlob)(bytes);
|
|
5369
|
+
const manifest = {
|
|
5370
|
+
blobHash,
|
|
5371
|
+
size: bytes.byteLength,
|
|
5372
|
+
chunkSize,
|
|
5373
|
+
chunkHashes
|
|
5374
|
+
};
|
|
5375
|
+
if (metadata.mimeType !== void 0) {
|
|
5376
|
+
manifest.mimeType = metadata.mimeType;
|
|
5377
|
+
}
|
|
5378
|
+
if (metadata.filename !== void 0) {
|
|
5379
|
+
manifest.filename = metadata.filename;
|
|
5380
|
+
}
|
|
5381
|
+
return { manifest, chunks };
|
|
5382
|
+
}
|
|
5383
|
+
async function reassembleBlob(manifest, chunkStore) {
|
|
5384
|
+
const parts = [];
|
|
5385
|
+
let total = 0;
|
|
5386
|
+
for (const hash of manifest.chunkHashes) {
|
|
5387
|
+
const chunk = await chunkStore.get(hash);
|
|
5388
|
+
if (chunk === null) {
|
|
5389
|
+
throw new Error(`Cannot reassemble blob ${manifest.blobHash}: missing chunk ${hash}`);
|
|
5390
|
+
}
|
|
5391
|
+
parts.push(chunk);
|
|
5392
|
+
total += chunk.byteLength;
|
|
5393
|
+
}
|
|
5394
|
+
const out = new Uint8Array(total);
|
|
5395
|
+
let offset = 0;
|
|
5396
|
+
for (const part of parts) {
|
|
5397
|
+
out.set(part, offset);
|
|
5398
|
+
offset += part.byteLength;
|
|
5399
|
+
}
|
|
5400
|
+
const actual = await (0, import_core23.hashBlob)(out);
|
|
5401
|
+
if (actual !== manifest.blobHash) {
|
|
5402
|
+
throw new BlobIntegrityError(manifest.blobHash, actual);
|
|
5403
|
+
}
|
|
5404
|
+
return out;
|
|
5405
|
+
}
|
|
5406
|
+
|
|
5407
|
+
// src/blob/blob-transfer.ts
|
|
5408
|
+
var import_core24 = require("@korajs/core");
|
|
5409
|
+
async function receiveBlob(manifest, provider, stores) {
|
|
5410
|
+
let chunksFetched = 0;
|
|
5411
|
+
let chunksSkipped = 0;
|
|
5412
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5413
|
+
for (const hash of manifest.chunkHashes) {
|
|
5414
|
+
if (seen.has(hash)) {
|
|
5415
|
+
continue;
|
|
5416
|
+
}
|
|
5417
|
+
seen.add(hash);
|
|
5418
|
+
if (await stores.chunkStore.has(hash)) {
|
|
5419
|
+
chunksSkipped++;
|
|
5420
|
+
continue;
|
|
5421
|
+
}
|
|
5422
|
+
const bytes = await provider.getChunk(hash);
|
|
5423
|
+
if (bytes === null) {
|
|
5424
|
+
throw new Error(`Blob transfer failed: provider could not supply chunk ${hash}`);
|
|
5425
|
+
}
|
|
5426
|
+
const actual = await (0, import_core24.hashBlob)(bytes);
|
|
5427
|
+
if (actual !== hash) {
|
|
5428
|
+
throw new BlobIntegrityError(hash, actual);
|
|
5429
|
+
}
|
|
5430
|
+
await stores.chunkStore.put(bytes);
|
|
5431
|
+
chunksFetched++;
|
|
5432
|
+
}
|
|
5433
|
+
const full = await reassembleBlob(manifest, stores.chunkStore);
|
|
5434
|
+
const ref = await stores.blobStore.put(full, {
|
|
5435
|
+
mimeType: manifest.mimeType,
|
|
5436
|
+
filename: manifest.filename
|
|
5437
|
+
});
|
|
5438
|
+
return { ref, chunksFetched, chunksSkipped };
|
|
5439
|
+
}
|
|
5440
|
+
async function prepareBlobForSend(bytes, chunkStore, options = {}) {
|
|
5441
|
+
const { manifest, chunks } = await chunkBlob(bytes, options.chunkSize, {
|
|
5442
|
+
mimeType: options.mimeType,
|
|
5443
|
+
filename: options.filename
|
|
5444
|
+
});
|
|
5445
|
+
for (const chunk of chunks.values()) {
|
|
5446
|
+
await chunkStore.put(chunk);
|
|
5447
|
+
}
|
|
5448
|
+
const provider = {
|
|
5449
|
+
getChunk: (hash) => chunkStore.get(hash)
|
|
5450
|
+
};
|
|
5451
|
+
return { manifest, provider };
|
|
5452
|
+
}
|
|
5453
|
+
|
|
5454
|
+
// src/blob/blob-manifest-transfer.ts
|
|
5455
|
+
var import_core25 = require("@korajs/core");
|
|
5456
|
+
function serializeBlobManifest(manifest) {
|
|
5457
|
+
const canonical = {
|
|
5458
|
+
blobHash: manifest.blobHash,
|
|
5459
|
+
size: manifest.size,
|
|
5460
|
+
chunkSize: manifest.chunkSize,
|
|
5461
|
+
chunkHashes: manifest.chunkHashes
|
|
5462
|
+
};
|
|
5463
|
+
if (manifest.mimeType !== void 0) {
|
|
5464
|
+
canonical.mimeType = manifest.mimeType;
|
|
5465
|
+
}
|
|
5466
|
+
if (manifest.filename !== void 0) {
|
|
5467
|
+
canonical.filename = manifest.filename;
|
|
5468
|
+
}
|
|
5469
|
+
return new TextEncoder().encode(JSON.stringify(canonical));
|
|
5470
|
+
}
|
|
5471
|
+
function parseBlobManifest(bytes) {
|
|
5472
|
+
const parsed = JSON.parse(new TextDecoder().decode(bytes));
|
|
5473
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
5474
|
+
throw new Error("Invalid blob manifest: not an object");
|
|
5475
|
+
}
|
|
5476
|
+
const obj = parsed;
|
|
5477
|
+
if (typeof obj.blobHash !== "string") {
|
|
5478
|
+
throw new Error("Invalid blob manifest: missing blobHash");
|
|
5479
|
+
}
|
|
5480
|
+
if (typeof obj.size !== "number" || typeof obj.chunkSize !== "number") {
|
|
5481
|
+
throw new Error("Invalid blob manifest: missing size or chunkSize");
|
|
5482
|
+
}
|
|
5483
|
+
if (!Array.isArray(obj.chunkHashes) || obj.chunkHashes.some((h) => typeof h !== "string")) {
|
|
5484
|
+
throw new Error("Invalid blob manifest: chunkHashes must be an array of strings");
|
|
5485
|
+
}
|
|
5486
|
+
const manifest = {
|
|
5487
|
+
blobHash: obj.blobHash,
|
|
5488
|
+
size: obj.size,
|
|
5489
|
+
chunkSize: obj.chunkSize,
|
|
5490
|
+
chunkHashes: obj.chunkHashes
|
|
5491
|
+
};
|
|
5492
|
+
if (typeof obj.mimeType === "string") {
|
|
5493
|
+
manifest.mimeType = obj.mimeType;
|
|
5494
|
+
}
|
|
5495
|
+
if (typeof obj.filename === "string") {
|
|
5496
|
+
manifest.filename = obj.filename;
|
|
5497
|
+
}
|
|
5498
|
+
return manifest;
|
|
5499
|
+
}
|
|
5500
|
+
async function putBlobForTransfer(store, bytes, options = {}) {
|
|
5501
|
+
const { manifest, chunks } = await chunkBlob(bytes, options.chunkSize, {
|
|
5502
|
+
...options.mimeType !== void 0 ? { mimeType: options.mimeType } : {},
|
|
5503
|
+
...options.filename !== void 0 ? { filename: options.filename } : {}
|
|
5504
|
+
});
|
|
5505
|
+
for (const chunk of chunks.values()) {
|
|
5506
|
+
await store.put(chunk);
|
|
5507
|
+
}
|
|
5508
|
+
const baseRef = await store.put(bytes, {
|
|
5509
|
+
...options.mimeType !== void 0 ? { mimeType: options.mimeType } : {},
|
|
5510
|
+
...options.filename !== void 0 ? { filename: options.filename } : {}
|
|
5511
|
+
});
|
|
5512
|
+
const manifestRef = await store.put(serializeBlobManifest(manifest));
|
|
5513
|
+
return { ref: { ...baseRef, manifestHash: manifestRef.hash }, manifest };
|
|
5514
|
+
}
|
|
5515
|
+
async function fetchBlobManifest(provider, manifestHash) {
|
|
5516
|
+
const bytes = await provider.getChunk(manifestHash);
|
|
5517
|
+
if (bytes === null) {
|
|
5518
|
+
throw new Error(`Blob manifest ${manifestHash} could not be fetched from the provider`);
|
|
5519
|
+
}
|
|
5520
|
+
const actual = await (0, import_core25.hashBlob)(bytes);
|
|
5521
|
+
if (actual !== manifestHash) {
|
|
5522
|
+
throw new BlobIntegrityError(manifestHash, actual);
|
|
5523
|
+
}
|
|
5524
|
+
return parseBlobManifest(bytes);
|
|
5525
|
+
}
|
|
5526
|
+
async function resolveBlobManifest(provider, ref) {
|
|
5527
|
+
if (ref.manifestHash === void 0) {
|
|
5528
|
+
throw new Error(
|
|
5529
|
+
`Blob ${ref.hash} has no manifest to resolve. Store it with putBlobForTransfer (or app.blobs.put) so its reference carries a manifestHash, or pull with an explicit manifest.`
|
|
5530
|
+
);
|
|
5531
|
+
}
|
|
5532
|
+
return fetchBlobManifest(provider, ref.manifestHash);
|
|
5533
|
+
}
|
|
5534
|
+
|
|
5535
|
+
// src/blob/server-blob-callbacks.ts
|
|
5536
|
+
var import_core26 = require("@korajs/core");
|
|
5537
|
+
function toServerBlobCallbacks(store) {
|
|
5538
|
+
return {
|
|
5539
|
+
resolveBlobChunk: (hash) => store.get(hash),
|
|
5540
|
+
async persistBlobChunk(hash, bytes) {
|
|
5541
|
+
const actual = await (0, import_core26.hashBlob)(bytes);
|
|
5542
|
+
if (actual !== hash) {
|
|
5543
|
+
return;
|
|
5544
|
+
}
|
|
5545
|
+
await store.put(bytes);
|
|
5546
|
+
}
|
|
5547
|
+
};
|
|
5548
|
+
}
|
|
5549
|
+
function createMemoryServerBlobStore() {
|
|
5550
|
+
const store = new MemoryBlobStore();
|
|
5551
|
+
return { store, callbacks: toServerBlobCallbacks(store) };
|
|
5552
|
+
}
|
|
5553
|
+
|
|
5554
|
+
// src/blob/blob-gc.ts
|
|
5555
|
+
var import_core27 = require("@korajs/core");
|
|
5556
|
+
function extractBlobRefs(record) {
|
|
5557
|
+
const refs = [];
|
|
5558
|
+
for (const value of Object.values(record)) {
|
|
5559
|
+
if ((0, import_core27.isBlobRef)(value)) {
|
|
5560
|
+
refs.push(value);
|
|
5561
|
+
}
|
|
5562
|
+
}
|
|
5563
|
+
return refs;
|
|
5564
|
+
}
|
|
5565
|
+
async function collectBlobGarbage(store, liveRefs, options = {}) {
|
|
5566
|
+
const live = /* @__PURE__ */ new Set();
|
|
5567
|
+
for (const ref of liveRefs) {
|
|
5568
|
+
live.add(ref.hash);
|
|
5569
|
+
if (ref.manifestHash === void 0) {
|
|
5570
|
+
continue;
|
|
5571
|
+
}
|
|
5572
|
+
live.add(ref.manifestHash);
|
|
5573
|
+
try {
|
|
5574
|
+
const manifestBytes = await store.get(ref.manifestHash);
|
|
5575
|
+
if (manifestBytes !== null) {
|
|
5576
|
+
const manifest = parseBlobManifest(manifestBytes);
|
|
5577
|
+
for (const chunkHash of manifest.chunkHashes) {
|
|
5578
|
+
live.add(chunkHash);
|
|
5579
|
+
}
|
|
5580
|
+
}
|
|
5581
|
+
} catch {
|
|
5582
|
+
}
|
|
5583
|
+
}
|
|
5584
|
+
const all = await store.list();
|
|
5585
|
+
const collectedHashes = [];
|
|
5586
|
+
for (const hash of all) {
|
|
5587
|
+
if (live.has(hash)) {
|
|
5588
|
+
continue;
|
|
5589
|
+
}
|
|
5590
|
+
collectedHashes.push(hash);
|
|
5591
|
+
if (!options.dryRun) {
|
|
5592
|
+
await store.delete(hash);
|
|
5593
|
+
}
|
|
5594
|
+
}
|
|
5595
|
+
return {
|
|
5596
|
+
scanned: all.length,
|
|
5597
|
+
live: live.size,
|
|
5598
|
+
collected: collectedHashes.length,
|
|
5599
|
+
collectedHashes
|
|
5600
|
+
};
|
|
5601
|
+
}
|
|
5602
|
+
|
|
5603
|
+
// src/blob/blob-chunk-transport.ts
|
|
5604
|
+
var import_core28 = require("@korajs/core");
|
|
5605
|
+
function createRemoteChunkProvider(port, options = {}) {
|
|
5606
|
+
const timeoutMs = options.timeoutMs ?? 3e4;
|
|
5607
|
+
const pending = /* @__PURE__ */ new Map();
|
|
5608
|
+
port.onMessage((message) => {
|
|
5609
|
+
if (message.type !== "blob-chunk-response") {
|
|
5610
|
+
return;
|
|
5611
|
+
}
|
|
5612
|
+
const entry = pending.get(message.requestId);
|
|
5613
|
+
if (!entry) {
|
|
5614
|
+
return;
|
|
5615
|
+
}
|
|
5616
|
+
pending.delete(message.requestId);
|
|
5617
|
+
clearTimeout(entry.timer);
|
|
5618
|
+
entry.resolve(message.bytes);
|
|
5619
|
+
});
|
|
5620
|
+
return {
|
|
5621
|
+
pendingCount: () => pending.size,
|
|
5622
|
+
getChunk(hash) {
|
|
5623
|
+
const requestId = (0, import_core28.generateUUIDv7)();
|
|
5624
|
+
return new Promise((resolve, reject) => {
|
|
5625
|
+
const timer = setTimeout(() => {
|
|
5626
|
+
pending.delete(requestId);
|
|
5627
|
+
reject(new Error(`Blob chunk request for ${hash} timed out after ${timeoutMs}ms`));
|
|
5628
|
+
}, timeoutMs);
|
|
5629
|
+
pending.set(requestId, { resolve, reject, timer });
|
|
5630
|
+
port.send({ type: "blob-chunk-request", requestId, hash });
|
|
5631
|
+
});
|
|
5632
|
+
}
|
|
5633
|
+
};
|
|
5634
|
+
}
|
|
5635
|
+
function serveBlobChunks(port, blobStore) {
|
|
5636
|
+
port.onMessage((message) => {
|
|
5637
|
+
if (message.type !== "blob-chunk-request") {
|
|
5638
|
+
return;
|
|
5639
|
+
}
|
|
5640
|
+
void blobStore.get(message.hash).then(
|
|
5641
|
+
(bytes) => {
|
|
5642
|
+
port.send({ type: "blob-chunk-response", requestId: message.requestId, bytes });
|
|
5643
|
+
},
|
|
5644
|
+
() => {
|
|
5645
|
+
port.send({ type: "blob-chunk-response", requestId: message.requestId, bytes: null });
|
|
5646
|
+
}
|
|
5647
|
+
);
|
|
5648
|
+
});
|
|
5649
|
+
}
|
|
5650
|
+
function createChunkPortPair() {
|
|
5651
|
+
const handlersA = [];
|
|
5652
|
+
const handlersB = [];
|
|
5653
|
+
const a = {
|
|
5654
|
+
send(message) {
|
|
5655
|
+
queueMicrotask(() => {
|
|
5656
|
+
for (const handler of handlersB) {
|
|
5657
|
+
handler(message);
|
|
5658
|
+
}
|
|
5659
|
+
});
|
|
5660
|
+
},
|
|
5661
|
+
onMessage(handler) {
|
|
5662
|
+
handlersA.push(handler);
|
|
5663
|
+
}
|
|
5664
|
+
};
|
|
5665
|
+
const b = {
|
|
5666
|
+
send(message) {
|
|
5667
|
+
queueMicrotask(() => {
|
|
5668
|
+
for (const handler of handlersA) {
|
|
5669
|
+
handler(message);
|
|
5670
|
+
}
|
|
5671
|
+
});
|
|
5672
|
+
},
|
|
5673
|
+
onMessage(handler) {
|
|
5674
|
+
handlersB.push(handler);
|
|
5675
|
+
}
|
|
5676
|
+
};
|
|
5677
|
+
return { a, b };
|
|
5678
|
+
}
|
|
4570
5679
|
// Annotate the CommonJS export names for ESM import in node:
|
|
4571
5680
|
0 && (module.exports = {
|
|
4572
5681
|
AdapterError,
|
|
5682
|
+
BlobIntegrityError,
|
|
4573
5683
|
COMPACTION_BASELINE_META_KEY,
|
|
4574
5684
|
Collection,
|
|
5685
|
+
DEFAULT_CHUNK_SIZE,
|
|
4575
5686
|
InvalidStateTransitionError,
|
|
4576
5687
|
LAST_ACKED_SERVER_VECTOR_META_KEY,
|
|
5688
|
+
MemoryBlobStore,
|
|
5689
|
+
OpfsBlobStore,
|
|
5690
|
+
OptimisticLockError,
|
|
4577
5691
|
PersistenceError,
|
|
4578
5692
|
QueryBuilder,
|
|
4579
5693
|
QueryError,
|
|
@@ -4590,26 +5704,44 @@ init_export_audit();
|
|
|
4590
5704
|
WorkerTimeoutError,
|
|
4591
5705
|
asRichTextSyncEngine,
|
|
4592
5706
|
assertQueryReady,
|
|
5707
|
+
chunkBlob,
|
|
5708
|
+
collectBlobGarbage,
|
|
4593
5709
|
collectOperationsAheadOfServer,
|
|
4594
5710
|
compactOperationLog,
|
|
4595
5711
|
computeAckCompactionWatermark,
|
|
5712
|
+
createChunkPortPair,
|
|
5713
|
+
createMemoryServerBlobStore,
|
|
5714
|
+
createOpfsBlobDirectory,
|
|
5715
|
+
createOpfsBlobStore,
|
|
5716
|
+
createRemoteChunkProvider,
|
|
4596
5717
|
createRichTextController,
|
|
4597
5718
|
decodeAuditExport,
|
|
4598
5719
|
decodeRichtext,
|
|
4599
5720
|
deserializeVersionVectorFromMeta,
|
|
4600
5721
|
encodeRichtext,
|
|
4601
5722
|
exportBackup,
|
|
5723
|
+
extractBlobRefs,
|
|
5724
|
+
fetchBlobManifest,
|
|
4602
5725
|
getSharedQueryStoreCache,
|
|
4603
5726
|
mergeVersionVectors,
|
|
5727
|
+
parseBlobManifest,
|
|
4604
5728
|
persistedAuditTraceFromEvent,
|
|
4605
5729
|
pluralize,
|
|
5730
|
+
prepareBlobForSend,
|
|
5731
|
+
putBlobForTransfer,
|
|
4606
5732
|
readAuditExportManifest,
|
|
4607
5733
|
readBackupManifest,
|
|
5734
|
+
reassembleBlob,
|
|
5735
|
+
receiveBlob,
|
|
5736
|
+
resolveBlobManifest,
|
|
4608
5737
|
restoreBackup,
|
|
4609
5738
|
richtextStatesEqual,
|
|
4610
5739
|
richtextToPlainText,
|
|
5740
|
+
serializeBlobManifest,
|
|
4611
5741
|
serializeVersionVectorToMeta,
|
|
5742
|
+
serveBlobChunks,
|
|
4612
5743
|
singularize,
|
|
5744
|
+
toServerBlobCallbacks,
|
|
4613
5745
|
validateStateTransition,
|
|
4614
5746
|
validateUpdateStateMachine,
|
|
4615
5747
|
verifyAuditExportChecksum,
|