@korajs/core 0.6.0 → 1.0.0-beta.0
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/bindings/index.d.cts +6 -4
- package/dist/bindings/index.d.ts +6 -4
- package/dist/{build-scope-map-B1CcncZN.d.cts → build-scope-map-BIeawJzC.d.cts} +1 -1
- package/dist/{build-scope-map-BmgAT82h.d.ts → build-scope-map-DOf4JLTh.d.ts} +1 -1
- package/dist/{chunk-3VIOZT7D.js → chunk-5IICSH6H.js} +150 -7
- package/dist/chunk-5IICSH6H.js.map +1 -0
- package/dist/{events-D-FWfFg8.d.cts → events-BynBOsO3.d.cts} +13 -1
- package/dist/{events-D-FWfFg8.d.ts → events-BynBOsO3.d.ts} +13 -1
- package/dist/index.cjs +274 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +73 -5
- package/dist/index.d.ts +73 -5
- package/dist/index.js +123 -3
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs +142 -17
- package/dist/internal.cjs.map +1 -1
- package/dist/internal.d.cts +2 -2
- package/dist/internal.d.ts +2 -2
- package/dist/internal.js +1 -1
- package/dist/operation-BpZlYSpe.d.cts +164 -0
- package/dist/operation-D5WOZYvy.d.ts +164 -0
- package/package.json +1 -1
- package/dist/chunk-3VIOZT7D.js.map +0 -1
- package/dist/operation-3-ZJJf3w.d.cts +0 -94
- package/dist/operation-CygOwIjZ.d.ts +0 -94
package/dist/index.cjs
CHANGED
|
@@ -30,21 +30,26 @@ __export(index_exports, {
|
|
|
30
30
|
EnumFieldBuilder: () => EnumFieldBuilder,
|
|
31
31
|
FieldBuilder: () => FieldBuilder,
|
|
32
32
|
HybridLogicalClock: () => HybridLogicalClock,
|
|
33
|
+
InvalidTimestampError: () => InvalidTimestampError,
|
|
33
34
|
KORA_ERROR_FIX_SUGGESTIONS: () => KORA_ERROR_FIX_SUGGESTIONS,
|
|
34
35
|
KoraError: () => KoraError,
|
|
36
|
+
MAX_LOGICAL: () => MAX_LOGICAL,
|
|
35
37
|
MERGE_STRATEGIES: () => MERGE_STRATEGIES,
|
|
36
38
|
MergeConflictError: () => MergeConflictError,
|
|
37
39
|
MigrationBuilder: () => MigrationBuilder,
|
|
38
40
|
MigrationRollbackError: () => MigrationRollbackError,
|
|
39
41
|
OperationError: () => OperationError,
|
|
42
|
+
RemoteClockDriftError: () => RemoteClockDriftError,
|
|
40
43
|
RollbackBuilder: () => RollbackBuilder,
|
|
41
44
|
SchemaValidationError: () => SchemaValidationError,
|
|
42
45
|
StorageError: () => StorageError,
|
|
43
46
|
SyncError: () => SyncError,
|
|
44
47
|
advanceVector: () => advanceVector,
|
|
45
48
|
applyOperationTransforms: () => applyOperationTransforms,
|
|
49
|
+
base64ToBytes: () => base64ToBytes,
|
|
46
50
|
buildScopeMap: () => buildScopeMap,
|
|
47
51
|
buildStateMachineConstraints: () => buildStateMachineConstraints,
|
|
52
|
+
bytesToBase64: () => bytesToBase64,
|
|
48
53
|
canAutoRollback: () => canAutoRollback,
|
|
49
54
|
collectSchemaScopeFields: () => collectSchemaScopeFields,
|
|
50
55
|
collectSchemaScopeValueKeys: () => collectSchemaScopeValueKeys,
|
|
@@ -52,11 +57,13 @@ __export(index_exports, {
|
|
|
52
57
|
createOperation: () => createOperation,
|
|
53
58
|
createReversibleMigration: () => createReversibleMigration,
|
|
54
59
|
createVersionVector: () => createVersionVector,
|
|
60
|
+
decodeBytesFromOpData: () => decodeBytesFromOpData,
|
|
55
61
|
defaultApplyFailureReason: () => defaultApplyFailureReason,
|
|
56
62
|
defaultSequenceFormat: () => defaultSequenceFormat,
|
|
57
63
|
defineSchema: () => defineSchema,
|
|
58
64
|
deserializeVector: () => deserializeVector,
|
|
59
65
|
dominates: () => dominates,
|
|
66
|
+
encodeBytesForOpData: () => encodeBytesForOpData,
|
|
60
67
|
extractScopeValuesFromClaims: () => extractScopeValuesFromClaims,
|
|
61
68
|
extractTimestamp: () => extractTimestamp,
|
|
62
69
|
formatSequenceValue: () => formatSequenceValue,
|
|
@@ -72,6 +79,8 @@ __export(index_exports, {
|
|
|
72
79
|
isApplyFailure: () => isApplyFailure,
|
|
73
80
|
isAtomicOp: () => isAtomicOp,
|
|
74
81
|
isCollectionSyncScoped: () => isCollectionSyncScoped,
|
|
82
|
+
isKoraBytesValue: () => isKoraBytesValue,
|
|
83
|
+
isLegacyNumericByteObject: () => isLegacyNumericByteObject,
|
|
75
84
|
isValidOperation: () => isValidOperation,
|
|
76
85
|
isValidUUIDv7: () => isValidUUIDv7,
|
|
77
86
|
mergeVectors: () => mergeVectors,
|
|
@@ -108,6 +117,7 @@ var KORA_ERROR_FIX_SUGGESTIONS = {
|
|
|
108
117
|
SYNC_ERROR: "Verify the sync server URL, auth token, and that the server accepts your schema version.",
|
|
109
118
|
STORAGE_ERROR: "Confirm the storage adapter is supported in this environment and the database path is writable.",
|
|
110
119
|
CLOCK_DRIFT: "Sync device wall-clock time (NTP). Kora refuses new timestamps when drift exceeds five minutes.",
|
|
120
|
+
INVALID_TIMESTAMP_FIELDS: "HLC timestamps need non-negative integer wallTime (< 10^15) and logical (<= 99999). Generate timestamps through HybridLogicalClock instead of building them by hand.",
|
|
111
121
|
PERSISTENCE_ERROR: "IndexedDB persistence failed; check browser storage settings and available disk quota.",
|
|
112
122
|
MISSING_WORKER_URL: "Pass store.workerUrl pointing to sqlite-wasm-worker.js (see create-kora-app templates).",
|
|
113
123
|
INVALID_SYNC_URL: "Use ws:// or wss:// for WebSocket transport, or http(s):// for HTTP long-polling.",
|
|
@@ -186,6 +196,31 @@ var AppNotReadyError = class extends KoraError {
|
|
|
186
196
|
this.name = "AppNotReadyError";
|
|
187
197
|
}
|
|
188
198
|
};
|
|
199
|
+
var RemoteClockDriftError = class extends KoraError {
|
|
200
|
+
constructor(remoteWallTime, localReferenceTime) {
|
|
201
|
+
const aheadSeconds = Math.round((remoteWallTime - localReferenceTime) / 1e3);
|
|
202
|
+
super(
|
|
203
|
+
`Rejected remote timestamp ${aheadSeconds}s ahead of local reference time. The sending device's clock is set too far in the future.`,
|
|
204
|
+
"REMOTE_CLOCK_DRIFT",
|
|
205
|
+
{ remoteWallTime, localReferenceTime, aheadSeconds }
|
|
206
|
+
);
|
|
207
|
+
this.remoteWallTime = remoteWallTime;
|
|
208
|
+
this.localReferenceTime = localReferenceTime;
|
|
209
|
+
this.name = "RemoteClockDriftError";
|
|
210
|
+
}
|
|
211
|
+
remoteWallTime;
|
|
212
|
+
localReferenceTime;
|
|
213
|
+
};
|
|
214
|
+
var InvalidTimestampError = class extends KoraError {
|
|
215
|
+
constructor(message, wallTime, logical) {
|
|
216
|
+
super(message, "INVALID_TIMESTAMP_FIELDS", { wallTime, logical });
|
|
217
|
+
this.wallTime = wallTime;
|
|
218
|
+
this.logical = logical;
|
|
219
|
+
this.name = "InvalidTimestampError";
|
|
220
|
+
}
|
|
221
|
+
wallTime;
|
|
222
|
+
logical;
|
|
223
|
+
};
|
|
189
224
|
var ClockDriftError = class extends KoraError {
|
|
190
225
|
constructor(currentHlcTime, physicalTime) {
|
|
191
226
|
const driftSeconds = Math.round((currentHlcTime - physicalTime) / 1e3);
|
|
@@ -204,24 +239,51 @@ var ClockDriftError = class extends KoraError {
|
|
|
204
239
|
|
|
205
240
|
// src/clock/hlc.ts
|
|
206
241
|
var systemTimeSource = { now: () => Date.now() };
|
|
242
|
+
var MAX_LOGICAL = 99999;
|
|
243
|
+
var LOGICAL_MODULUS = MAX_LOGICAL + 1;
|
|
244
|
+
var WALL_TIME_LIMIT = 10 ** 15;
|
|
207
245
|
var DRIFT_WARN_MS = 6e4;
|
|
208
246
|
var DRIFT_ERROR_MS = 5 * 6e4;
|
|
209
|
-
var
|
|
210
|
-
|
|
247
|
+
var MAX_REMOTE_FUTURE_MS = 5 * 6e4;
|
|
248
|
+
var HybridLogicalClock = class _HybridLogicalClock {
|
|
249
|
+
constructor(nodeId, timeSource = systemTimeSource, onDriftWarning, onDriftError) {
|
|
211
250
|
this.nodeId = nodeId;
|
|
212
251
|
this.timeSource = timeSource;
|
|
213
252
|
this.onDriftWarning = onDriftWarning;
|
|
253
|
+
this.onDriftError = onDriftError;
|
|
214
254
|
}
|
|
215
255
|
nodeId;
|
|
216
256
|
timeSource;
|
|
217
257
|
onDriftWarning;
|
|
258
|
+
onDriftError;
|
|
218
259
|
wallTime = 0;
|
|
219
260
|
logical = 0;
|
|
261
|
+
referenceOffsetMs = null;
|
|
262
|
+
/**
|
|
263
|
+
* Records the known offset between an external time reference (usually the
|
|
264
|
+
* sync server, learned at handshake) and this device's physical clock:
|
|
265
|
+
* `referenceTime - localPhysicalTime`. Once set, drift evaluation and remote
|
|
266
|
+
* timestamp validation are performed against reference-corrected time, so a
|
|
267
|
+
* device with a wrong local clock still validates remote timestamps correctly.
|
|
268
|
+
*/
|
|
269
|
+
setReferenceOffset(offsetMs) {
|
|
270
|
+
this.referenceOffsetMs = offsetMs;
|
|
271
|
+
}
|
|
272
|
+
getReferenceOffset() {
|
|
273
|
+
return this.referenceOffsetMs;
|
|
274
|
+
}
|
|
275
|
+
/** Physical time corrected by the known reference offset, when available. */
|
|
276
|
+
effectiveTime(physicalTime) {
|
|
277
|
+
return physicalTime + (this.referenceOffsetMs ?? 0);
|
|
278
|
+
}
|
|
220
279
|
/**
|
|
221
280
|
* Generate a new timestamp for a local event.
|
|
222
281
|
* Guarantees monotonicity: each call returns a timestamp strictly greater than the previous.
|
|
223
282
|
*
|
|
224
|
-
*
|
|
283
|
+
* Never throws and never blocks a local write: if the physical clock has
|
|
284
|
+
* fallen behind the HLC (e.g. the user corrected a fast clock), the HLC
|
|
285
|
+
* freezes wallTime and advances the logical counter, and drift is reported
|
|
286
|
+
* through the onDriftWarning / onDriftError callbacks instead.
|
|
225
287
|
*/
|
|
226
288
|
now() {
|
|
227
289
|
const physicalTime = this.timeSource.now();
|
|
@@ -232,17 +294,40 @@ var HybridLogicalClock = class {
|
|
|
232
294
|
} else {
|
|
233
295
|
this.logical++;
|
|
234
296
|
}
|
|
297
|
+
this.carryLogicalOverflow();
|
|
235
298
|
return { wallTime: this.wallTime, logical: this.logical, nodeId: this.nodeId };
|
|
236
299
|
}
|
|
237
300
|
/**
|
|
238
301
|
* Update clock on receiving a remote timestamp.
|
|
239
302
|
* Merges the remote clock state with the local state to maintain causal ordering.
|
|
240
303
|
*
|
|
241
|
-
* @throws {
|
|
304
|
+
* @throws {InvalidTimestampError} If the remote timestamp has non-integer or
|
|
305
|
+
* negative wallTime/logical, or logical beyond {@link MAX_LOGICAL}. Checked
|
|
306
|
+
* BEFORE any state change, so malformed input cannot corrupt this clock.
|
|
307
|
+
* @throws {RemoteClockDriftError} If the remote timestamp is more than 5 minutes
|
|
308
|
+
* ahead of reference-corrected local time. Validation happens BEFORE any state
|
|
309
|
+
* is adopted, so a rejected timestamp cannot poison this clock. When no
|
|
310
|
+
* reference offset is known and this clock is uninitialized (cold start with
|
|
311
|
+
* a possibly-wrong local clock), validation is skipped, matching the previous
|
|
312
|
+
* cold-start behavior but now without a corrupting failure mode afterward.
|
|
242
313
|
*/
|
|
243
314
|
receive(remote) {
|
|
244
315
|
const physicalTime = this.timeSource.now();
|
|
245
316
|
const wasColdStart = this.wallTime === 0;
|
|
317
|
+
if (!Number.isInteger(remote.wallTime) || remote.wallTime < 0 || !Number.isInteger(remote.logical) || remote.logical < 0 || remote.logical > MAX_LOGICAL) {
|
|
318
|
+
throw new InvalidTimestampError(
|
|
319
|
+
`Rejected remote HLC timestamp with invalid fields (wallTime=${remote.wallTime}, logical=${remote.logical}). wallTime and logical must be non-negative integers and logical must not exceed ${MAX_LOGICAL}.`,
|
|
320
|
+
remote.wallTime,
|
|
321
|
+
remote.logical
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
const canValidate = this.referenceOffsetMs !== null || !wasColdStart;
|
|
325
|
+
if (canValidate) {
|
|
326
|
+
const reference = this.effectiveTime(physicalTime);
|
|
327
|
+
if (remote.wallTime > reference + MAX_REMOTE_FUTURE_MS) {
|
|
328
|
+
throw new RemoteClockDriftError(remote.wallTime, reference);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
246
331
|
if (physicalTime > this.wallTime && physicalTime > remote.wallTime) {
|
|
247
332
|
this.wallTime = physicalTime;
|
|
248
333
|
this.logical = 0;
|
|
@@ -254,11 +339,54 @@ var HybridLogicalClock = class {
|
|
|
254
339
|
} else {
|
|
255
340
|
this.logical++;
|
|
256
341
|
}
|
|
342
|
+
this.carryLogicalOverflow();
|
|
257
343
|
if (!wasColdStart) {
|
|
258
344
|
this.checkDrift(physicalTime);
|
|
259
345
|
}
|
|
260
346
|
return { wallTime: this.wallTime, logical: this.logical, nodeId: this.nodeId };
|
|
261
347
|
}
|
|
348
|
+
/**
|
|
349
|
+
* Advance this clock to at least the given timestamp.
|
|
350
|
+
*
|
|
351
|
+
* Used after a timestamp rebase rewrites unsynced operations: future `now()`
|
|
352
|
+
* timestamps must sort after every rebased operation, otherwise a write issued
|
|
353
|
+
* immediately after the rebase could interleave with (or precede) rebased ops
|
|
354
|
+
* and break the log's total order. Never moves the clock backward — a
|
|
355
|
+
* timestamp at or before the current state is a no-op, preserving the
|
|
356
|
+
* monotonicity guarantee of `now()`.
|
|
357
|
+
*
|
|
358
|
+
* Inputs with logical > MAX_LOGICAL are normalized deterministically by
|
|
359
|
+
* carrying the excess into wallTime, so the adopted state always leaves room
|
|
360
|
+
* for the next increment inside the serializable range.
|
|
361
|
+
*/
|
|
362
|
+
advanceTo(ts) {
|
|
363
|
+
const normalized = {
|
|
364
|
+
wallTime: ts.wallTime + Math.floor(ts.logical / LOGICAL_MODULUS),
|
|
365
|
+
logical: ts.logical % LOGICAL_MODULUS,
|
|
366
|
+
nodeId: ts.nodeId
|
|
367
|
+
};
|
|
368
|
+
const current = {
|
|
369
|
+
wallTime: this.wallTime,
|
|
370
|
+
logical: this.logical,
|
|
371
|
+
nodeId: this.nodeId
|
|
372
|
+
};
|
|
373
|
+
if (_HybridLogicalClock.compare(normalized, current) > 0) {
|
|
374
|
+
this.wallTime = normalized.wallTime;
|
|
375
|
+
this.logical = normalized.logical;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Carry the logical counter into wallTime when an increment pushed it past
|
|
380
|
+
* MAX_LOGICAL. Bumping wallTime by 1ms keeps the timestamp strictly greater
|
|
381
|
+
* than everything issued before (monotonicity) while keeping the logical
|
|
382
|
+
* counter inside the 5-digit slot the serialized form depends on.
|
|
383
|
+
*/
|
|
384
|
+
carryLogicalOverflow() {
|
|
385
|
+
if (this.logical > MAX_LOGICAL) {
|
|
386
|
+
this.wallTime += 1;
|
|
387
|
+
this.logical = 0;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
262
390
|
/**
|
|
263
391
|
* Compare two timestamps. Returns negative if a < b, positive if a > b, zero if equal.
|
|
264
392
|
* Total order: wallTime first, then logical, then nodeId (lexicographic).
|
|
@@ -273,8 +401,23 @@ var HybridLogicalClock = class {
|
|
|
273
401
|
/**
|
|
274
402
|
* Serialize an HLC timestamp to a string that sorts lexicographically.
|
|
275
403
|
* Format: zero-padded wallTime:logical:nodeId
|
|
404
|
+
*
|
|
405
|
+
* @throws {InvalidTimestampError} If wallTime does not fit 15 digits or
|
|
406
|
+
* logical does not fit 5 digits (or either is negative/non-integer). Such a
|
|
407
|
+
* value would overflow its zero-padded slot and the serialized string would
|
|
408
|
+
* no longer sort in the same order as {@link HybridLogicalClock.compare} —
|
|
409
|
+
* silently corrupting every LWW comparison on stored `_version` columns.
|
|
410
|
+
* Internal clocks can no longer produce such values (the logical counter
|
|
411
|
+
* carries into wallTime); this guards against hand-built timestamps.
|
|
276
412
|
*/
|
|
277
413
|
static serialize(ts) {
|
|
414
|
+
if (!Number.isInteger(ts.wallTime) || ts.wallTime < 0 || ts.wallTime >= WALL_TIME_LIMIT || !Number.isInteger(ts.logical) || ts.logical < 0 || ts.logical > MAX_LOGICAL) {
|
|
415
|
+
throw new InvalidTimestampError(
|
|
416
|
+
`Cannot serialize HLC timestamp (wallTime=${ts.wallTime}, logical=${ts.logical}): wallTime must be an integer in [0, 10^15) and logical an integer in [0, ${MAX_LOGICAL}]. Values outside these ranges overflow the zero-padded serialized form, which must sort lexicographically identically to HybridLogicalClock.compare.`,
|
|
417
|
+
ts.wallTime,
|
|
418
|
+
ts.logical
|
|
419
|
+
);
|
|
420
|
+
}
|
|
278
421
|
const wall = ts.wallTime.toString().padStart(15, "0");
|
|
279
422
|
const log = ts.logical.toString().padStart(5, "0");
|
|
280
423
|
return `${wall}:${log}:${ts.nodeId}`;
|
|
@@ -294,10 +437,16 @@ var HybridLogicalClock = class {
|
|
|
294
437
|
nodeId: parts.slice(2).join(":")
|
|
295
438
|
};
|
|
296
439
|
}
|
|
440
|
+
/**
|
|
441
|
+
* Reports drift between the HLC and (reference-corrected) physical time.
|
|
442
|
+
* Reporting only: local timestamp generation is never blocked, because the
|
|
443
|
+
* user's data always outranks the quality of its timestamps.
|
|
444
|
+
*/
|
|
297
445
|
checkDrift(physicalTime) {
|
|
298
|
-
const drift = this.wallTime - physicalTime;
|
|
446
|
+
const drift = this.wallTime - this.effectiveTime(physicalTime);
|
|
299
447
|
if (drift > DRIFT_ERROR_MS) {
|
|
300
|
-
|
|
448
|
+
this.onDriftError?.(drift);
|
|
449
|
+
return;
|
|
301
450
|
}
|
|
302
451
|
if (drift > DRIFT_WARN_MS) {
|
|
303
452
|
this.onDriftWarning?.(drift);
|
|
@@ -680,6 +829,106 @@ function toAtomicOp(sentinel) {
|
|
|
680
829
|
return { type: sentinel.type, value: sentinel.value };
|
|
681
830
|
}
|
|
682
831
|
|
|
832
|
+
// src/operations/op-data-binary.ts
|
|
833
|
+
function isKoraBytesValue(value) {
|
|
834
|
+
if (typeof value !== "object" || value === null) {
|
|
835
|
+
return false;
|
|
836
|
+
}
|
|
837
|
+
const record = value;
|
|
838
|
+
return Object.keys(record).length === 1 && typeof record.$koraBytes === "string";
|
|
839
|
+
}
|
|
840
|
+
function isLegacyNumericByteObject(value) {
|
|
841
|
+
if (typeof value !== "object" || value === null || ArrayBuffer.isView(value)) {
|
|
842
|
+
return false;
|
|
843
|
+
}
|
|
844
|
+
const record = value;
|
|
845
|
+
const keys = Object.keys(record);
|
|
846
|
+
if (keys.length === 0) {
|
|
847
|
+
return false;
|
|
848
|
+
}
|
|
849
|
+
for (let i = 0; i < keys.length; i++) {
|
|
850
|
+
const byte = record[String(i)];
|
|
851
|
+
if (typeof byte !== "number" || !Number.isInteger(byte) || byte < 0 || byte > 255) {
|
|
852
|
+
return false;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
return true;
|
|
856
|
+
}
|
|
857
|
+
var BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
858
|
+
var BASE64_REVERSE = new Map(
|
|
859
|
+
Array.from(BASE64_ALPHABET, (char, index) => [char, index])
|
|
860
|
+
);
|
|
861
|
+
function bytesToBase64(bytes) {
|
|
862
|
+
let out = "";
|
|
863
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
864
|
+
const b0 = bytes[i] ?? 0;
|
|
865
|
+
const b1 = bytes[i + 1] ?? 0;
|
|
866
|
+
const b2 = bytes[i + 2] ?? 0;
|
|
867
|
+
const triple = b0 << 16 | b1 << 8 | b2;
|
|
868
|
+
out += BASE64_ALPHABET[triple >> 18 & 63] ?? "";
|
|
869
|
+
out += BASE64_ALPHABET[triple >> 12 & 63] ?? "";
|
|
870
|
+
out += i + 1 < bytes.length ? BASE64_ALPHABET[triple >> 6 & 63] ?? "" : "=";
|
|
871
|
+
out += i + 2 < bytes.length ? BASE64_ALPHABET[triple & 63] ?? "" : "=";
|
|
872
|
+
}
|
|
873
|
+
return out;
|
|
874
|
+
}
|
|
875
|
+
function base64ToBytes(base64) {
|
|
876
|
+
const cleaned = base64.replace(/=+$/, "");
|
|
877
|
+
const out = new Uint8Array(Math.floor(cleaned.length * 6 / 8));
|
|
878
|
+
let buffer = 0;
|
|
879
|
+
let bits = 0;
|
|
880
|
+
let index = 0;
|
|
881
|
+
for (const char of cleaned) {
|
|
882
|
+
const value = BASE64_REVERSE.get(char);
|
|
883
|
+
if (value === void 0) {
|
|
884
|
+
throw new OperationError(`Invalid base64 character "${char}" in tagged binary value.`, {
|
|
885
|
+
char
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
buffer = buffer << 6 | value;
|
|
889
|
+
bits += 6;
|
|
890
|
+
if (bits >= 8) {
|
|
891
|
+
bits -= 8;
|
|
892
|
+
out[index] = buffer >> bits & 255;
|
|
893
|
+
index += 1;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
return out;
|
|
897
|
+
}
|
|
898
|
+
function encodeBytesForOpData(value) {
|
|
899
|
+
if (typeof value === "string") {
|
|
900
|
+
return value;
|
|
901
|
+
}
|
|
902
|
+
const bytes = value instanceof Uint8Array ? value : new Uint8Array(value);
|
|
903
|
+
return { $koraBytes: bytesToBase64(bytes) };
|
|
904
|
+
}
|
|
905
|
+
function decodeBytesFromOpData(value) {
|
|
906
|
+
if (typeof value === "string") {
|
|
907
|
+
return value;
|
|
908
|
+
}
|
|
909
|
+
if (value instanceof Uint8Array) {
|
|
910
|
+
return value;
|
|
911
|
+
}
|
|
912
|
+
if (value instanceof ArrayBuffer) {
|
|
913
|
+
return new Uint8Array(value);
|
|
914
|
+
}
|
|
915
|
+
if (isKoraBytesValue(value)) {
|
|
916
|
+
return base64ToBytes(value.$koraBytes);
|
|
917
|
+
}
|
|
918
|
+
if (isLegacyNumericByteObject(value)) {
|
|
919
|
+
const keys = Object.keys(value);
|
|
920
|
+
const bytes = new Uint8Array(keys.length);
|
|
921
|
+
for (let i = 0; i < keys.length; i++) {
|
|
922
|
+
bytes[i] = value[String(i)] ?? 0;
|
|
923
|
+
}
|
|
924
|
+
return bytes;
|
|
925
|
+
}
|
|
926
|
+
throw new OperationError(
|
|
927
|
+
"Binary op-data value must be a string, Uint8Array, ArrayBuffer, tagged { $koraBytes } object, or legacy numeric-key byte object.",
|
|
928
|
+
{ receivedType: typeof value }
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
|
|
683
932
|
// src/scopes/sync-scope-bindings.ts
|
|
684
933
|
function hasSchemaSyncRules(schema) {
|
|
685
934
|
return schema.sync !== void 0 && Object.keys(schema.sync).length > 0;
|
|
@@ -1109,6 +1358,7 @@ function generateSQL(collectionName, collection, relations) {
|
|
|
1109
1358
|
columns.push("_created_at INTEGER NOT NULL");
|
|
1110
1359
|
columns.push("_updated_at INTEGER NOT NULL");
|
|
1111
1360
|
columns.push("_version TEXT NOT NULL DEFAULT ''");
|
|
1361
|
+
columns.push("_field_versions TEXT NOT NULL DEFAULT '{}'");
|
|
1112
1362
|
columns.push("_deleted INTEGER NOT NULL DEFAULT 0");
|
|
1113
1363
|
statements.push(`CREATE TABLE IF NOT EXISTS ${collectionName} (
|
|
1114
1364
|
${columns.join(",\n ")}
|
|
@@ -1121,6 +1371,10 @@ ALTER TABLE ${collectionName} ADD COLUMN ${colDef}`);
|
|
|
1121
1371
|
statements.push(
|
|
1122
1372
|
`--kora:safe-alter
|
|
1123
1373
|
ALTER TABLE ${collectionName} ADD COLUMN _version TEXT NOT NULL DEFAULT ''`
|
|
1374
|
+
);
|
|
1375
|
+
statements.push(
|
|
1376
|
+
`--kora:safe-alter
|
|
1377
|
+
ALTER TABLE ${collectionName} ADD COLUMN _field_versions TEXT NOT NULL DEFAULT '{}'`
|
|
1124
1378
|
);
|
|
1125
1379
|
for (const indexField of collection.indexes) {
|
|
1126
1380
|
statements.push(
|
|
@@ -1148,6 +1402,9 @@ ALTER TABLE ${collectionName} ADD COLUMN _version TEXT NOT NULL DEFAULT ''`
|
|
|
1148
1402
|
schema_version INTEGER NOT NULL
|
|
1149
1403
|
)`
|
|
1150
1404
|
);
|
|
1405
|
+
statements.push(
|
|
1406
|
+
`CREATE INDEX IF NOT EXISTS idx_kora_ops_${collectionName}_record_id ON _kora_ops_${collectionName} (record_id)`
|
|
1407
|
+
);
|
|
1151
1408
|
return statements;
|
|
1152
1409
|
}
|
|
1153
1410
|
function generateFullDDL(schema) {
|
|
@@ -1613,9 +1870,9 @@ function validateFieldValue(collection, fieldName, descriptor, value) {
|
|
|
1613
1870
|
break;
|
|
1614
1871
|
}
|
|
1615
1872
|
case "richtext": {
|
|
1616
|
-
if (!(value instanceof Uint8Array) && typeof value !== "string") {
|
|
1873
|
+
if (!(value instanceof Uint8Array) && !(value instanceof ArrayBuffer) && typeof value !== "string") {
|
|
1617
1874
|
throw new SchemaValidationError(
|
|
1618
|
-
`Field "${fieldName}" in collection "${collection}" must be a Uint8Array or string for richtext, got ${typeof value}`,
|
|
1875
|
+
`Field "${fieldName}" in collection "${collection}" must be a Uint8Array, ArrayBuffer, or string for richtext, got ${typeof value}`,
|
|
1619
1876
|
{
|
|
1620
1877
|
collection,
|
|
1621
1878
|
field: fieldName,
|
|
@@ -2479,21 +2736,26 @@ function generateProtoDefinitions(schema) {
|
|
|
2479
2736
|
EnumFieldBuilder,
|
|
2480
2737
|
FieldBuilder,
|
|
2481
2738
|
HybridLogicalClock,
|
|
2739
|
+
InvalidTimestampError,
|
|
2482
2740
|
KORA_ERROR_FIX_SUGGESTIONS,
|
|
2483
2741
|
KoraError,
|
|
2742
|
+
MAX_LOGICAL,
|
|
2484
2743
|
MERGE_STRATEGIES,
|
|
2485
2744
|
MergeConflictError,
|
|
2486
2745
|
MigrationBuilder,
|
|
2487
2746
|
MigrationRollbackError,
|
|
2488
2747
|
OperationError,
|
|
2748
|
+
RemoteClockDriftError,
|
|
2489
2749
|
RollbackBuilder,
|
|
2490
2750
|
SchemaValidationError,
|
|
2491
2751
|
StorageError,
|
|
2492
2752
|
SyncError,
|
|
2493
2753
|
advanceVector,
|
|
2494
2754
|
applyOperationTransforms,
|
|
2755
|
+
base64ToBytes,
|
|
2495
2756
|
buildScopeMap,
|
|
2496
2757
|
buildStateMachineConstraints,
|
|
2758
|
+
bytesToBase64,
|
|
2497
2759
|
canAutoRollback,
|
|
2498
2760
|
collectSchemaScopeFields,
|
|
2499
2761
|
collectSchemaScopeValueKeys,
|
|
@@ -2501,11 +2763,13 @@ function generateProtoDefinitions(schema) {
|
|
|
2501
2763
|
createOperation,
|
|
2502
2764
|
createReversibleMigration,
|
|
2503
2765
|
createVersionVector,
|
|
2766
|
+
decodeBytesFromOpData,
|
|
2504
2767
|
defaultApplyFailureReason,
|
|
2505
2768
|
defaultSequenceFormat,
|
|
2506
2769
|
defineSchema,
|
|
2507
2770
|
deserializeVector,
|
|
2508
2771
|
dominates,
|
|
2772
|
+
encodeBytesForOpData,
|
|
2509
2773
|
extractScopeValuesFromClaims,
|
|
2510
2774
|
extractTimestamp,
|
|
2511
2775
|
formatSequenceValue,
|
|
@@ -2521,6 +2785,8 @@ function generateProtoDefinitions(schema) {
|
|
|
2521
2785
|
isApplyFailure,
|
|
2522
2786
|
isAtomicOp,
|
|
2523
2787
|
isCollectionSyncScoped,
|
|
2788
|
+
isKoraBytesValue,
|
|
2789
|
+
isLegacyNumericByteObject,
|
|
2524
2790
|
isValidOperation,
|
|
2525
2791
|
isValidUUIDv7,
|
|
2526
2792
|
mergeVectors,
|