@korajs/core 0.1.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/dist/index.cjs ADDED
@@ -0,0 +1,1153 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ ArrayFieldBuilder: () => ArrayFieldBuilder,
24
+ CONNECTION_QUALITIES: () => CONNECTION_QUALITIES,
25
+ ClockDriftError: () => ClockDriftError,
26
+ EnumFieldBuilder: () => EnumFieldBuilder,
27
+ FieldBuilder: () => FieldBuilder,
28
+ HybridLogicalClock: () => HybridLogicalClock,
29
+ KoraError: () => KoraError,
30
+ MERGE_STRATEGIES: () => MERGE_STRATEGIES,
31
+ MergeConflictError: () => MergeConflictError,
32
+ OperationError: () => OperationError,
33
+ SchemaValidationError: () => SchemaValidationError,
34
+ StorageError: () => StorageError,
35
+ SyncError: () => SyncError,
36
+ advanceVector: () => advanceVector,
37
+ computeDelta: () => computeDelta,
38
+ createOperation: () => createOperation,
39
+ createVersionVector: () => createVersionVector,
40
+ defineSchema: () => defineSchema,
41
+ deserializeVector: () => deserializeVector,
42
+ dominates: () => dominates,
43
+ extractTimestamp: () => extractTimestamp,
44
+ generateFullDDL: () => generateFullDDL,
45
+ generateSQL: () => generateSQL,
46
+ generateUUIDv7: () => generateUUIDv7,
47
+ isValidOperation: () => isValidOperation,
48
+ isValidUUIDv7: () => isValidUUIDv7,
49
+ mergeVectors: () => mergeVectors,
50
+ serializeVector: () => serializeVector,
51
+ t: () => t,
52
+ validateRecord: () => validateRecord,
53
+ vectorsEqual: () => vectorsEqual,
54
+ verifyOperationIntegrity: () => verifyOperationIntegrity
55
+ });
56
+ module.exports = __toCommonJS(index_exports);
57
+
58
+ // src/types.ts
59
+ var MERGE_STRATEGIES = [
60
+ "auto-merge",
61
+ "lww",
62
+ "first-write-wins",
63
+ "server-decides",
64
+ "custom"
65
+ ];
66
+ var CONNECTION_QUALITIES = ["excellent", "good", "fair", "poor", "offline"];
67
+
68
+ // src/errors/errors.ts
69
+ var KoraError = class extends Error {
70
+ constructor(message, code, context) {
71
+ super(message);
72
+ this.code = code;
73
+ this.context = context;
74
+ this.name = "KoraError";
75
+ }
76
+ code;
77
+ context;
78
+ };
79
+ var SchemaValidationError = class extends KoraError {
80
+ constructor(message, context) {
81
+ super(message, "SCHEMA_VALIDATION", context);
82
+ this.name = "SchemaValidationError";
83
+ }
84
+ };
85
+ var OperationError = class extends KoraError {
86
+ constructor(message, context) {
87
+ super(message, "OPERATION_ERROR", context);
88
+ this.name = "OperationError";
89
+ }
90
+ };
91
+ var MergeConflictError = class extends KoraError {
92
+ constructor(operationA, operationB, field) {
93
+ super(
94
+ `Merge conflict on field "${field}" in collection "${operationA.collection}"`,
95
+ "MERGE_CONFLICT",
96
+ { operationA: operationA.id, operationB: operationB.id, field }
97
+ );
98
+ this.operationA = operationA;
99
+ this.operationB = operationB;
100
+ this.field = field;
101
+ this.name = "MergeConflictError";
102
+ }
103
+ operationA;
104
+ operationB;
105
+ field;
106
+ };
107
+ var SyncError = class extends KoraError {
108
+ constructor(message, context) {
109
+ super(message, "SYNC_ERROR", context);
110
+ this.name = "SyncError";
111
+ }
112
+ };
113
+ var StorageError = class extends KoraError {
114
+ constructor(message, context) {
115
+ super(message, "STORAGE_ERROR", context);
116
+ this.name = "StorageError";
117
+ }
118
+ };
119
+ var ClockDriftError = class extends KoraError {
120
+ constructor(currentHlcTime, physicalTime) {
121
+ const driftSeconds = Math.round((currentHlcTime - physicalTime) / 1e3);
122
+ super(
123
+ `Clock drift of ${driftSeconds}s detected. Physical time is behind HLC by more than 5 minutes. This indicates a severe clock issue.`,
124
+ "CLOCK_DRIFT",
125
+ { currentHlcTime, physicalTime, driftSeconds }
126
+ );
127
+ this.currentHlcTime = currentHlcTime;
128
+ this.physicalTime = physicalTime;
129
+ this.name = "ClockDriftError";
130
+ }
131
+ currentHlcTime;
132
+ physicalTime;
133
+ };
134
+
135
+ // src/clock/hlc.ts
136
+ var systemTimeSource = { now: () => Date.now() };
137
+ var DRIFT_WARN_MS = 6e4;
138
+ var DRIFT_ERROR_MS = 5 * 6e4;
139
+ var HybridLogicalClock = class {
140
+ constructor(nodeId, timeSource = systemTimeSource, onDriftWarning) {
141
+ this.nodeId = nodeId;
142
+ this.timeSource = timeSource;
143
+ this.onDriftWarning = onDriftWarning;
144
+ }
145
+ nodeId;
146
+ timeSource;
147
+ onDriftWarning;
148
+ wallTime = 0;
149
+ logical = 0;
150
+ /**
151
+ * Generate a new timestamp for a local event.
152
+ * Guarantees monotonicity: each call returns a timestamp strictly greater than the previous.
153
+ *
154
+ * @throws {ClockDriftError} If physical time is more than 5 minutes behind the HLC wallTime
155
+ */
156
+ now() {
157
+ const physicalTime = this.timeSource.now();
158
+ this.checkDrift(physicalTime);
159
+ if (physicalTime > this.wallTime) {
160
+ this.wallTime = physicalTime;
161
+ this.logical = 0;
162
+ } else {
163
+ this.logical++;
164
+ }
165
+ return { wallTime: this.wallTime, logical: this.logical, nodeId: this.nodeId };
166
+ }
167
+ /**
168
+ * Update clock on receiving a remote timestamp.
169
+ * Merges the remote clock state with the local state to maintain causal ordering.
170
+ *
171
+ * @throws {ClockDriftError} If physical time is more than 5 minutes behind the resulting wallTime
172
+ */
173
+ receive(remote) {
174
+ const physicalTime = this.timeSource.now();
175
+ if (physicalTime > this.wallTime && physicalTime > remote.wallTime) {
176
+ this.wallTime = physicalTime;
177
+ this.logical = 0;
178
+ } else if (remote.wallTime > this.wallTime) {
179
+ this.wallTime = remote.wallTime;
180
+ this.logical = remote.logical + 1;
181
+ } else if (this.wallTime === remote.wallTime) {
182
+ this.logical = Math.max(this.logical, remote.logical) + 1;
183
+ } else {
184
+ this.logical++;
185
+ }
186
+ this.checkDrift(physicalTime);
187
+ return { wallTime: this.wallTime, logical: this.logical, nodeId: this.nodeId };
188
+ }
189
+ /**
190
+ * Compare two timestamps. Returns negative if a < b, positive if a > b, zero if equal.
191
+ * Total order: wallTime first, then logical, then nodeId (lexicographic).
192
+ */
193
+ static compare(a, b) {
194
+ if (a.wallTime !== b.wallTime) return a.wallTime - b.wallTime;
195
+ if (a.logical !== b.logical) return a.logical - b.logical;
196
+ if (a.nodeId < b.nodeId) return -1;
197
+ if (a.nodeId > b.nodeId) return 1;
198
+ return 0;
199
+ }
200
+ /**
201
+ * Serialize an HLC timestamp to a string that sorts lexicographically.
202
+ * Format: zero-padded wallTime:logical:nodeId
203
+ */
204
+ static serialize(ts) {
205
+ const wall = ts.wallTime.toString().padStart(15, "0");
206
+ const log = ts.logical.toString().padStart(5, "0");
207
+ return `${wall}:${log}:${ts.nodeId}`;
208
+ }
209
+ /**
210
+ * Deserialize an HLC timestamp from its serialized string form.
211
+ */
212
+ static deserialize(s) {
213
+ const parts = s.split(":");
214
+ if (parts.length < 3) {
215
+ throw new Error(`Invalid HLC timestamp string: "${s}"`);
216
+ }
217
+ return {
218
+ wallTime: Number.parseInt(parts[0] ?? "0", 10),
219
+ logical: Number.parseInt(parts[1] ?? "0", 10),
220
+ // nodeId may contain colons, so rejoin remaining parts
221
+ nodeId: parts.slice(2).join(":")
222
+ };
223
+ }
224
+ checkDrift(physicalTime) {
225
+ const drift = this.wallTime - physicalTime;
226
+ if (drift > DRIFT_ERROR_MS) {
227
+ throw new ClockDriftError(this.wallTime, physicalTime);
228
+ }
229
+ if (drift > DRIFT_WARN_MS) {
230
+ this.onDriftWarning?.(drift);
231
+ }
232
+ }
233
+ };
234
+
235
+ // src/identifiers/uuid-v7.ts
236
+ var defaultRandom = globalThis.crypto;
237
+ function generateUUIDv7(timestamp = Date.now(), randomSource = defaultRandom) {
238
+ const bytes = new Uint8Array(16);
239
+ randomSource.getRandomValues(bytes);
240
+ const ms = Math.max(0, Math.floor(timestamp));
241
+ bytes[0] = ms / 2 ** 40 & 255;
242
+ bytes[1] = ms / 2 ** 32 & 255;
243
+ bytes[2] = ms / 2 ** 24 & 255;
244
+ bytes[3] = ms / 2 ** 16 & 255;
245
+ bytes[4] = ms / 2 ** 8 & 255;
246
+ bytes[5] = ms & 255;
247
+ bytes[6] = (bytes[6] ?? 0) & 15 | 112;
248
+ bytes[8] = (bytes[8] ?? 0) & 63 | 128;
249
+ return formatUUID(bytes);
250
+ }
251
+ function extractTimestamp(uuid) {
252
+ const hex = uuid.replace(/-/g, "");
253
+ const high = Number.parseInt(hex.slice(0, 8), 16);
254
+ const low = Number.parseInt(hex.slice(8, 12), 16);
255
+ return high * 2 ** 16 + low;
256
+ }
257
+ function isValidUUIDv7(uuid) {
258
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(uuid)) {
259
+ return false;
260
+ }
261
+ const hex = uuid.replace(/-/g, "");
262
+ if (hex[12] !== "7") return false;
263
+ const variantNibble = Number.parseInt(hex[16] ?? "0", 16);
264
+ return variantNibble >= 8 && variantNibble <= 11;
265
+ }
266
+ function formatUUID(bytes) {
267
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
268
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
269
+ }
270
+
271
+ // src/operations/content-hash.ts
272
+ async function computeOperationId(input, timestamp) {
273
+ const canonical = canonicalize({
274
+ type: input.type,
275
+ collection: input.collection,
276
+ recordId: input.recordId,
277
+ data: input.data,
278
+ timestamp,
279
+ nodeId: input.nodeId
280
+ });
281
+ const encoded = new TextEncoder().encode(canonical);
282
+ const hashBuffer = await globalThis.crypto.subtle.digest("SHA-256", encoded);
283
+ return bufferToHex(hashBuffer);
284
+ }
285
+ function canonicalize(obj) {
286
+ if (obj === null || obj === void 0) {
287
+ return JSON.stringify(obj);
288
+ }
289
+ if (typeof obj !== "object") {
290
+ return JSON.stringify(obj);
291
+ }
292
+ if (Array.isArray(obj)) {
293
+ const items = obj.map((item) => canonicalize(item));
294
+ return `[${items.join(",")}]`;
295
+ }
296
+ const keys = Object.keys(obj).sort();
297
+ const pairs = keys.map((key) => {
298
+ const value = obj[key];
299
+ return `${JSON.stringify(key)}:${canonicalize(value)}`;
300
+ });
301
+ return `{${pairs.join(",")}}`;
302
+ }
303
+ function bufferToHex(buffer) {
304
+ const bytes = new Uint8Array(buffer);
305
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
306
+ }
307
+
308
+ // src/operations/operation.ts
309
+ async function createOperation(input, clock) {
310
+ validateOperationParams(input);
311
+ const timestamp = clock.now();
312
+ const serializedTs = HybridLogicalClock.serialize(timestamp);
313
+ const id = await computeOperationId(input, serializedTs);
314
+ const operation = {
315
+ id,
316
+ nodeId: input.nodeId,
317
+ type: input.type,
318
+ collection: input.collection,
319
+ recordId: input.recordId,
320
+ data: input.data ? { ...input.data } : null,
321
+ previousData: input.previousData ? { ...input.previousData } : null,
322
+ timestamp,
323
+ sequenceNumber: input.sequenceNumber,
324
+ causalDeps: [...input.causalDeps],
325
+ schemaVersion: input.schemaVersion
326
+ };
327
+ return deepFreeze(operation);
328
+ }
329
+ function validateOperationParams(input) {
330
+ if (!input.nodeId || typeof input.nodeId !== "string") {
331
+ throw new OperationError("nodeId is required and must be a non-empty string", {
332
+ received: input.nodeId
333
+ });
334
+ }
335
+ if (!input.type || !["insert", "update", "delete"].includes(input.type)) {
336
+ throw new OperationError('type must be "insert", "update", or "delete"', {
337
+ received: input.type
338
+ });
339
+ }
340
+ if (!input.collection || typeof input.collection !== "string") {
341
+ throw new OperationError("collection is required and must be a non-empty string", {
342
+ received: input.collection
343
+ });
344
+ }
345
+ if (!input.recordId || typeof input.recordId !== "string") {
346
+ throw new OperationError("recordId is required and must be a non-empty string", {
347
+ received: input.recordId
348
+ });
349
+ }
350
+ if (input.type === "insert" && input.data === null) {
351
+ throw new OperationError("insert operations must include data", {
352
+ type: input.type,
353
+ collection: input.collection
354
+ });
355
+ }
356
+ if (input.type === "update" && input.data === null) {
357
+ throw new OperationError("update operations must include data with changed fields", {
358
+ type: input.type,
359
+ collection: input.collection
360
+ });
361
+ }
362
+ if (input.type === "update" && input.previousData === null) {
363
+ throw new OperationError(
364
+ "update operations must include previousData for 3-way merge support",
365
+ {
366
+ type: input.type,
367
+ collection: input.collection
368
+ }
369
+ );
370
+ }
371
+ if (input.type === "delete" && input.data !== null) {
372
+ throw new OperationError("delete operations must have null data", {
373
+ type: input.type,
374
+ collection: input.collection
375
+ });
376
+ }
377
+ if (typeof input.sequenceNumber !== "number" || input.sequenceNumber < 0) {
378
+ throw new OperationError("sequenceNumber must be a non-negative number", {
379
+ received: input.sequenceNumber
380
+ });
381
+ }
382
+ if (!Array.isArray(input.causalDeps)) {
383
+ throw new OperationError("causalDeps must be an array of operation IDs", {
384
+ received: typeof input.causalDeps
385
+ });
386
+ }
387
+ if (typeof input.schemaVersion !== "number" || input.schemaVersion < 1) {
388
+ throw new OperationError("schemaVersion must be a positive number", {
389
+ received: input.schemaVersion
390
+ });
391
+ }
392
+ }
393
+ async function verifyOperationIntegrity(op) {
394
+ const input = {
395
+ nodeId: op.nodeId,
396
+ type: op.type,
397
+ collection: op.collection,
398
+ recordId: op.recordId,
399
+ data: op.data,
400
+ previousData: op.previousData,
401
+ sequenceNumber: op.sequenceNumber,
402
+ causalDeps: op.causalDeps,
403
+ schemaVersion: op.schemaVersion
404
+ };
405
+ const serializedTs = HybridLogicalClock.serialize(op.timestamp);
406
+ const expectedId = await computeOperationId(input, serializedTs);
407
+ return op.id === expectedId;
408
+ }
409
+ function isValidOperation(value) {
410
+ if (typeof value !== "object" || value === null) return false;
411
+ const op = value;
412
+ 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;
413
+ }
414
+ function deepFreeze(obj) {
415
+ if (typeof obj !== "object" || obj === null) return obj;
416
+ Object.freeze(obj);
417
+ for (const value of Object.values(obj)) {
418
+ if (typeof value === "object" && value !== null && !Object.isFrozen(value)) {
419
+ deepFreeze(value);
420
+ }
421
+ }
422
+ return obj;
423
+ }
424
+
425
+ // src/schema/define.ts
426
+ var COLLECTION_NAME_RE = /^[a-z][a-z0-9_]*$/;
427
+ var FIELD_NAME_RE = /^[a-z][a-z0-9_]*$/;
428
+ var RESERVED_FIELDS = /* @__PURE__ */ new Set(["id", "_created_at", "_updated_at", "_deleted"]);
429
+ function defineSchema(input) {
430
+ validateVersion(input.version);
431
+ const collections = {};
432
+ for (const [name, collectionInput] of Object.entries(input.collections)) {
433
+ validateCollectionName(name);
434
+ collections[name] = buildCollection(name, collectionInput);
435
+ }
436
+ if (Object.keys(collections).length === 0) {
437
+ throw new SchemaValidationError("Schema must define at least one collection");
438
+ }
439
+ const relations = {};
440
+ if (input.relations) {
441
+ for (const [name, relationInput] of Object.entries(input.relations)) {
442
+ validateRelation(name, relationInput, collections);
443
+ relations[name] = { ...relationInput };
444
+ }
445
+ }
446
+ return { version: input.version, collections, relations };
447
+ }
448
+ function validateVersion(version) {
449
+ if (typeof version !== "number" || !Number.isInteger(version) || version < 1) {
450
+ throw new SchemaValidationError("Schema version must be a positive integer", {
451
+ received: version
452
+ });
453
+ }
454
+ }
455
+ function validateCollectionName(name) {
456
+ if (!COLLECTION_NAME_RE.test(name)) {
457
+ throw new SchemaValidationError(
458
+ `Collection name "${name}" is invalid. Must be lowercase, start with a letter, and contain only letters, numbers, and underscores.`,
459
+ { collection: name }
460
+ );
461
+ }
462
+ }
463
+ function buildCollection(name, input) {
464
+ const fields = {};
465
+ if (!input.fields || Object.keys(input.fields).length === 0) {
466
+ throw new SchemaValidationError(`Collection "${name}" must define at least one field`, {
467
+ collection: name
468
+ });
469
+ }
470
+ for (const [fieldName, builder] of Object.entries(input.fields)) {
471
+ validateFieldName(name, fieldName);
472
+ fields[fieldName] = builder._build();
473
+ }
474
+ const indexes = input.indexes ?? [];
475
+ for (const indexField of indexes) {
476
+ if (!(indexField in fields)) {
477
+ throw new SchemaValidationError(
478
+ `Index field "${indexField}" does not exist in collection "${name}". Available fields: ${Object.keys(fields).join(", ")}`,
479
+ { collection: name, field: indexField }
480
+ );
481
+ }
482
+ }
483
+ const constraints = [];
484
+ if (input.constraints) {
485
+ for (const constraintInput of input.constraints) {
486
+ validateConstraint(name, constraintInput, fields);
487
+ constraints.push({ ...constraintInput });
488
+ }
489
+ }
490
+ const resolvers = {};
491
+ if (input.resolve) {
492
+ for (const [fieldName, resolver] of Object.entries(input.resolve)) {
493
+ if (!(fieldName in fields)) {
494
+ throw new SchemaValidationError(
495
+ `Resolver for field "${fieldName}" does not exist in collection "${name}". Available fields: ${Object.keys(fields).join(", ")}`,
496
+ { collection: name, field: fieldName }
497
+ );
498
+ }
499
+ if (typeof resolver !== "function") {
500
+ throw new SchemaValidationError(
501
+ `Resolver for field "${fieldName}" in collection "${name}" must be a function`,
502
+ { collection: name, field: fieldName }
503
+ );
504
+ }
505
+ resolvers[fieldName] = resolver;
506
+ }
507
+ }
508
+ return { fields, indexes, constraints, resolvers };
509
+ }
510
+ function validateFieldName(collection, fieldName) {
511
+ if (RESERVED_FIELDS.has(fieldName)) {
512
+ throw new SchemaValidationError(
513
+ `Field name "${fieldName}" is reserved in collection "${collection}". Reserved fields: ${[...RESERVED_FIELDS].join(", ")}`,
514
+ { collection, field: fieldName }
515
+ );
516
+ }
517
+ if (!FIELD_NAME_RE.test(fieldName)) {
518
+ throw new SchemaValidationError(
519
+ `Field name "${fieldName}" in collection "${collection}" is invalid. Must be lowercase, start with a letter, and contain only letters, numbers, and underscores.`,
520
+ { collection, field: fieldName }
521
+ );
522
+ }
523
+ }
524
+ function validateConstraint(collection, constraint, fields) {
525
+ for (const field of constraint.fields) {
526
+ if (!(field in fields)) {
527
+ throw new SchemaValidationError(
528
+ `Constraint references field "${field}" which does not exist in collection "${collection}". Available fields: ${Object.keys(fields).join(", ")}`,
529
+ { collection, field }
530
+ );
531
+ }
532
+ }
533
+ if (constraint.onConflict === "priority-field" && !constraint.priorityField) {
534
+ throw new SchemaValidationError(
535
+ `Constraint with "priority-field" onConflict strategy in collection "${collection}" requires a priorityField`,
536
+ { collection }
537
+ );
538
+ }
539
+ if (constraint.onConflict === "priority-field" && constraint.priorityField) {
540
+ if (!(constraint.priorityField in fields)) {
541
+ throw new SchemaValidationError(
542
+ `Constraint priorityField "${constraint.priorityField}" does not exist in collection "${collection}"`,
543
+ { collection, field: constraint.priorityField }
544
+ );
545
+ }
546
+ }
547
+ if (constraint.onConflict === "custom" && typeof constraint.resolve !== "function") {
548
+ throw new SchemaValidationError(
549
+ `Constraint with "custom" onConflict strategy in collection "${collection}" requires a resolve function`,
550
+ { collection }
551
+ );
552
+ }
553
+ }
554
+ function validateRelation(name, relation, collections) {
555
+ if (!(relation.from in collections)) {
556
+ throw new SchemaValidationError(
557
+ `Relation "${name}" references source collection "${relation.from}" which does not exist. Available collections: ${Object.keys(collections).join(", ")}`,
558
+ { relation: name, collection: relation.from }
559
+ );
560
+ }
561
+ if (!(relation.to in collections)) {
562
+ throw new SchemaValidationError(
563
+ `Relation "${name}" references target collection "${relation.to}" which does not exist. Available collections: ${Object.keys(collections).join(", ")}`,
564
+ { relation: name, collection: relation.to }
565
+ );
566
+ }
567
+ const fromCollection = collections[relation.from];
568
+ if (fromCollection && !(relation.field in fromCollection.fields)) {
569
+ throw new SchemaValidationError(
570
+ `Relation "${name}" references field "${relation.field}" which does not exist in collection "${relation.from}". Available fields: ${Object.keys(fromCollection.fields).join(", ")}`,
571
+ { relation: name, collection: relation.from, field: relation.field }
572
+ );
573
+ }
574
+ }
575
+
576
+ // src/schema/sql-gen.ts
577
+ function generateSQL(collectionName, collection, relations) {
578
+ const statements = [];
579
+ const columns = ["id TEXT PRIMARY KEY NOT NULL"];
580
+ const indexedFields = new Set(collection.indexes);
581
+ const fkFields = [];
582
+ for (const [fieldName, descriptor] of Object.entries(collection.fields)) {
583
+ let colDef = columnDefinition(fieldName, descriptor);
584
+ if (relations) {
585
+ for (const rel of Object.values(relations)) {
586
+ if (rel.from === collectionName && rel.field === fieldName) {
587
+ colDef += ` REFERENCES ${rel.to}(id)`;
588
+ fkFields.push(fieldName);
589
+ break;
590
+ }
591
+ }
592
+ }
593
+ columns.push(colDef);
594
+ }
595
+ columns.push("_created_at INTEGER NOT NULL");
596
+ columns.push("_updated_at INTEGER NOT NULL");
597
+ columns.push("_deleted INTEGER NOT NULL DEFAULT 0");
598
+ statements.push(`CREATE TABLE IF NOT EXISTS ${collectionName} (
599
+ ${columns.join(",\n ")}
600
+ )`);
601
+ for (const indexField of collection.indexes) {
602
+ statements.push(
603
+ `CREATE INDEX IF NOT EXISTS idx_${collectionName}_${indexField} ON ${collectionName} (${indexField})`
604
+ );
605
+ }
606
+ for (const fkField of fkFields) {
607
+ if (!indexedFields.has(fkField)) {
608
+ statements.push(
609
+ `CREATE INDEX IF NOT EXISTS idx_${collectionName}_${fkField} ON ${collectionName} (${fkField})`
610
+ );
611
+ }
612
+ }
613
+ statements.push(
614
+ `CREATE TABLE IF NOT EXISTS _kora_ops_${collectionName} (
615
+ id TEXT PRIMARY KEY NOT NULL,
616
+ node_id TEXT NOT NULL,
617
+ type TEXT NOT NULL,
618
+ record_id TEXT NOT NULL,
619
+ data TEXT,
620
+ previous_data TEXT,
621
+ timestamp TEXT NOT NULL,
622
+ sequence_number INTEGER NOT NULL,
623
+ causal_deps TEXT NOT NULL,
624
+ schema_version INTEGER NOT NULL
625
+ )`
626
+ );
627
+ return statements;
628
+ }
629
+ function generateFullDDL(schema) {
630
+ const statements = [];
631
+ statements.push(
632
+ "CREATE TABLE IF NOT EXISTS _kora_meta (\n key TEXT PRIMARY KEY NOT NULL,\n value TEXT NOT NULL\n)"
633
+ );
634
+ statements.push(
635
+ "CREATE TABLE IF NOT EXISTS _kora_version_vector (\n node_id TEXT PRIMARY KEY NOT NULL,\n sequence_number INTEGER NOT NULL\n)"
636
+ );
637
+ for (const [name, collection] of Object.entries(schema.collections)) {
638
+ statements.push(...generateSQL(name, collection, schema.relations));
639
+ }
640
+ return statements;
641
+ }
642
+ function columnDefinition(fieldName, descriptor) {
643
+ const sqlType = mapFieldType(descriptor);
644
+ const parts = [fieldName, sqlType];
645
+ if (descriptor.required && descriptor.defaultValue === void 0 && !descriptor.auto) {
646
+ parts.push("NOT NULL");
647
+ }
648
+ if (descriptor.defaultValue !== void 0) {
649
+ parts.push(`DEFAULT ${sqlDefault(descriptor.defaultValue)}`);
650
+ }
651
+ if (descriptor.kind === "enum" && descriptor.enumValues) {
652
+ const values = descriptor.enumValues.map((v) => `'${v}'`).join(", ");
653
+ parts.push(`CHECK (${fieldName} IN (${values}))`);
654
+ }
655
+ return parts.join(" ");
656
+ }
657
+ function mapFieldType(descriptor) {
658
+ switch (descriptor.kind) {
659
+ case "string":
660
+ return "TEXT";
661
+ case "number":
662
+ return "REAL";
663
+ case "boolean":
664
+ return "INTEGER";
665
+ case "enum":
666
+ return "TEXT";
667
+ case "timestamp":
668
+ return "INTEGER";
669
+ case "array":
670
+ return "TEXT";
671
+ // JSON-serialized
672
+ case "richtext":
673
+ return "BLOB";
674
+ }
675
+ }
676
+ function sqlDefault(value) {
677
+ if (value === null) return "NULL";
678
+ if (typeof value === "string") return `'${value}'`;
679
+ if (typeof value === "number") return String(value);
680
+ if (typeof value === "boolean") return value ? "1" : "0";
681
+ return `'${JSON.stringify(value)}'`;
682
+ }
683
+
684
+ // src/schema/types.ts
685
+ var FieldBuilder = class _FieldBuilder {
686
+ _kind;
687
+ _required;
688
+ _defaultValue;
689
+ _auto;
690
+ constructor(kind, required = true, defaultValue = void 0, auto = false) {
691
+ this._kind = kind;
692
+ this._required = required;
693
+ this._defaultValue = defaultValue;
694
+ this._auto = auto;
695
+ }
696
+ /** Mark this field as optional (not required on insert) */
697
+ optional() {
698
+ return new _FieldBuilder(this._kind, false, this._defaultValue, this._auto);
699
+ }
700
+ /** Set a default value for this field. Implicitly makes the field optional. */
701
+ default(value) {
702
+ return new _FieldBuilder(this._kind, false, value, this._auto);
703
+ }
704
+ /** Mark this field as auto-populated (e.g., createdAt timestamps). Developers cannot set auto fields. */
705
+ auto() {
706
+ return new _FieldBuilder(this._kind, false, void 0, true);
707
+ }
708
+ /** @internal Build the final FieldDescriptor. Used by defineSchema(). */
709
+ _build() {
710
+ return {
711
+ kind: this._kind,
712
+ required: this._required,
713
+ defaultValue: this._defaultValue,
714
+ auto: this._auto,
715
+ enumValues: null,
716
+ itemKind: null
717
+ };
718
+ }
719
+ };
720
+ var EnumFieldBuilder = class _EnumFieldBuilder extends FieldBuilder {
721
+ _enumValues;
722
+ constructor(values, required = true, defaultValue = void 0, auto = false) {
723
+ super("enum", required, defaultValue, auto);
724
+ this._enumValues = values;
725
+ }
726
+ optional() {
727
+ return new _EnumFieldBuilder(this._enumValues, false, this._defaultValue, this._auto);
728
+ }
729
+ default(value) {
730
+ return new _EnumFieldBuilder(this._enumValues, false, value, this._auto);
731
+ }
732
+ auto() {
733
+ return new _EnumFieldBuilder(this._enumValues, false, void 0, true);
734
+ }
735
+ _build() {
736
+ return {
737
+ kind: "enum",
738
+ required: this._required,
739
+ defaultValue: this._defaultValue,
740
+ auto: this._auto,
741
+ enumValues: this._enumValues,
742
+ itemKind: null
743
+ };
744
+ }
745
+ };
746
+ var ArrayFieldBuilder = class _ArrayFieldBuilder extends FieldBuilder {
747
+ _itemKind;
748
+ constructor(itemBuilder, required = true, defaultValue = void 0, auto = false) {
749
+ super("array", required, defaultValue, auto);
750
+ this._itemKind = itemBuilder._build().kind;
751
+ }
752
+ optional() {
753
+ return new _ArrayFieldBuilder(
754
+ new FieldBuilder(this._itemKind),
755
+ false,
756
+ this._defaultValue,
757
+ this._auto
758
+ );
759
+ }
760
+ default(value) {
761
+ return new _ArrayFieldBuilder(new FieldBuilder(this._itemKind), false, value, this._auto);
762
+ }
763
+ auto() {
764
+ return new _ArrayFieldBuilder(new FieldBuilder(this._itemKind), false, void 0, true);
765
+ }
766
+ _build() {
767
+ return {
768
+ kind: "array",
769
+ required: this._required,
770
+ defaultValue: this._defaultValue,
771
+ auto: this._auto,
772
+ enumValues: null,
773
+ itemKind: this._itemKind
774
+ };
775
+ }
776
+ };
777
+ var t = {
778
+ string() {
779
+ return new FieldBuilder("string", true, void 0, false);
780
+ },
781
+ number() {
782
+ return new FieldBuilder("number", true, void 0, false);
783
+ },
784
+ boolean() {
785
+ return new FieldBuilder("boolean", true, void 0, false);
786
+ },
787
+ timestamp() {
788
+ return new FieldBuilder("timestamp", true, void 0, false);
789
+ },
790
+ richtext() {
791
+ return new FieldBuilder("richtext", true, void 0, false);
792
+ },
793
+ enum(values) {
794
+ return new EnumFieldBuilder(values, true, void 0, false);
795
+ },
796
+ array(itemBuilder) {
797
+ return new ArrayFieldBuilder(itemBuilder, true, void 0, false);
798
+ }
799
+ };
800
+
801
+ // src/schema/validation.ts
802
+ function validateRecord(collection, collectionDef, data, operationType) {
803
+ if (operationType === "delete") {
804
+ return {};
805
+ }
806
+ const result = {};
807
+ const fieldNames = Object.keys(collectionDef.fields);
808
+ for (const key of Object.keys(data)) {
809
+ if (!(key in collectionDef.fields)) {
810
+ throw new SchemaValidationError(
811
+ `Unknown field "${key}" in collection "${collection}". Available fields: ${fieldNames.join(", ")}`,
812
+ { collection, field: key }
813
+ );
814
+ }
815
+ }
816
+ for (const [fieldName, descriptor] of Object.entries(collectionDef.fields)) {
817
+ const value = data[fieldName];
818
+ const hasValue = fieldName in data;
819
+ if (descriptor.auto && hasValue) {
820
+ throw new SchemaValidationError(
821
+ `Field "${fieldName}" in collection "${collection}" is auto-populated and cannot be set manually`,
822
+ { collection, field: fieldName }
823
+ );
824
+ }
825
+ if (operationType === "update") {
826
+ if (hasValue) {
827
+ if (value !== void 0 && value !== null) {
828
+ validateFieldValue(collection, fieldName, descriptor, value);
829
+ }
830
+ result[fieldName] = value;
831
+ }
832
+ continue;
833
+ }
834
+ if (descriptor.auto) {
835
+ continue;
836
+ }
837
+ if (!hasValue || value === void 0) {
838
+ if (descriptor.defaultValue !== void 0) {
839
+ result[fieldName] = typeof descriptor.defaultValue === "object" && descriptor.defaultValue !== null ? JSON.parse(JSON.stringify(descriptor.defaultValue)) : descriptor.defaultValue;
840
+ continue;
841
+ }
842
+ if (descriptor.required) {
843
+ throw new SchemaValidationError(
844
+ `Required field "${fieldName}" is missing in collection "${collection}"`,
845
+ { collection, field: fieldName }
846
+ );
847
+ }
848
+ continue;
849
+ }
850
+ validateFieldValue(collection, fieldName, descriptor, value);
851
+ result[fieldName] = value;
852
+ }
853
+ return result;
854
+ }
855
+ function validateFieldValue(collection, fieldName, descriptor, value) {
856
+ switch (descriptor.kind) {
857
+ case "string": {
858
+ if (typeof value !== "string") {
859
+ throw new SchemaValidationError(
860
+ `Field "${fieldName}" in collection "${collection}" must be a string, got ${typeof value}`,
861
+ { collection, field: fieldName, expectedType: "string", receivedType: typeof value }
862
+ );
863
+ }
864
+ break;
865
+ }
866
+ case "number": {
867
+ if (typeof value !== "number" || Number.isNaN(value)) {
868
+ throw new SchemaValidationError(
869
+ `Field "${fieldName}" in collection "${collection}" must be a number, got ${typeof value}`,
870
+ { collection, field: fieldName, expectedType: "number", receivedType: typeof value }
871
+ );
872
+ }
873
+ break;
874
+ }
875
+ case "boolean": {
876
+ if (typeof value !== "boolean") {
877
+ throw new SchemaValidationError(
878
+ `Field "${fieldName}" in collection "${collection}" must be a boolean, got ${typeof value}`,
879
+ { collection, field: fieldName, expectedType: "boolean", receivedType: typeof value }
880
+ );
881
+ }
882
+ break;
883
+ }
884
+ case "timestamp": {
885
+ if (typeof value !== "number" || !Number.isFinite(value)) {
886
+ throw new SchemaValidationError(
887
+ `Field "${fieldName}" in collection "${collection}" must be a timestamp (number), got ${typeof value}`,
888
+ {
889
+ collection,
890
+ field: fieldName,
891
+ expectedType: "timestamp",
892
+ receivedType: typeof value
893
+ }
894
+ );
895
+ }
896
+ break;
897
+ }
898
+ case "enum": {
899
+ if (typeof value !== "string") {
900
+ throw new SchemaValidationError(
901
+ `Field "${fieldName}" in collection "${collection}" must be a string (enum), got ${typeof value}`,
902
+ { collection, field: fieldName, expectedType: "enum", receivedType: typeof value }
903
+ );
904
+ }
905
+ if (descriptor.enumValues && !descriptor.enumValues.includes(value)) {
906
+ throw new SchemaValidationError(
907
+ `Field "${fieldName}" in collection "${collection}" must be one of: ${descriptor.enumValues.join(", ")}. Got "${value}"`,
908
+ {
909
+ collection,
910
+ field: fieldName,
911
+ allowedValues: [...descriptor.enumValues],
912
+ received: value
913
+ }
914
+ );
915
+ }
916
+ break;
917
+ }
918
+ case "array": {
919
+ if (!Array.isArray(value)) {
920
+ throw new SchemaValidationError(
921
+ `Field "${fieldName}" in collection "${collection}" must be an array, got ${typeof value}`,
922
+ { collection, field: fieldName, expectedType: "array", receivedType: typeof value }
923
+ );
924
+ }
925
+ if (descriptor.itemKind) {
926
+ const expectedType = jsTypeForKind(descriptor.itemKind);
927
+ for (let i = 0; i < value.length; i++) {
928
+ const item = value[i];
929
+ if (!matchesJsType(item, expectedType)) {
930
+ throw new SchemaValidationError(
931
+ `Field "${fieldName}[${i}]" in collection "${collection}" must be a ${descriptor.itemKind}, got ${typeof item}`,
932
+ {
933
+ collection,
934
+ field: `${fieldName}[${i}]`,
935
+ expectedType: descriptor.itemKind,
936
+ receivedType: typeof item
937
+ }
938
+ );
939
+ }
940
+ }
941
+ }
942
+ break;
943
+ }
944
+ case "richtext": {
945
+ if (!(value instanceof Uint8Array) && typeof value !== "string") {
946
+ throw new SchemaValidationError(
947
+ `Field "${fieldName}" in collection "${collection}" must be a Uint8Array or string for richtext, got ${typeof value}`,
948
+ {
949
+ collection,
950
+ field: fieldName,
951
+ expectedType: "richtext",
952
+ receivedType: typeof value
953
+ }
954
+ );
955
+ }
956
+ break;
957
+ }
958
+ }
959
+ }
960
+ function jsTypeForKind(kind) {
961
+ switch (kind) {
962
+ case "string":
963
+ case "enum":
964
+ return "string";
965
+ case "number":
966
+ case "timestamp":
967
+ return "number";
968
+ case "boolean":
969
+ return "boolean";
970
+ default:
971
+ return "object";
972
+ }
973
+ }
974
+ function matchesJsType(value, expected) {
975
+ switch (expected) {
976
+ case "string":
977
+ return typeof value === "string";
978
+ case "number":
979
+ return typeof value === "number";
980
+ case "boolean":
981
+ return typeof value === "boolean";
982
+ case "object":
983
+ return typeof value === "object";
984
+ default:
985
+ return false;
986
+ }
987
+ }
988
+
989
+ // src/version-vector/topological-sort.ts
990
+ function topologicalSort(operations) {
991
+ if (operations.length <= 1) return [...operations];
992
+ const opMap = /* @__PURE__ */ new Map();
993
+ for (const op of operations) {
994
+ opMap.set(op.id, op);
995
+ }
996
+ const inDegree = /* @__PURE__ */ new Map();
997
+ const dependents = /* @__PURE__ */ new Map();
998
+ for (const op of operations) {
999
+ if (!inDegree.has(op.id)) {
1000
+ inDegree.set(op.id, 0);
1001
+ }
1002
+ if (!dependents.has(op.id)) {
1003
+ dependents.set(op.id, []);
1004
+ }
1005
+ for (const depId of op.causalDeps) {
1006
+ if (opMap.has(depId)) {
1007
+ inDegree.set(op.id, (inDegree.get(op.id) ?? 0) + 1);
1008
+ const deps = dependents.get(depId);
1009
+ if (deps) {
1010
+ deps.push(op.id);
1011
+ } else {
1012
+ dependents.set(depId, [op.id]);
1013
+ }
1014
+ }
1015
+ }
1016
+ }
1017
+ const queue = [];
1018
+ for (const op of operations) {
1019
+ if ((inDegree.get(op.id) ?? 0) === 0) {
1020
+ queue.push(op);
1021
+ }
1022
+ }
1023
+ queue.sort(compareByTimestamp);
1024
+ const result = [];
1025
+ while (queue.length > 0) {
1026
+ const current = queue.shift();
1027
+ if (!current) break;
1028
+ result.push(current);
1029
+ const deps = dependents.get(current.id) ?? [];
1030
+ const newlyReady = [];
1031
+ for (const depId of deps) {
1032
+ const deg = (inDegree.get(depId) ?? 0) - 1;
1033
+ inDegree.set(depId, deg);
1034
+ if (deg === 0) {
1035
+ const op = opMap.get(depId);
1036
+ if (op) newlyReady.push(op);
1037
+ }
1038
+ }
1039
+ if (newlyReady.length > 0) {
1040
+ newlyReady.sort(compareByTimestamp);
1041
+ mergeIntoSorted(queue, newlyReady);
1042
+ }
1043
+ }
1044
+ if (result.length !== operations.length) {
1045
+ throw new OperationError(
1046
+ `Cycle detected in operation dependency graph. Sorted ${result.length} of ${operations.length} operations.`,
1047
+ {
1048
+ sortedCount: result.length,
1049
+ totalCount: operations.length
1050
+ }
1051
+ );
1052
+ }
1053
+ return result;
1054
+ }
1055
+ function compareByTimestamp(a, b) {
1056
+ return HybridLogicalClock.compare(a.timestamp, b.timestamp);
1057
+ }
1058
+ function mergeIntoSorted(target, items) {
1059
+ let insertIndex = 0;
1060
+ for (const item of items) {
1061
+ while (insertIndex < target.length) {
1062
+ const existing = target[insertIndex];
1063
+ if (existing && compareByTimestamp(item, existing) <= 0) break;
1064
+ insertIndex++;
1065
+ }
1066
+ target.splice(insertIndex, 0, item);
1067
+ insertIndex++;
1068
+ }
1069
+ }
1070
+
1071
+ // src/version-vector/version-vector.ts
1072
+ function createVersionVector() {
1073
+ return /* @__PURE__ */ new Map();
1074
+ }
1075
+ function mergeVectors(a, b) {
1076
+ const merged = new Map(a);
1077
+ for (const [nodeId, seq] of b) {
1078
+ merged.set(nodeId, Math.max(merged.get(nodeId) ?? 0, seq));
1079
+ }
1080
+ return merged;
1081
+ }
1082
+ function advanceVector(vector, nodeId, seq) {
1083
+ const updated = new Map(vector);
1084
+ updated.set(nodeId, Math.max(updated.get(nodeId) ?? 0, seq));
1085
+ return updated;
1086
+ }
1087
+ function dominates(a, b) {
1088
+ for (const [nodeId, bSeq] of b) {
1089
+ if ((a.get(nodeId) ?? 0) < bSeq) return false;
1090
+ }
1091
+ return true;
1092
+ }
1093
+ function vectorsEqual(a, b) {
1094
+ if (a.size !== b.size) return false;
1095
+ for (const [nodeId, aSeq] of a) {
1096
+ if (b.get(nodeId) !== aSeq) return false;
1097
+ }
1098
+ return true;
1099
+ }
1100
+ function computeDelta(localVector, remoteVector, operationLog) {
1101
+ const missing = [];
1102
+ for (const [nodeId, localSeq] of localVector) {
1103
+ const remoteSeq = remoteVector.get(nodeId) ?? 0;
1104
+ if (localSeq > remoteSeq) {
1105
+ missing.push(...operationLog.getRange(nodeId, remoteSeq + 1, localSeq));
1106
+ }
1107
+ }
1108
+ return topologicalSort(missing);
1109
+ }
1110
+ function serializeVector(vector) {
1111
+ const entries = [...vector.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
1112
+ return JSON.stringify(entries);
1113
+ }
1114
+ function deserializeVector(s) {
1115
+ const entries = JSON.parse(s);
1116
+ return new Map(entries);
1117
+ }
1118
+ // Annotate the CommonJS export names for ESM import in node:
1119
+ 0 && (module.exports = {
1120
+ ArrayFieldBuilder,
1121
+ CONNECTION_QUALITIES,
1122
+ ClockDriftError,
1123
+ EnumFieldBuilder,
1124
+ FieldBuilder,
1125
+ HybridLogicalClock,
1126
+ KoraError,
1127
+ MERGE_STRATEGIES,
1128
+ MergeConflictError,
1129
+ OperationError,
1130
+ SchemaValidationError,
1131
+ StorageError,
1132
+ SyncError,
1133
+ advanceVector,
1134
+ computeDelta,
1135
+ createOperation,
1136
+ createVersionVector,
1137
+ defineSchema,
1138
+ deserializeVector,
1139
+ dominates,
1140
+ extractTimestamp,
1141
+ generateFullDDL,
1142
+ generateSQL,
1143
+ generateUUIDv7,
1144
+ isValidOperation,
1145
+ isValidUUIDv7,
1146
+ mergeVectors,
1147
+ serializeVector,
1148
+ t,
1149
+ validateRecord,
1150
+ vectorsEqual,
1151
+ verifyOperationIntegrity
1152
+ });
1153
+ //# sourceMappingURL=index.cjs.map