@korajs/core 0.0.0-canary-20261008174129

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.
@@ -0,0 +1,1297 @@
1
+ // src/errors/error-fixes.ts
2
+ var KORA_ERROR_FIX_SUGGESTIONS = {
3
+ SCHEMA_VALIDATION: "Review your defineSchema() definition: every collection needs at least one field and a positive version.",
4
+ OPERATION_ERROR: "Check the operation payload matches your schema field types and required fields.",
5
+ MERGE_CONFLICT: "Add a custom resolver for the conflicting field in defineSchema(), or adjust constraint onConflict rules.",
6
+ SYNC_ERROR: "Verify the sync server URL, auth token, and that the server accepts your schema version.",
7
+ STORAGE_ERROR: "Confirm the storage adapter is supported in this environment and the database path is writable.",
8
+ CLOCK_DRIFT: "Sync device wall-clock time (NTP). Kora refuses new timestamps when drift exceeds five minutes.",
9
+ 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.",
10
+ PERSISTENCE_ERROR: "IndexedDB persistence failed; check browser storage settings and available disk quota.",
11
+ MISSING_WORKER_URL: "Pass store.workerUrl pointing to sqlite-wasm-worker.js (see create-kora-app templates).",
12
+ INVALID_SYNC_URL: "Use ws:// or wss:// for WebSocket transport, or http(s):// for HTTP long-polling.",
13
+ APP_NOT_READY: "Await app.ready before querying or mutating collections, or wrap the tree in <KoraProvider app={app}>."
14
+ };
15
+ function getKoraErrorFix(code) {
16
+ return KORA_ERROR_FIX_SUGGESTIONS[code];
17
+ }
18
+
19
+ // src/errors/errors.ts
20
+ var KoraError = class extends Error {
21
+ constructor(message, code, context) {
22
+ super(message);
23
+ this.code = code;
24
+ this.context = context;
25
+ this.name = "KoraError";
26
+ }
27
+ code;
28
+ context;
29
+ /**
30
+ * Actionable hint for resolving this error (from registry or error context).
31
+ */
32
+ get fix() {
33
+ const fromContext = this.context?.fix;
34
+ if (typeof fromContext === "string" && fromContext.length > 0) {
35
+ return fromContext;
36
+ }
37
+ return getKoraErrorFix(this.code);
38
+ }
39
+ };
40
+ var SchemaValidationError = class extends KoraError {
41
+ constructor(message, context) {
42
+ super(message, "SCHEMA_VALIDATION", context);
43
+ this.name = "SchemaValidationError";
44
+ }
45
+ };
46
+ var OperationError = class extends KoraError {
47
+ constructor(message, context) {
48
+ super(message, "OPERATION_ERROR", context);
49
+ this.name = "OperationError";
50
+ }
51
+ };
52
+ var OperationTooLargeError = class extends KoraError {
53
+ constructor(collection, recordId, bytes, maxBytes) {
54
+ super(
55
+ `A write to ${collection}/${recordId} would produce an operation of ${bytes} bytes, more than the ${maxBytes}-byte limit (maxOperationBytes). Nothing was written. Store large content as a blob (t.blob()) or split it across records.`,
56
+ "OPERATION_TOO_LARGE",
57
+ { collection, recordId, bytes, maxBytes }
58
+ );
59
+ this.collection = collection;
60
+ this.recordId = recordId;
61
+ this.bytes = bytes;
62
+ this.maxBytes = maxBytes;
63
+ this.name = "OperationTooLargeError";
64
+ }
65
+ collection;
66
+ recordId;
67
+ bytes;
68
+ maxBytes;
69
+ };
70
+ var MergeConflictError = class extends KoraError {
71
+ constructor(operationA, operationB, field) {
72
+ super(
73
+ `Merge conflict on field "${field}" in collection "${operationA.collection}"`,
74
+ "MERGE_CONFLICT",
75
+ { operationA: operationA.id, operationB: operationB.id, field }
76
+ );
77
+ this.operationA = operationA;
78
+ this.operationB = operationB;
79
+ this.field = field;
80
+ this.name = "MergeConflictError";
81
+ }
82
+ operationA;
83
+ operationB;
84
+ field;
85
+ };
86
+ var SyncError = class extends KoraError {
87
+ constructor(message, context) {
88
+ super(message, "SYNC_ERROR", context);
89
+ this.name = "SyncError";
90
+ }
91
+ };
92
+ var StorageError = class extends KoraError {
93
+ constructor(message, context) {
94
+ super(message, "STORAGE_ERROR", context);
95
+ this.name = "StorageError";
96
+ }
97
+ };
98
+ var AppNotReadyError = class extends KoraError {
99
+ constructor(detail) {
100
+ super(detail, "APP_NOT_READY", {
101
+ fix: "Await app.ready, or wrap your UI in <KoraProvider app={app}> before calling useQuery()."
102
+ });
103
+ this.name = "AppNotReadyError";
104
+ }
105
+ };
106
+ var RemoteClockDriftError = class extends KoraError {
107
+ constructor(remoteWallTime, localReferenceTime) {
108
+ const aheadSeconds = Math.round((remoteWallTime - localReferenceTime) / 1e3);
109
+ super(
110
+ `Rejected remote timestamp ${aheadSeconds}s ahead of local reference time. The sending device's clock is set too far in the future.`,
111
+ "REMOTE_CLOCK_DRIFT",
112
+ { remoteWallTime, localReferenceTime, aheadSeconds }
113
+ );
114
+ this.remoteWallTime = remoteWallTime;
115
+ this.localReferenceTime = localReferenceTime;
116
+ this.name = "RemoteClockDriftError";
117
+ }
118
+ remoteWallTime;
119
+ localReferenceTime;
120
+ };
121
+ var InvalidTimestampError = class extends KoraError {
122
+ constructor(message, wallTime, logical) {
123
+ super(message, "INVALID_TIMESTAMP_FIELDS", { wallTime, logical });
124
+ this.wallTime = wallTime;
125
+ this.logical = logical;
126
+ this.name = "InvalidTimestampError";
127
+ }
128
+ wallTime;
129
+ logical;
130
+ };
131
+ var ClockDriftError = class extends KoraError {
132
+ constructor(currentHlcTime, physicalTime) {
133
+ const driftSeconds = Math.round((currentHlcTime - physicalTime) / 1e3);
134
+ super(
135
+ `Clock drift of ${driftSeconds}s detected. Physical time is behind HLC by more than 5 minutes. This indicates a severe clock issue.`,
136
+ "CLOCK_DRIFT",
137
+ { currentHlcTime, physicalTime, driftSeconds }
138
+ );
139
+ this.currentHlcTime = currentHlcTime;
140
+ this.physicalTime = physicalTime;
141
+ this.name = "ClockDriftError";
142
+ }
143
+ currentHlcTime;
144
+ physicalTime;
145
+ };
146
+
147
+ // src/clock/hlc.ts
148
+ var systemTimeSource = { now: () => Date.now() };
149
+ var MAX_LOGICAL = 99999;
150
+ var LOGICAL_MODULUS = MAX_LOGICAL + 1;
151
+ var WALL_TIME_LIMIT = 10 ** 15;
152
+ var DRIFT_WARN_MS = 6e4;
153
+ var DRIFT_ERROR_MS = 5 * 6e4;
154
+ var MAX_REMOTE_FUTURE_MS = 5 * 6e4;
155
+ var HybridLogicalClock = class _HybridLogicalClock {
156
+ constructor(nodeId, timeSource = systemTimeSource, onDriftWarning, onDriftError) {
157
+ this.nodeId = nodeId;
158
+ this.timeSource = timeSource;
159
+ this.onDriftWarning = onDriftWarning;
160
+ this.onDriftError = onDriftError;
161
+ }
162
+ nodeId;
163
+ timeSource;
164
+ onDriftWarning;
165
+ onDriftError;
166
+ wallTime = 0;
167
+ logical = 0;
168
+ referenceOffsetMs = null;
169
+ /**
170
+ * Records the known offset between an external time reference (usually the
171
+ * sync server, learned at handshake) and this device's physical clock:
172
+ * `referenceTime - localPhysicalTime`. Once set, drift evaluation and remote
173
+ * timestamp validation are performed against reference-corrected time, so a
174
+ * device with a wrong local clock still validates remote timestamps correctly.
175
+ */
176
+ setReferenceOffset(offsetMs) {
177
+ this.referenceOffsetMs = offsetMs;
178
+ }
179
+ getReferenceOffset() {
180
+ return this.referenceOffsetMs;
181
+ }
182
+ /** Physical time corrected by the known reference offset, when available. */
183
+ effectiveTime(physicalTime) {
184
+ return physicalTime + (this.referenceOffsetMs ?? 0);
185
+ }
186
+ /**
187
+ * Generate a new timestamp for a local event.
188
+ * Guarantees monotonicity: each call returns a timestamp strictly greater than the previous.
189
+ *
190
+ * Never throws and never blocks a local write: if the physical clock has
191
+ * fallen behind the HLC (e.g. the user corrected a fast clock), the HLC
192
+ * freezes wallTime and advances the logical counter, and drift is reported
193
+ * through the onDriftWarning / onDriftError callbacks instead.
194
+ */
195
+ now() {
196
+ const physicalTime = this.timeSource.now();
197
+ this.checkDrift(physicalTime);
198
+ if (physicalTime > this.wallTime) {
199
+ this.wallTime = physicalTime;
200
+ this.logical = 0;
201
+ } else {
202
+ this.logical++;
203
+ }
204
+ this.carryLogicalOverflow();
205
+ return { wallTime: this.wallTime, logical: this.logical, nodeId: this.nodeId };
206
+ }
207
+ /**
208
+ * Update clock on receiving a remote timestamp.
209
+ * Merges the remote clock state with the local state to maintain causal ordering.
210
+ *
211
+ * @throws {InvalidTimestampError} If the remote timestamp has non-integer or
212
+ * negative wallTime/logical, or logical beyond {@link MAX_LOGICAL}. Checked
213
+ * BEFORE any state change, so malformed input cannot corrupt this clock.
214
+ * @throws {RemoteClockDriftError} If the remote timestamp is more than 5 minutes
215
+ * ahead of reference-corrected local time. Validation happens BEFORE any state
216
+ * is adopted, so a rejected timestamp cannot poison this clock. When no
217
+ * reference offset is known and this clock is uninitialized (cold start with
218
+ * a possibly-wrong local clock), validation is skipped, matching the previous
219
+ * cold-start behavior but now without a corrupting failure mode afterward.
220
+ */
221
+ receive(remote) {
222
+ const physicalTime = this.timeSource.now();
223
+ const wasColdStart = this.wallTime === 0;
224
+ if (!Number.isInteger(remote.wallTime) || remote.wallTime < 0 || !Number.isInteger(remote.logical) || remote.logical < 0 || remote.logical > MAX_LOGICAL) {
225
+ throw new InvalidTimestampError(
226
+ `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}.`,
227
+ remote.wallTime,
228
+ remote.logical
229
+ );
230
+ }
231
+ const canValidate = this.referenceOffsetMs !== null || !wasColdStart;
232
+ if (canValidate) {
233
+ const reference = this.effectiveTime(physicalTime);
234
+ if (remote.wallTime > reference + MAX_REMOTE_FUTURE_MS) {
235
+ throw new RemoteClockDriftError(remote.wallTime, reference);
236
+ }
237
+ }
238
+ if (physicalTime > this.wallTime && physicalTime > remote.wallTime) {
239
+ this.wallTime = physicalTime;
240
+ this.logical = 0;
241
+ } else if (remote.wallTime > this.wallTime) {
242
+ this.wallTime = remote.wallTime;
243
+ this.logical = remote.logical + 1;
244
+ } else if (this.wallTime === remote.wallTime) {
245
+ this.logical = Math.max(this.logical, remote.logical) + 1;
246
+ } else {
247
+ this.logical++;
248
+ }
249
+ this.carryLogicalOverflow();
250
+ if (!wasColdStart) {
251
+ this.checkDrift(physicalTime);
252
+ }
253
+ return { wallTime: this.wallTime, logical: this.logical, nodeId: this.nodeId };
254
+ }
255
+ /**
256
+ * Advance this clock to at least the given timestamp.
257
+ *
258
+ * Used after a timestamp rebase rewrites unsynced operations: future `now()`
259
+ * timestamps must sort after every rebased operation, otherwise a write issued
260
+ * immediately after the rebase could interleave with (or precede) rebased ops
261
+ * and break the log's total order. Never moves the clock backward — a
262
+ * timestamp at or before the current state is a no-op, preserving the
263
+ * monotonicity guarantee of `now()`.
264
+ *
265
+ * Inputs with logical > MAX_LOGICAL are normalized deterministically by
266
+ * carrying the excess into wallTime, so the adopted state always leaves room
267
+ * for the next increment inside the serializable range.
268
+ */
269
+ advanceTo(ts) {
270
+ const normalized = {
271
+ wallTime: ts.wallTime + Math.floor(ts.logical / LOGICAL_MODULUS),
272
+ logical: ts.logical % LOGICAL_MODULUS,
273
+ nodeId: ts.nodeId
274
+ };
275
+ const current = {
276
+ wallTime: this.wallTime,
277
+ logical: this.logical,
278
+ nodeId: this.nodeId
279
+ };
280
+ if (_HybridLogicalClock.compare(normalized, current) > 0) {
281
+ this.wallTime = normalized.wallTime;
282
+ this.logical = normalized.logical;
283
+ }
284
+ }
285
+ /**
286
+ * Carry the logical counter into wallTime when an increment pushed it past
287
+ * MAX_LOGICAL. Bumping wallTime by 1ms keeps the timestamp strictly greater
288
+ * than everything issued before (monotonicity) while keeping the logical
289
+ * counter inside the 5-digit slot the serialized form depends on.
290
+ */
291
+ carryLogicalOverflow() {
292
+ if (this.logical > MAX_LOGICAL) {
293
+ this.wallTime += 1;
294
+ this.logical = 0;
295
+ }
296
+ }
297
+ /**
298
+ * Compare two timestamps. Returns negative if a < b, positive if a > b, zero if equal.
299
+ * Total order: wallTime first, then logical, then nodeId (lexicographic).
300
+ */
301
+ static compare(a, b) {
302
+ if (a.wallTime !== b.wallTime) return a.wallTime - b.wallTime;
303
+ if (a.logical !== b.logical) return a.logical - b.logical;
304
+ if (a.nodeId < b.nodeId) return -1;
305
+ if (a.nodeId > b.nodeId) return 1;
306
+ return 0;
307
+ }
308
+ /**
309
+ * Serialize an HLC timestamp to a string that sorts lexicographically.
310
+ * Format: zero-padded wallTime:logical:nodeId
311
+ *
312
+ * @throws {InvalidTimestampError} If wallTime does not fit 15 digits or
313
+ * logical does not fit 5 digits (or either is negative/non-integer). Such a
314
+ * value would overflow its zero-padded slot and the serialized string would
315
+ * no longer sort in the same order as {@link HybridLogicalClock.compare} —
316
+ * silently corrupting every LWW comparison on stored `_version` columns.
317
+ * Internal clocks can no longer produce such values (the logical counter
318
+ * carries into wallTime); this guards against hand-built timestamps.
319
+ */
320
+ static serialize(ts) {
321
+ if (!Number.isInteger(ts.wallTime) || ts.wallTime < 0 || ts.wallTime >= WALL_TIME_LIMIT || !Number.isInteger(ts.logical) || ts.logical < 0 || ts.logical > MAX_LOGICAL) {
322
+ throw new InvalidTimestampError(
323
+ `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.`,
324
+ ts.wallTime,
325
+ ts.logical
326
+ );
327
+ }
328
+ const wall = ts.wallTime.toString().padStart(15, "0");
329
+ const log = ts.logical.toString().padStart(5, "0");
330
+ return `${wall}:${log}:${ts.nodeId}`;
331
+ }
332
+ /**
333
+ * Deserialize an HLC timestamp from its serialized string form.
334
+ */
335
+ static deserialize(s) {
336
+ const parts = s.split(":");
337
+ if (parts.length < 3) {
338
+ throw new Error(`Invalid HLC timestamp string: "${s}"`);
339
+ }
340
+ return {
341
+ wallTime: Number.parseInt(parts[0] ?? "0", 10),
342
+ logical: Number.parseInt(parts[1] ?? "0", 10),
343
+ // nodeId may contain colons, so rejoin remaining parts
344
+ nodeId: parts.slice(2).join(":")
345
+ };
346
+ }
347
+ /**
348
+ * Reports drift between the HLC and (reference-corrected) physical time.
349
+ * Reporting only: local timestamp generation is never blocked, because the
350
+ * user's data always outranks the quality of its timestamps.
351
+ */
352
+ checkDrift(physicalTime) {
353
+ const drift = this.wallTime - this.effectiveTime(physicalTime);
354
+ if (drift > DRIFT_ERROR_MS) {
355
+ this.onDriftError?.(drift);
356
+ return;
357
+ }
358
+ if (drift > DRIFT_WARN_MS) {
359
+ this.onDriftWarning?.(drift);
360
+ }
361
+ }
362
+ };
363
+
364
+ // src/operations/op-data-binary.ts
365
+ function isKoraBytesValue(value) {
366
+ if (typeof value !== "object" || value === null) {
367
+ return false;
368
+ }
369
+ const record = value;
370
+ return Object.keys(record).length === 1 && typeof record.$koraBytes === "string";
371
+ }
372
+ function isLegacyNumericByteObject(value) {
373
+ if (typeof value !== "object" || value === null || ArrayBuffer.isView(value)) {
374
+ return false;
375
+ }
376
+ const record = value;
377
+ const keys = Object.keys(record);
378
+ if (keys.length === 0) {
379
+ return false;
380
+ }
381
+ for (let i = 0; i < keys.length; i++) {
382
+ const byte = record[String(i)];
383
+ if (typeof byte !== "number" || !Number.isInteger(byte) || byte < 0 || byte > 255) {
384
+ return false;
385
+ }
386
+ }
387
+ return true;
388
+ }
389
+ var BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
390
+ var BASE64_REVERSE = new Map(
391
+ Array.from(BASE64_ALPHABET, (char, index) => [char, index])
392
+ );
393
+ function bytesToBase64(bytes) {
394
+ let out = "";
395
+ for (let i = 0; i < bytes.length; i += 3) {
396
+ const b0 = bytes[i] ?? 0;
397
+ const b1 = bytes[i + 1] ?? 0;
398
+ const b2 = bytes[i + 2] ?? 0;
399
+ const triple = b0 << 16 | b1 << 8 | b2;
400
+ out += BASE64_ALPHABET[triple >> 18 & 63] ?? "";
401
+ out += BASE64_ALPHABET[triple >> 12 & 63] ?? "";
402
+ out += i + 1 < bytes.length ? BASE64_ALPHABET[triple >> 6 & 63] ?? "" : "=";
403
+ out += i + 2 < bytes.length ? BASE64_ALPHABET[triple & 63] ?? "" : "=";
404
+ }
405
+ return out;
406
+ }
407
+ function base64ToBytes(base64) {
408
+ const cleaned = base64.replace(/=+$/, "");
409
+ const out = new Uint8Array(Math.floor(cleaned.length * 6 / 8));
410
+ let buffer = 0;
411
+ let bits = 0;
412
+ let index = 0;
413
+ for (const char of cleaned) {
414
+ const value = BASE64_REVERSE.get(char);
415
+ if (value === void 0) {
416
+ throw new OperationError(`Invalid base64 character "${char}" in tagged binary value.`, {
417
+ char
418
+ });
419
+ }
420
+ buffer = buffer << 6 | value;
421
+ bits += 6;
422
+ if (bits >= 8) {
423
+ bits -= 8;
424
+ out[index] = buffer >> bits & 255;
425
+ index += 1;
426
+ }
427
+ }
428
+ return out;
429
+ }
430
+ function encodeBytesForOpData(value) {
431
+ if (typeof value === "string") {
432
+ return value;
433
+ }
434
+ const bytes = value instanceof Uint8Array ? value : new Uint8Array(value);
435
+ return { $koraBytes: bytesToBase64(bytes) };
436
+ }
437
+ function decodeBytesFromOpData(value) {
438
+ if (typeof value === "string") {
439
+ return value;
440
+ }
441
+ if (value instanceof Uint8Array) {
442
+ return value;
443
+ }
444
+ if (value instanceof ArrayBuffer) {
445
+ return new Uint8Array(value);
446
+ }
447
+ if (isKoraBytesValue(value)) {
448
+ return base64ToBytes(value.$koraBytes);
449
+ }
450
+ if (isLegacyNumericByteObject(value)) {
451
+ const keys = Object.keys(value);
452
+ const bytes = new Uint8Array(keys.length);
453
+ for (let i = 0; i < keys.length; i++) {
454
+ bytes[i] = value[String(i)] ?? 0;
455
+ }
456
+ return bytes;
457
+ }
458
+ throw new OperationError(
459
+ "Binary op-data value must be a string, Uint8Array, ArrayBuffer, tagged { $koraBytes } object, or legacy numeric-key byte object.",
460
+ { receivedType: typeof value }
461
+ );
462
+ }
463
+
464
+ // src/operations/content-hash.ts
465
+ var DEFAULT_OPERATION_HASH_VERSION = 2;
466
+ async function computeOperationId(input, timestampOrVersion) {
467
+ if (typeof timestampOrVersion === "string") {
468
+ return sha256Hex(canonicalize(v1HashInput(input, timestampOrVersion)));
469
+ }
470
+ const op = input;
471
+ const serializedTs = HybridLogicalClock.serialize(op.timestamp);
472
+ if (timestampOrVersion === 1) return sha256Hex(canonicalize(v1HashInput(op, serializedTs)));
473
+ return sha256Hex(canonicalize(v2HashInput(op, serializedTs)));
474
+ }
475
+ function v1HashInput(input, timestamp) {
476
+ const hashInput = {
477
+ type: input.type,
478
+ collection: input.collection,
479
+ recordId: input.recordId,
480
+ data: input.data,
481
+ timestamp,
482
+ nodeId: input.nodeId
483
+ };
484
+ if (input.atomicOps !== void 0 && Object.keys(input.atomicOps).length > 0) {
485
+ hashInput.atomicOps = input.atomicOps;
486
+ }
487
+ return hashInput;
488
+ }
489
+ function v2HashInput(op, timestamp) {
490
+ const body = canonicalizeOperationBody(op);
491
+ const atomicOps = body.atomicOps !== void 0 && Object.keys(body.atomicOps).length > 0 ? body.atomicOps : null;
492
+ return {
493
+ hashVersion: 2,
494
+ type: op.type,
495
+ collection: op.collection,
496
+ recordId: op.recordId,
497
+ data: body.data,
498
+ previousData: body.previousData,
499
+ timestamp,
500
+ nodeId: op.nodeId,
501
+ sequenceNumber: op.sequenceNumber,
502
+ // A set of parents: their order carries no meaning, so a relay that reorders
503
+ // them neither breaks verification nor creates a second id for one op.
504
+ causalDeps: [...op.causalDeps].sort(),
505
+ schemaVersion: op.schemaVersion,
506
+ atomicOps
507
+ };
508
+ }
509
+ async function sha256Hex(canonical2) {
510
+ const encoded = new TextEncoder().encode(canonical2);
511
+ const hashBuffer = await globalThis.crypto.subtle.digest("SHA-256", encoded);
512
+ return bufferToHex(hashBuffer);
513
+ }
514
+ function canonicalize(obj) {
515
+ if (obj === null) {
516
+ return "null";
517
+ }
518
+ if (obj === void 0) {
519
+ return "null";
520
+ }
521
+ if (typeof obj !== "object") {
522
+ return JSON.stringify(obj);
523
+ }
524
+ if (Array.isArray(obj)) {
525
+ const items = obj.map((item) => canonicalize(item));
526
+ return `[${items.join(",")}]`;
527
+ }
528
+ const keys = Object.keys(obj).sort();
529
+ const pairs = keys.map((key) => {
530
+ const value = obj[key];
531
+ return `${JSON.stringify(key)}:${canonicalize(value === void 0 ? null : value)}`;
532
+ });
533
+ return `{${pairs.join(",")}}`;
534
+ }
535
+ function bufferToHex(buffer) {
536
+ const bytes = new Uint8Array(buffer);
537
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
538
+ }
539
+
540
+ // src/operations/operation.ts
541
+ async function createOperation(rawInput, clock, options = {}) {
542
+ validateOperationParams(rawInput);
543
+ const input = canonicalizeOperationBody(rawInput);
544
+ const hashVersion = options.hashVersion ?? DEFAULT_OPERATION_HASH_VERSION;
545
+ const timestamp = clock.now();
546
+ const id = hashVersion === 1 ? await computeOperationId(input, HybridLogicalClock.serialize(timestamp)) : await computeOperationId({ ...input, timestamp }, hashVersion);
547
+ const operation = {
548
+ id,
549
+ nodeId: input.nodeId,
550
+ type: input.type,
551
+ collection: input.collection,
552
+ recordId: input.recordId,
553
+ data: input.data ? { ...input.data } : null,
554
+ previousData: input.previousData ? { ...input.previousData } : null,
555
+ timestamp,
556
+ sequenceNumber: input.sequenceNumber,
557
+ causalDeps: [...input.causalDeps],
558
+ schemaVersion: input.schemaVersion,
559
+ ...input.atomicOps !== void 0 && Object.keys(input.atomicOps).length > 0 ? { atomicOps: { ...input.atomicOps } } : {},
560
+ ...input.transactionId !== void 0 ? { transactionId: input.transactionId } : {},
561
+ ...input.mutationName !== void 0 ? { mutationName: input.mutationName } : {},
562
+ ...hashVersion !== 1 ? { hashVersion } : {}
563
+ };
564
+ return deepFreeze(operation);
565
+ }
566
+ function validateOperationParams(input) {
567
+ if (!input.nodeId || typeof input.nodeId !== "string") {
568
+ throw new OperationError("nodeId is required and must be a non-empty string", {
569
+ received: input.nodeId
570
+ });
571
+ }
572
+ if (!input.type || !["insert", "update", "delete"].includes(input.type)) {
573
+ throw new OperationError('type must be "insert", "update", or "delete"', {
574
+ received: input.type
575
+ });
576
+ }
577
+ if (!input.collection || typeof input.collection !== "string") {
578
+ throw new OperationError("collection is required and must be a non-empty string", {
579
+ received: input.collection
580
+ });
581
+ }
582
+ if (!input.recordId || typeof input.recordId !== "string") {
583
+ throw new OperationError("recordId is required and must be a non-empty string", {
584
+ received: input.recordId
585
+ });
586
+ }
587
+ if (input.type === "insert" && input.data === null) {
588
+ throw new OperationError("insert operations must include data", {
589
+ type: input.type,
590
+ collection: input.collection
591
+ });
592
+ }
593
+ if (input.type === "update" && input.data === null) {
594
+ throw new OperationError("update operations must include data with changed fields", {
595
+ type: input.type,
596
+ collection: input.collection
597
+ });
598
+ }
599
+ if (input.type === "update" && input.previousData === null) {
600
+ throw new OperationError(
601
+ "update operations must include previousData for 3-way merge support",
602
+ {
603
+ type: input.type,
604
+ collection: input.collection
605
+ }
606
+ );
607
+ }
608
+ if (input.type === "delete" && input.data !== null) {
609
+ throw new OperationError("delete operations must have null data", {
610
+ type: input.type,
611
+ collection: input.collection
612
+ });
613
+ }
614
+ if (typeof input.sequenceNumber !== "number" || input.sequenceNumber < 0) {
615
+ throw new OperationError("sequenceNumber must be a non-negative number", {
616
+ received: input.sequenceNumber
617
+ });
618
+ }
619
+ if (!Array.isArray(input.causalDeps)) {
620
+ throw new OperationError("causalDeps must be an array of operation IDs", {
621
+ received: typeof input.causalDeps
622
+ });
623
+ }
624
+ if (typeof input.schemaVersion !== "number" || input.schemaVersion < 1) {
625
+ throw new OperationError("schemaVersion must be a positive number", {
626
+ received: input.schemaVersion
627
+ });
628
+ }
629
+ }
630
+ async function verifyOperationId(op) {
631
+ const version = op.hashVersion ?? 1;
632
+ if (version !== 1 && version !== 2) return false;
633
+ let expectedId;
634
+ try {
635
+ expectedId = await computeOperationId(
636
+ {
637
+ nodeId: op.nodeId,
638
+ type: op.type,
639
+ collection: op.collection,
640
+ recordId: op.recordId,
641
+ data: op.data,
642
+ previousData: op.previousData,
643
+ timestamp: op.timestamp,
644
+ sequenceNumber: op.sequenceNumber,
645
+ causalDeps: op.causalDeps,
646
+ schemaVersion: op.schemaVersion,
647
+ ...op.atomicOps !== void 0 ? { atomicOps: op.atomicOps } : {}
648
+ },
649
+ version
650
+ );
651
+ } catch {
652
+ return false;
653
+ }
654
+ return op.id === expectedId;
655
+ }
656
+ async function verifyOperationIntegrity(op) {
657
+ return verifyOperationId(op);
658
+ }
659
+ function isValidOperation(value) {
660
+ if (typeof value !== "object" || value === null) return false;
661
+ const op = value;
662
+ return typeof op.id === "string" && typeof op.nodeId === "string" && (op.type === "insert" || op.type === "update" || op.type === "delete") && typeof op.collection === "string" && typeof op.recordId === "string" && typeof op.sequenceNumber === "number" && Array.isArray(op.causalDeps) && typeof op.schemaVersion === "number" && typeof op.timestamp === "object" && op.timestamp !== null;
663
+ }
664
+ function deepFreeze(obj) {
665
+ if (typeof obj !== "object" || obj === null) return obj;
666
+ if (ArrayBuffer.isView(obj)) return obj;
667
+ Object.freeze(obj);
668
+ for (const value of Object.values(obj)) {
669
+ if (typeof value === "object" && value !== null && !Object.isFrozen(value)) {
670
+ deepFreeze(value);
671
+ }
672
+ }
673
+ return obj;
674
+ }
675
+
676
+ // src/operations/canonical-body.ts
677
+ var NonCanonicalValueError = class extends KoraError {
678
+ constructor(message, path, receivedType) {
679
+ super(message, "NON_CANONICAL_VALUE", { path, receivedType });
680
+ this.path = path;
681
+ this.receivedType = receivedType;
682
+ this.name = "NonCanonicalValueError";
683
+ }
684
+ path;
685
+ receivedType;
686
+ };
687
+ function canonicalizeOperationBody(input) {
688
+ const isUpdate = input.type === "update";
689
+ const where = `${input.collection}/${input.recordId}`;
690
+ const data = canonicalTopLevel(input.data, isUpdate, "data", where);
691
+ const previousData = canonicalTopLevel(input.previousData, isUpdate, "previousData", where);
692
+ let atomicOps = input.atomicOps;
693
+ if (atomicOps !== void 0) {
694
+ const canonical2 = canonicalTopLevel(
695
+ atomicOps,
696
+ false,
697
+ "atomicOps",
698
+ where
699
+ );
700
+ atomicOps = canonical2 ?? void 0;
701
+ }
702
+ if (data === input.data && previousData === input.previousData && atomicOps === input.atomicOps) {
703
+ return input;
704
+ }
705
+ return {
706
+ ...input,
707
+ data,
708
+ previousData,
709
+ ...atomicOps !== void 0 ? { atomicOps } : {}
710
+ };
711
+ }
712
+ function canonicalValue(value, path) {
713
+ return canonical(value, path, /* @__PURE__ */ new Set());
714
+ }
715
+ function canonicalTopLevel(record, undefinedIsNull, label, where) {
716
+ if (record === null || record === void 0) return null;
717
+ if (!isPlainObject(record)) {
718
+ throw new NonCanonicalValueError(
719
+ `Operation ${label} of ${where} must be a plain object, got ${describe(record)}.`,
720
+ label,
721
+ describe(record)
722
+ );
723
+ }
724
+ let changed = false;
725
+ const out = {};
726
+ for (const [key, member] of Object.entries(record)) {
727
+ if (member === void 0) {
728
+ changed = true;
729
+ if (undefinedIsNull) out[key] = null;
730
+ continue;
731
+ }
732
+ const next = canonical(member, `${where}.${key}`, /* @__PURE__ */ new Set());
733
+ if (!Object.is(next, member)) changed = true;
734
+ out[key] = next;
735
+ }
736
+ if (Object.getPrototypeOf(record) !== Object.prototype) changed = true;
737
+ return changed ? out : record;
738
+ }
739
+ function canonical(value, path, ancestors) {
740
+ switch (typeof value) {
741
+ case "string":
742
+ case "boolean":
743
+ return value;
744
+ case "number":
745
+ if (!Number.isFinite(value)) {
746
+ throw refusal(path, String(value), 'Use a finite number, or null for "no value".');
747
+ }
748
+ return Object.is(value, -0) ? 0 : value;
749
+ case "undefined":
750
+ return null;
751
+ case "bigint":
752
+ throw refusal(path, "bigint", "Convert it to a number or a string.");
753
+ case "function":
754
+ throw refusal(path, "function", "Store data only, not functions.");
755
+ case "symbol":
756
+ throw refusal(path, "symbol", "Use a string instead.");
757
+ }
758
+ if (value === null) return null;
759
+ const object = value;
760
+ if (object instanceof Uint8Array) return { $koraBytes: bytesToBase64(object) };
761
+ if (object instanceof ArrayBuffer) return { $koraBytes: bytesToBase64(new Uint8Array(object)) };
762
+ if (object instanceof Date) {
763
+ const time = object.getTime();
764
+ if (Number.isNaN(time)) {
765
+ throw refusal(path, "invalid Date", "Pass a valid Date, or null.");
766
+ }
767
+ return object.toISOString();
768
+ }
769
+ if (ancestors.has(object)) {
770
+ throw refusal(path, "circular reference", "Remove the cycle: JSON cannot hold it.");
771
+ }
772
+ if (Array.isArray(object)) {
773
+ ancestors.add(object);
774
+ let changed2 = false;
775
+ const out2 = [];
776
+ for (let index = 0; index < object.length; index++) {
777
+ if (!(index in object)) {
778
+ ancestors.delete(object);
779
+ throw refusal(
780
+ `${path}[${String(index)}]`,
781
+ "array hole",
782
+ "Use a dense array (fill the gap with null)."
783
+ );
784
+ }
785
+ const item = object[index];
786
+ const next = canonical(item, `${path}[${String(index)}]`, ancestors);
787
+ if (!Object.is(next, item)) changed2 = true;
788
+ out2.push(next);
789
+ }
790
+ ancestors.delete(object);
791
+ return changed2 ? out2 : object;
792
+ }
793
+ if (!isPlainObject(object)) {
794
+ const type = describe(object);
795
+ throw refusal(
796
+ path,
797
+ type,
798
+ type === "Map" ? "Convert it with Object.fromEntries(map)." : type === "Set" ? "Convert it with [...set]." : "Convert it to a plain object (JSON data only)."
799
+ );
800
+ }
801
+ if (typeof object.toJSON === "function") {
802
+ throw refusal(path, "object with toJSON", "Store the value toJSON() returns instead.");
803
+ }
804
+ if (Object.prototype.hasOwnProperty.call(object, "__proto__")) {
805
+ throw refusal(`${path}.__proto__`, '"__proto__" key', "Rename the key.");
806
+ }
807
+ ancestors.add(object);
808
+ let changed = Object.getPrototypeOf(object) !== Object.prototype;
809
+ const out = {};
810
+ for (const [key, member] of Object.entries(object)) {
811
+ if (member === void 0) {
812
+ changed = true;
813
+ continue;
814
+ }
815
+ const next = canonical(member, `${path}.${key}`, ancestors);
816
+ if (!Object.is(next, member)) changed = true;
817
+ out[key] = next;
818
+ }
819
+ ancestors.delete(object);
820
+ return changed ? out : object;
821
+ }
822
+ function refusal(path, receivedType, fix) {
823
+ return new NonCanonicalValueError(
824
+ `Value at "${path}" (${receivedType}) has no JSON form, so it cannot be stored or synced unchanged. ${fix}`,
825
+ path,
826
+ receivedType
827
+ );
828
+ }
829
+ function isPlainObject(value) {
830
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
831
+ const proto = Object.getPrototypeOf(value);
832
+ return proto === Object.prototype || proto === null;
833
+ }
834
+ function describe(value) {
835
+ if (value === null) return "null";
836
+ if (Array.isArray(value)) return "array";
837
+ if (typeof value !== "object") return typeof value;
838
+ const ctor = value.constructor;
839
+ return typeof ctor?.name === "string" && ctor.name.length > 0 ? ctor.name : "object";
840
+ }
841
+ async function canonicalizeProvenLegacyClear(op) {
842
+ const restored = canonicalizeLegacyOperation(op);
843
+ if (restored === op) return op;
844
+ if (await verifyOperationId(op)) return op;
845
+ if (await verifyOperationId(restored)) return restored;
846
+ if (op.hashVersion === void 0 && await verifyOperationId({ ...restored, hashVersion: 2 })) {
847
+ return restored;
848
+ }
849
+ return op;
850
+ }
851
+ function canonicalizeLegacyOperation(op) {
852
+ if (op.type !== "update" || op.hashVersion === 2 || op.encrypted !== void 0) return op;
853
+ const previous = op.previousData;
854
+ if (previous === null || previous === void 0) return op;
855
+ const data = op.data ?? {};
856
+ const missing = Object.keys(previous).filter((key) => !(key in data));
857
+ if (missing.length === 0) return op;
858
+ const out = { ...data };
859
+ for (const key of missing) out[key] = null;
860
+ return { ...op, data: out };
861
+ }
862
+
863
+ // src/fold/field-kind.ts
864
+ var LWW_PLAN = {
865
+ kind: "reg",
866
+ appendOnly: false,
867
+ strategy: "lww",
868
+ tier: 1,
869
+ secret: false
870
+ };
871
+ function planField(collection, field) {
872
+ const descriptor = collection?.fields[field];
873
+ const secret = descriptor?.kind === "secret";
874
+ const resolver = collection?.resolvers?.[field];
875
+ if (resolver) {
876
+ return { kind: "res", appendOnly: false, resolver, strategy: "custom", tier: 3, secret };
877
+ }
878
+ if (!descriptor) return LWW_PLAN;
879
+ switch (descriptor.mergeStrategy) {
880
+ case "counter":
881
+ return { kind: "ctr", appendOnly: false, strategy: "schema-counter", tier: 1, secret };
882
+ case "max":
883
+ return { kind: "max", appendOnly: false, strategy: "schema-max", tier: 1, secret };
884
+ case "min":
885
+ return { kind: "min", appendOnly: false, strategy: "schema-min", tier: 1, secret };
886
+ case "append-only":
887
+ if (descriptor.kind === "array") {
888
+ return {
889
+ kind: "set",
890
+ appendOnly: true,
891
+ strategy: "schema-append-only",
892
+ tier: 1,
893
+ secret
894
+ };
895
+ }
896
+ return { ...LWW_PLAN, secret };
897
+ case "lww":
898
+ return { ...LWW_PLAN, secret };
899
+ case "server-authoritative":
900
+ return {
901
+ ...LWW_PLAN,
902
+ strategy: "schema-server-authoritative",
903
+ secret,
904
+ authoritative: true
905
+ };
906
+ default:
907
+ break;
908
+ }
909
+ switch (descriptor.kind) {
910
+ case "array":
911
+ return { kind: "set", appendOnly: false, strategy: "lww-element-set", tier: 1, secret };
912
+ case "object":
913
+ case "json":
914
+ return { kind: "map", appendOnly: false, strategy: "object-key-lww", tier: 1, secret };
915
+ case "richtext":
916
+ return { kind: "rt", appendOnly: false, strategy: "crdt-text", tier: 1, secret };
917
+ default:
918
+ return { ...LWW_PLAN, secret };
919
+ }
920
+ }
921
+
922
+ // src/fold/stamp.ts
923
+ function compareStamps(a, b) {
924
+ const ca = a.c ?? 0;
925
+ const cb = b.c ?? 0;
926
+ if (ca !== cb) return ca - cb;
927
+ if (a.t !== b.t) return a.t < b.t ? -1 : 1;
928
+ if (a.o !== b.o) return a.o < b.o ? -1 : 1;
929
+ return 0;
930
+ }
931
+ function maxStamp(a, b) {
932
+ if (a === null) return b;
933
+ if (b === null) return a;
934
+ return compareStamps(a, b) >= 0 ? a : b;
935
+ }
936
+ function minStamp(a, b) {
937
+ if (a === null) return b;
938
+ if (b === null) return a;
939
+ return compareStamps(a, b) <= 0 ? a : b;
940
+ }
941
+ function isAfter(a, b) {
942
+ if (a === null) return false;
943
+ if (b === null) return true;
944
+ return compareStamps(a, b) > 0;
945
+ }
946
+ function stampOf(timestamp, opId) {
947
+ return { t: HybridLogicalClock.serialize(timestamp), o: opId };
948
+ }
949
+ function stampTimestamp(stamp) {
950
+ return HybridLogicalClock.deserialize(stamp.t);
951
+ }
952
+
953
+ // src/schema/quote-ident.ts
954
+ function quoteIdent(name) {
955
+ return `"${name.replace(/"/g, '""')}"`;
956
+ }
957
+
958
+ // src/schema/sql-gen.ts
959
+ function generateSQL(collectionName, collection, relations) {
960
+ const statements = [];
961
+ const columns = ["id TEXT PRIMARY KEY NOT NULL"];
962
+ const indexedFields = new Set(collection.indexes);
963
+ const fkFields = [];
964
+ for (const [fieldName, descriptor] of Object.entries(collection.fields)) {
965
+ let colDef = columnDefinition(fieldName, descriptor);
966
+ if (relations) {
967
+ for (const rel of Object.values(relations)) {
968
+ if (rel.from === collectionName && rel.field === fieldName) {
969
+ colDef += ` REFERENCES ${quoteIdent(rel.to)}(id)`;
970
+ fkFields.push(fieldName);
971
+ break;
972
+ }
973
+ }
974
+ }
975
+ columns.push(colDef);
976
+ }
977
+ columns.push("_created_at INTEGER NOT NULL");
978
+ columns.push("_updated_at INTEGER NOT NULL");
979
+ columns.push("_version TEXT NOT NULL DEFAULT ''");
980
+ columns.push("_field_versions TEXT NOT NULL DEFAULT '{}'");
981
+ columns.push("_deleted INTEGER NOT NULL DEFAULT 0");
982
+ statements.push(
983
+ `CREATE TABLE IF NOT EXISTS ${quoteIdent(collectionName)} (
984
+ ${columns.join(",\n ")}
985
+ )`
986
+ );
987
+ for (const [fieldName, descriptor] of Object.entries(collection.fields)) {
988
+ const colDef = columnDefinition(fieldName, descriptor);
989
+ statements.push(
990
+ `--kora:safe-alter
991
+ ALTER TABLE ${quoteIdent(collectionName)} ADD COLUMN ${colDef}`
992
+ );
993
+ }
994
+ statements.push(
995
+ `--kora:safe-alter
996
+ ALTER TABLE ${quoteIdent(collectionName)} ADD COLUMN _version TEXT NOT NULL DEFAULT ''`
997
+ );
998
+ statements.push(
999
+ `--kora:safe-alter
1000
+ ALTER TABLE ${quoteIdent(collectionName)} ADD COLUMN _field_versions TEXT NOT NULL DEFAULT '{}'`
1001
+ );
1002
+ for (const indexField of collection.indexes) {
1003
+ statements.push(
1004
+ `CREATE INDEX IF NOT EXISTS ${quoteIdent(collectionIndexName(collectionName, indexField))} ON ${quoteIdent(collectionName)} (${quoteIdent(indexField)})`
1005
+ );
1006
+ }
1007
+ for (const fkField of fkFields) {
1008
+ if (!indexedFields.has(fkField)) {
1009
+ statements.push(
1010
+ `CREATE INDEX IF NOT EXISTS ${quoteIdent(collectionIndexName(collectionName, fkField))} ON ${quoteIdent(collectionName)} (${quoteIdent(fkField)})`
1011
+ );
1012
+ }
1013
+ }
1014
+ statements.push(
1015
+ `CREATE TABLE IF NOT EXISTS ${quoteIdent(`_kora_ops_${collectionName}`)} (
1016
+ id TEXT PRIMARY KEY NOT NULL,
1017
+ node_id TEXT NOT NULL,
1018
+ type TEXT NOT NULL,
1019
+ record_id TEXT NOT NULL,
1020
+ data TEXT,
1021
+ previous_data TEXT,
1022
+ timestamp TEXT NOT NULL,
1023
+ sequence_number INTEGER NOT NULL,
1024
+ causal_deps TEXT NOT NULL,
1025
+ schema_version INTEGER NOT NULL
1026
+ )`
1027
+ );
1028
+ statements.push(
1029
+ `CREATE INDEX IF NOT EXISTS idx_kora_ops_${collectionName}_record_id ON ${quoteIdent(`_kora_ops_${collectionName}`)} (record_id)`
1030
+ );
1031
+ return statements;
1032
+ }
1033
+ function collectionIndexName(collection, field) {
1034
+ return `idx_${collection.length}_${collection}_${field}`;
1035
+ }
1036
+ function legacyCollectionIndexName(collection, field) {
1037
+ return `idx_${collection}_${field}`;
1038
+ }
1039
+ var SCHEMA_CEILING_DIRECTIVE = "--kora:schema-ceiling";
1040
+ function generateFullDDL(schema) {
1041
+ const statements = [];
1042
+ statements.push(
1043
+ "CREATE TABLE IF NOT EXISTS _kora_meta (\n key TEXT PRIMARY KEY NOT NULL,\n value TEXT NOT NULL\n)"
1044
+ );
1045
+ statements.push(`${SCHEMA_CEILING_DIRECTIVE} ${schema.version}`);
1046
+ statements.push(
1047
+ "CREATE TABLE IF NOT EXISTS _kora_version_vector (\n node_id TEXT PRIMARY KEY NOT NULL,\n sequence_number INTEGER NOT NULL\n)"
1048
+ );
1049
+ statements.push(
1050
+ "CREATE TABLE IF NOT EXISTS _kora_sequences (\n name TEXT NOT NULL,\n scope TEXT NOT NULL DEFAULT '',\n node_id TEXT NOT NULL,\n counter INTEGER NOT NULL DEFAULT 0,\n PRIMARY KEY (name, scope, node_id)\n)"
1051
+ );
1052
+ statements.push(
1053
+ "CREATE TABLE IF NOT EXISTS _kora_sync_queue (\n id TEXT PRIMARY KEY NOT NULL,\n payload TEXT NOT NULL\n)"
1054
+ );
1055
+ statements.push(
1056
+ "CREATE TABLE IF NOT EXISTS _kora_sync_rejected (\n operation_id TEXT PRIMARY KEY NOT NULL,\n collection TEXT NOT NULL,\n record_id TEXT NOT NULL,\n code TEXT NOT NULL,\n message TEXT NOT NULL,\n retriable INTEGER NOT NULL,\n rejected_at INTEGER NOT NULL\n)"
1057
+ );
1058
+ statements.push(
1059
+ "CREATE TABLE IF NOT EXISTS _kora_audit_traces (\n id TEXT PRIMARY KEY NOT NULL,\n recorded_at INTEGER NOT NULL,\n event_type TEXT NOT NULL,\n collection TEXT NOT NULL,\n record_id TEXT NOT NULL,\n field TEXT NOT NULL,\n strategy TEXT NOT NULL,\n tier INTEGER NOT NULL,\n constraint_name TEXT,\n trace_json TEXT NOT NULL\n)"
1060
+ );
1061
+ statements.push(
1062
+ "CREATE INDEX IF NOT EXISTS idx_kora_audit_traces_recorded_at ON _kora_audit_traces (recorded_at)"
1063
+ );
1064
+ statements.push(
1065
+ "CREATE INDEX IF NOT EXISTS idx_kora_audit_traces_collection ON _kora_audit_traces (collection)"
1066
+ );
1067
+ for (const [name, collection] of Object.entries(schema.collections)) {
1068
+ statements.push(...generateSQL(name, collection, schema.relations));
1069
+ }
1070
+ return statements;
1071
+ }
1072
+ function columnDefinition(fieldName, descriptor) {
1073
+ const sqlType = mapFieldType(descriptor);
1074
+ const parts = [quoteIdent(fieldName), sqlType];
1075
+ if (descriptor.defaultValue !== void 0) {
1076
+ parts.push(`DEFAULT ${sqlDefaultLiteral(descriptor.defaultValue)}`);
1077
+ }
1078
+ return parts.join(" ");
1079
+ }
1080
+ function sqlStringLiteral(value) {
1081
+ return `'${value.replaceAll("'", "''")}'`;
1082
+ }
1083
+ function sqlDefaultLiteral(value) {
1084
+ if (value === null) return "NULL";
1085
+ if (typeof value === "string") return sqlStringLiteral(value);
1086
+ if (typeof value === "number") {
1087
+ return Number.isFinite(value) ? String(value) : "NULL";
1088
+ }
1089
+ if (typeof value === "boolean") return value ? "1" : "0";
1090
+ return sqlStringLiteral(JSON.stringify(value) ?? "null");
1091
+ }
1092
+ function enumCheckConstraint(fieldName, values) {
1093
+ return `CHECK (${quoteIdent(fieldName)} IN (${values.map(sqlStringLiteral).join(", ")}))`;
1094
+ }
1095
+ function mapFieldType(descriptor) {
1096
+ switch (descriptor.kind) {
1097
+ case "string":
1098
+ return "TEXT";
1099
+ case "number":
1100
+ return "REAL";
1101
+ case "boolean":
1102
+ return "INTEGER";
1103
+ case "enum":
1104
+ return "TEXT";
1105
+ case "timestamp":
1106
+ return "INTEGER";
1107
+ case "array":
1108
+ return "TEXT";
1109
+ // JSON-serialized
1110
+ case "object":
1111
+ return "TEXT";
1112
+ // JSON-serialized structured object
1113
+ case "json":
1114
+ return "TEXT";
1115
+ // JSON-serialized dynamic-key value
1116
+ case "blob":
1117
+ return "TEXT";
1118
+ // JSON-serialized content-addressed BlobRef (bytes stored out of band)
1119
+ case "secret":
1120
+ return "TEXT";
1121
+ case "richtext":
1122
+ return "BLOB";
1123
+ }
1124
+ }
1125
+
1126
+ // src/version-vector/topological-sort.ts
1127
+ function topologicalSort(operations) {
1128
+ if (operations.length <= 1) return [...operations];
1129
+ const opMap = /* @__PURE__ */ new Map();
1130
+ for (const op of operations) {
1131
+ opMap.set(op.id, op);
1132
+ }
1133
+ const inDegree = /* @__PURE__ */ new Map();
1134
+ const dependents = /* @__PURE__ */ new Map();
1135
+ for (const op of operations) {
1136
+ if (!inDegree.has(op.id)) {
1137
+ inDegree.set(op.id, 0);
1138
+ }
1139
+ if (!dependents.has(op.id)) {
1140
+ dependents.set(op.id, []);
1141
+ }
1142
+ for (const depId of op.causalDeps) {
1143
+ if (opMap.has(depId)) {
1144
+ inDegree.set(op.id, (inDegree.get(op.id) ?? 0) + 1);
1145
+ const deps = dependents.get(depId);
1146
+ if (deps) {
1147
+ deps.push(op.id);
1148
+ } else {
1149
+ dependents.set(depId, [op.id]);
1150
+ }
1151
+ }
1152
+ }
1153
+ }
1154
+ const heap = new MinHeap(compareByTimestamp);
1155
+ for (const op of operations) {
1156
+ if ((inDegree.get(op.id) ?? 0) === 0) {
1157
+ heap.push(op);
1158
+ }
1159
+ }
1160
+ const result = [];
1161
+ while (heap.size > 0) {
1162
+ const current = heap.pop();
1163
+ result.push(current);
1164
+ const deps = dependents.get(current.id) ?? [];
1165
+ for (const depId of deps) {
1166
+ const deg = (inDegree.get(depId) ?? 0) - 1;
1167
+ inDegree.set(depId, deg);
1168
+ if (deg === 0) {
1169
+ const op = opMap.get(depId);
1170
+ if (op) heap.push(op);
1171
+ }
1172
+ }
1173
+ }
1174
+ if (result.length !== operations.length) {
1175
+ throw new OperationError(
1176
+ `Cycle detected in operation dependency graph. Sorted ${result.length} of ${operations.length} operations.`,
1177
+ {
1178
+ sortedCount: result.length,
1179
+ totalCount: operations.length
1180
+ }
1181
+ );
1182
+ }
1183
+ return result;
1184
+ }
1185
+ function compareByTimestamp(a, b) {
1186
+ return HybridLogicalClock.compare(a.timestamp, b.timestamp);
1187
+ }
1188
+ var MinHeap = class {
1189
+ data = [];
1190
+ cmp;
1191
+ constructor(comparator) {
1192
+ this.cmp = comparator;
1193
+ }
1194
+ get size() {
1195
+ return this.data.length;
1196
+ }
1197
+ push(item) {
1198
+ this.data.push(item);
1199
+ this.bubbleUp(this.data.length - 1);
1200
+ }
1201
+ pop() {
1202
+ const top = this.data[0];
1203
+ const last = this.data.pop();
1204
+ if (this.data.length > 0) {
1205
+ this.data[0] = last;
1206
+ this.sinkDown(0);
1207
+ }
1208
+ return top;
1209
+ }
1210
+ bubbleUp(index) {
1211
+ let current = index;
1212
+ while (current > 0) {
1213
+ const parentIndex = current - 1 >> 1;
1214
+ if (this.cmp(this.data[current], this.data[parentIndex]) >= 0) break;
1215
+ this.swap(current, parentIndex);
1216
+ current = parentIndex;
1217
+ }
1218
+ }
1219
+ sinkDown(index) {
1220
+ const length = this.data.length;
1221
+ let current = index;
1222
+ while (true) {
1223
+ let smallest = current;
1224
+ const left = 2 * current + 1;
1225
+ const right = 2 * current + 2;
1226
+ if (left < length && this.cmp(this.data[left], this.data[smallest]) < 0) {
1227
+ smallest = left;
1228
+ }
1229
+ if (right < length && this.cmp(this.data[right], this.data[smallest]) < 0) {
1230
+ smallest = right;
1231
+ }
1232
+ if (smallest === current) break;
1233
+ this.swap(current, smallest);
1234
+ current = smallest;
1235
+ }
1236
+ }
1237
+ swap(i, j) {
1238
+ const tmp = this.data[i];
1239
+ this.data[i] = this.data[j];
1240
+ this.data[j] = tmp;
1241
+ }
1242
+ };
1243
+
1244
+ export {
1245
+ KORA_ERROR_FIX_SUGGESTIONS,
1246
+ getKoraErrorFix,
1247
+ KoraError,
1248
+ SchemaValidationError,
1249
+ OperationError,
1250
+ OperationTooLargeError,
1251
+ MergeConflictError,
1252
+ SyncError,
1253
+ StorageError,
1254
+ AppNotReadyError,
1255
+ RemoteClockDriftError,
1256
+ InvalidTimestampError,
1257
+ ClockDriftError,
1258
+ MAX_LOGICAL,
1259
+ HybridLogicalClock,
1260
+ isKoraBytesValue,
1261
+ isLegacyNumericByteObject,
1262
+ bytesToBase64,
1263
+ base64ToBytes,
1264
+ encodeBytesForOpData,
1265
+ decodeBytesFromOpData,
1266
+ NonCanonicalValueError,
1267
+ canonicalizeOperationBody,
1268
+ canonicalValue,
1269
+ canonicalizeProvenLegacyClear,
1270
+ canonicalizeLegacyOperation,
1271
+ DEFAULT_OPERATION_HASH_VERSION,
1272
+ computeOperationId,
1273
+ canonicalize,
1274
+ createOperation,
1275
+ validateOperationParams,
1276
+ verifyOperationId,
1277
+ verifyOperationIntegrity,
1278
+ isValidOperation,
1279
+ planField,
1280
+ compareStamps,
1281
+ maxStamp,
1282
+ minStamp,
1283
+ isAfter,
1284
+ stampOf,
1285
+ stampTimestamp,
1286
+ quoteIdent,
1287
+ generateSQL,
1288
+ collectionIndexName,
1289
+ legacyCollectionIndexName,
1290
+ SCHEMA_CEILING_DIRECTIVE,
1291
+ generateFullDDL,
1292
+ sqlStringLiteral,
1293
+ sqlDefaultLiteral,
1294
+ enumCheckConstraint,
1295
+ topologicalSort
1296
+ };
1297
+ //# sourceMappingURL=chunk-QUD3DMSD.js.map