@korajs/core 0.5.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.cjs +105 -0
- package/dist/bindings/index.cjs.map +1 -0
- package/dist/bindings/index.d.cts +103 -0
- package/dist/bindings/index.d.ts +103 -0
- package/dist/bindings/index.js +78 -0
- package/dist/bindings/index.js.map +1 -0
- package/dist/build-scope-map-BIeawJzC.d.cts +33 -0
- package/dist/build-scope-map-DOf4JLTh.d.ts +33 -0
- package/dist/{chunk-H4FXU5OP.js → chunk-5IICSH6H.js} +159 -7
- package/dist/chunk-5IICSH6H.js.map +1 -0
- package/dist/{events-BeIEDJBW.d.cts → events-BynBOsO3.d.cts} +14 -93
- package/dist/{events-BeIEDJBW.d.ts → events-BynBOsO3.d.ts} +14 -93
- package/dist/index.cjs +284 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +79 -33
- package/dist/index.d.ts +79 -33
- package/dist/index.js +125 -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 +11 -1
- package/dist/chunk-H4FXU5OP.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -22,6 +22,7 @@ var index_exports = {};
|
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
APPLY_FAILURE_CODES: () => APPLY_FAILURE_CODES,
|
|
24
24
|
APPLY_RESULTS: () => APPLY_RESULTS,
|
|
25
|
+
AppNotReadyError: () => AppNotReadyError,
|
|
25
26
|
ArrayFieldBuilder: () => ArrayFieldBuilder,
|
|
26
27
|
CONNECTION_QUALITIES: () => CONNECTION_QUALITIES,
|
|
27
28
|
CausalTracker: () => CausalTracker,
|
|
@@ -29,21 +30,26 @@ __export(index_exports, {
|
|
|
29
30
|
EnumFieldBuilder: () => EnumFieldBuilder,
|
|
30
31
|
FieldBuilder: () => FieldBuilder,
|
|
31
32
|
HybridLogicalClock: () => HybridLogicalClock,
|
|
33
|
+
InvalidTimestampError: () => InvalidTimestampError,
|
|
32
34
|
KORA_ERROR_FIX_SUGGESTIONS: () => KORA_ERROR_FIX_SUGGESTIONS,
|
|
33
35
|
KoraError: () => KoraError,
|
|
36
|
+
MAX_LOGICAL: () => MAX_LOGICAL,
|
|
34
37
|
MERGE_STRATEGIES: () => MERGE_STRATEGIES,
|
|
35
38
|
MergeConflictError: () => MergeConflictError,
|
|
36
39
|
MigrationBuilder: () => MigrationBuilder,
|
|
37
40
|
MigrationRollbackError: () => MigrationRollbackError,
|
|
38
41
|
OperationError: () => OperationError,
|
|
42
|
+
RemoteClockDriftError: () => RemoteClockDriftError,
|
|
39
43
|
RollbackBuilder: () => RollbackBuilder,
|
|
40
44
|
SchemaValidationError: () => SchemaValidationError,
|
|
41
45
|
StorageError: () => StorageError,
|
|
42
46
|
SyncError: () => SyncError,
|
|
43
47
|
advanceVector: () => advanceVector,
|
|
44
48
|
applyOperationTransforms: () => applyOperationTransforms,
|
|
49
|
+
base64ToBytes: () => base64ToBytes,
|
|
45
50
|
buildScopeMap: () => buildScopeMap,
|
|
46
51
|
buildStateMachineConstraints: () => buildStateMachineConstraints,
|
|
52
|
+
bytesToBase64: () => bytesToBase64,
|
|
47
53
|
canAutoRollback: () => canAutoRollback,
|
|
48
54
|
collectSchemaScopeFields: () => collectSchemaScopeFields,
|
|
49
55
|
collectSchemaScopeValueKeys: () => collectSchemaScopeValueKeys,
|
|
@@ -51,11 +57,13 @@ __export(index_exports, {
|
|
|
51
57
|
createOperation: () => createOperation,
|
|
52
58
|
createReversibleMigration: () => createReversibleMigration,
|
|
53
59
|
createVersionVector: () => createVersionVector,
|
|
60
|
+
decodeBytesFromOpData: () => decodeBytesFromOpData,
|
|
54
61
|
defaultApplyFailureReason: () => defaultApplyFailureReason,
|
|
55
62
|
defaultSequenceFormat: () => defaultSequenceFormat,
|
|
56
63
|
defineSchema: () => defineSchema,
|
|
57
64
|
deserializeVector: () => deserializeVector,
|
|
58
65
|
dominates: () => dominates,
|
|
66
|
+
encodeBytesForOpData: () => encodeBytesForOpData,
|
|
59
67
|
extractScopeValuesFromClaims: () => extractScopeValuesFromClaims,
|
|
60
68
|
extractTimestamp: () => extractTimestamp,
|
|
61
69
|
formatSequenceValue: () => formatSequenceValue,
|
|
@@ -71,6 +79,8 @@ __export(index_exports, {
|
|
|
71
79
|
isApplyFailure: () => isApplyFailure,
|
|
72
80
|
isAtomicOp: () => isAtomicOp,
|
|
73
81
|
isCollectionSyncScoped: () => isCollectionSyncScoped,
|
|
82
|
+
isKoraBytesValue: () => isKoraBytesValue,
|
|
83
|
+
isLegacyNumericByteObject: () => isLegacyNumericByteObject,
|
|
74
84
|
isValidOperation: () => isValidOperation,
|
|
75
85
|
isValidUUIDv7: () => isValidUUIDv7,
|
|
76
86
|
mergeVectors: () => mergeVectors,
|
|
@@ -107,6 +117,7 @@ var KORA_ERROR_FIX_SUGGESTIONS = {
|
|
|
107
117
|
SYNC_ERROR: "Verify the sync server URL, auth token, and that the server accepts your schema version.",
|
|
108
118
|
STORAGE_ERROR: "Confirm the storage adapter is supported in this environment and the database path is writable.",
|
|
109
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.",
|
|
110
121
|
PERSISTENCE_ERROR: "IndexedDB persistence failed; check browser storage settings and available disk quota.",
|
|
111
122
|
MISSING_WORKER_URL: "Pass store.workerUrl pointing to sqlite-wasm-worker.js (see create-kora-app templates).",
|
|
112
123
|
INVALID_SYNC_URL: "Use ws:// or wss:// for WebSocket transport, or http(s):// for HTTP long-polling.",
|
|
@@ -177,6 +188,39 @@ var StorageError = class extends KoraError {
|
|
|
177
188
|
this.name = "StorageError";
|
|
178
189
|
}
|
|
179
190
|
};
|
|
191
|
+
var AppNotReadyError = class extends KoraError {
|
|
192
|
+
constructor(detail) {
|
|
193
|
+
super(detail, "APP_NOT_READY", {
|
|
194
|
+
fix: "Await app.ready, or wrap your UI in <KoraProvider app={app}> before calling useQuery()."
|
|
195
|
+
});
|
|
196
|
+
this.name = "AppNotReadyError";
|
|
197
|
+
}
|
|
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
|
+
};
|
|
180
224
|
var ClockDriftError = class extends KoraError {
|
|
181
225
|
constructor(currentHlcTime, physicalTime) {
|
|
182
226
|
const driftSeconds = Math.round((currentHlcTime - physicalTime) / 1e3);
|
|
@@ -195,24 +239,51 @@ var ClockDriftError = class extends KoraError {
|
|
|
195
239
|
|
|
196
240
|
// src/clock/hlc.ts
|
|
197
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;
|
|
198
245
|
var DRIFT_WARN_MS = 6e4;
|
|
199
246
|
var DRIFT_ERROR_MS = 5 * 6e4;
|
|
200
|
-
var
|
|
201
|
-
|
|
247
|
+
var MAX_REMOTE_FUTURE_MS = 5 * 6e4;
|
|
248
|
+
var HybridLogicalClock = class _HybridLogicalClock {
|
|
249
|
+
constructor(nodeId, timeSource = systemTimeSource, onDriftWarning, onDriftError) {
|
|
202
250
|
this.nodeId = nodeId;
|
|
203
251
|
this.timeSource = timeSource;
|
|
204
252
|
this.onDriftWarning = onDriftWarning;
|
|
253
|
+
this.onDriftError = onDriftError;
|
|
205
254
|
}
|
|
206
255
|
nodeId;
|
|
207
256
|
timeSource;
|
|
208
257
|
onDriftWarning;
|
|
258
|
+
onDriftError;
|
|
209
259
|
wallTime = 0;
|
|
210
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
|
+
}
|
|
211
279
|
/**
|
|
212
280
|
* Generate a new timestamp for a local event.
|
|
213
281
|
* Guarantees monotonicity: each call returns a timestamp strictly greater than the previous.
|
|
214
282
|
*
|
|
215
|
-
*
|
|
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.
|
|
216
287
|
*/
|
|
217
288
|
now() {
|
|
218
289
|
const physicalTime = this.timeSource.now();
|
|
@@ -223,17 +294,40 @@ var HybridLogicalClock = class {
|
|
|
223
294
|
} else {
|
|
224
295
|
this.logical++;
|
|
225
296
|
}
|
|
297
|
+
this.carryLogicalOverflow();
|
|
226
298
|
return { wallTime: this.wallTime, logical: this.logical, nodeId: this.nodeId };
|
|
227
299
|
}
|
|
228
300
|
/**
|
|
229
301
|
* Update clock on receiving a remote timestamp.
|
|
230
302
|
* Merges the remote clock state with the local state to maintain causal ordering.
|
|
231
303
|
*
|
|
232
|
-
* @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.
|
|
233
313
|
*/
|
|
234
314
|
receive(remote) {
|
|
235
315
|
const physicalTime = this.timeSource.now();
|
|
236
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
|
+
}
|
|
237
331
|
if (physicalTime > this.wallTime && physicalTime > remote.wallTime) {
|
|
238
332
|
this.wallTime = physicalTime;
|
|
239
333
|
this.logical = 0;
|
|
@@ -245,11 +339,54 @@ var HybridLogicalClock = class {
|
|
|
245
339
|
} else {
|
|
246
340
|
this.logical++;
|
|
247
341
|
}
|
|
342
|
+
this.carryLogicalOverflow();
|
|
248
343
|
if (!wasColdStart) {
|
|
249
344
|
this.checkDrift(physicalTime);
|
|
250
345
|
}
|
|
251
346
|
return { wallTime: this.wallTime, logical: this.logical, nodeId: this.nodeId };
|
|
252
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
|
+
}
|
|
253
390
|
/**
|
|
254
391
|
* Compare two timestamps. Returns negative if a < b, positive if a > b, zero if equal.
|
|
255
392
|
* Total order: wallTime first, then logical, then nodeId (lexicographic).
|
|
@@ -264,8 +401,23 @@ var HybridLogicalClock = class {
|
|
|
264
401
|
/**
|
|
265
402
|
* Serialize an HLC timestamp to a string that sorts lexicographically.
|
|
266
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.
|
|
267
412
|
*/
|
|
268
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
|
+
}
|
|
269
421
|
const wall = ts.wallTime.toString().padStart(15, "0");
|
|
270
422
|
const log = ts.logical.toString().padStart(5, "0");
|
|
271
423
|
return `${wall}:${log}:${ts.nodeId}`;
|
|
@@ -285,10 +437,16 @@ var HybridLogicalClock = class {
|
|
|
285
437
|
nodeId: parts.slice(2).join(":")
|
|
286
438
|
};
|
|
287
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
|
+
*/
|
|
288
445
|
checkDrift(physicalTime) {
|
|
289
|
-
const drift = this.wallTime - physicalTime;
|
|
446
|
+
const drift = this.wallTime - this.effectiveTime(physicalTime);
|
|
290
447
|
if (drift > DRIFT_ERROR_MS) {
|
|
291
|
-
|
|
448
|
+
this.onDriftError?.(drift);
|
|
449
|
+
return;
|
|
292
450
|
}
|
|
293
451
|
if (drift > DRIFT_WARN_MS) {
|
|
294
452
|
this.onDriftWarning?.(drift);
|
|
@@ -671,6 +829,106 @@ function toAtomicOp(sentinel) {
|
|
|
671
829
|
return { type: sentinel.type, value: sentinel.value };
|
|
672
830
|
}
|
|
673
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
|
+
|
|
674
932
|
// src/scopes/sync-scope-bindings.ts
|
|
675
933
|
function hasSchemaSyncRules(schema) {
|
|
676
934
|
return schema.sync !== void 0 && Object.keys(schema.sync).length > 0;
|
|
@@ -1100,6 +1358,7 @@ function generateSQL(collectionName, collection, relations) {
|
|
|
1100
1358
|
columns.push("_created_at INTEGER NOT NULL");
|
|
1101
1359
|
columns.push("_updated_at INTEGER NOT NULL");
|
|
1102
1360
|
columns.push("_version TEXT NOT NULL DEFAULT ''");
|
|
1361
|
+
columns.push("_field_versions TEXT NOT NULL DEFAULT '{}'");
|
|
1103
1362
|
columns.push("_deleted INTEGER NOT NULL DEFAULT 0");
|
|
1104
1363
|
statements.push(`CREATE TABLE IF NOT EXISTS ${collectionName} (
|
|
1105
1364
|
${columns.join(",\n ")}
|
|
@@ -1112,6 +1371,10 @@ ALTER TABLE ${collectionName} ADD COLUMN ${colDef}`);
|
|
|
1112
1371
|
statements.push(
|
|
1113
1372
|
`--kora:safe-alter
|
|
1114
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 '{}'`
|
|
1115
1378
|
);
|
|
1116
1379
|
for (const indexField of collection.indexes) {
|
|
1117
1380
|
statements.push(
|
|
@@ -1139,6 +1402,9 @@ ALTER TABLE ${collectionName} ADD COLUMN _version TEXT NOT NULL DEFAULT ''`
|
|
|
1139
1402
|
schema_version INTEGER NOT NULL
|
|
1140
1403
|
)`
|
|
1141
1404
|
);
|
|
1405
|
+
statements.push(
|
|
1406
|
+
`CREATE INDEX IF NOT EXISTS idx_kora_ops_${collectionName}_record_id ON _kora_ops_${collectionName} (record_id)`
|
|
1407
|
+
);
|
|
1142
1408
|
return statements;
|
|
1143
1409
|
}
|
|
1144
1410
|
function generateFullDDL(schema) {
|
|
@@ -1604,9 +1870,9 @@ function validateFieldValue(collection, fieldName, descriptor, value) {
|
|
|
1604
1870
|
break;
|
|
1605
1871
|
}
|
|
1606
1872
|
case "richtext": {
|
|
1607
|
-
if (!(value instanceof Uint8Array) && typeof value !== "string") {
|
|
1873
|
+
if (!(value instanceof Uint8Array) && !(value instanceof ArrayBuffer) && typeof value !== "string") {
|
|
1608
1874
|
throw new SchemaValidationError(
|
|
1609
|
-
`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}`,
|
|
1610
1876
|
{
|
|
1611
1877
|
collection,
|
|
1612
1878
|
field: fieldName,
|
|
@@ -2462,6 +2728,7 @@ function generateProtoDefinitions(schema) {
|
|
|
2462
2728
|
0 && (module.exports = {
|
|
2463
2729
|
APPLY_FAILURE_CODES,
|
|
2464
2730
|
APPLY_RESULTS,
|
|
2731
|
+
AppNotReadyError,
|
|
2465
2732
|
ArrayFieldBuilder,
|
|
2466
2733
|
CONNECTION_QUALITIES,
|
|
2467
2734
|
CausalTracker,
|
|
@@ -2469,21 +2736,26 @@ function generateProtoDefinitions(schema) {
|
|
|
2469
2736
|
EnumFieldBuilder,
|
|
2470
2737
|
FieldBuilder,
|
|
2471
2738
|
HybridLogicalClock,
|
|
2739
|
+
InvalidTimestampError,
|
|
2472
2740
|
KORA_ERROR_FIX_SUGGESTIONS,
|
|
2473
2741
|
KoraError,
|
|
2742
|
+
MAX_LOGICAL,
|
|
2474
2743
|
MERGE_STRATEGIES,
|
|
2475
2744
|
MergeConflictError,
|
|
2476
2745
|
MigrationBuilder,
|
|
2477
2746
|
MigrationRollbackError,
|
|
2478
2747
|
OperationError,
|
|
2748
|
+
RemoteClockDriftError,
|
|
2479
2749
|
RollbackBuilder,
|
|
2480
2750
|
SchemaValidationError,
|
|
2481
2751
|
StorageError,
|
|
2482
2752
|
SyncError,
|
|
2483
2753
|
advanceVector,
|
|
2484
2754
|
applyOperationTransforms,
|
|
2755
|
+
base64ToBytes,
|
|
2485
2756
|
buildScopeMap,
|
|
2486
2757
|
buildStateMachineConstraints,
|
|
2758
|
+
bytesToBase64,
|
|
2487
2759
|
canAutoRollback,
|
|
2488
2760
|
collectSchemaScopeFields,
|
|
2489
2761
|
collectSchemaScopeValueKeys,
|
|
@@ -2491,11 +2763,13 @@ function generateProtoDefinitions(schema) {
|
|
|
2491
2763
|
createOperation,
|
|
2492
2764
|
createReversibleMigration,
|
|
2493
2765
|
createVersionVector,
|
|
2766
|
+
decodeBytesFromOpData,
|
|
2494
2767
|
defaultApplyFailureReason,
|
|
2495
2768
|
defaultSequenceFormat,
|
|
2496
2769
|
defineSchema,
|
|
2497
2770
|
deserializeVector,
|
|
2498
2771
|
dominates,
|
|
2772
|
+
encodeBytesForOpData,
|
|
2499
2773
|
extractScopeValuesFromClaims,
|
|
2500
2774
|
extractTimestamp,
|
|
2501
2775
|
formatSequenceValue,
|
|
@@ -2511,6 +2785,8 @@ function generateProtoDefinitions(schema) {
|
|
|
2511
2785
|
isApplyFailure,
|
|
2512
2786
|
isAtomicOp,
|
|
2513
2787
|
isCollectionSyncScoped,
|
|
2788
|
+
isKoraBytesValue,
|
|
2789
|
+
isLegacyNumericByteObject,
|
|
2514
2790
|
isValidOperation,
|
|
2515
2791
|
isValidUUIDv7,
|
|
2516
2792
|
mergeVectors,
|