@korajs/store 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.
Files changed (43) hide show
  1. package/dist/adapters/better-sqlite3.cjs +166 -0
  2. package/dist/adapters/better-sqlite3.cjs.map +1 -0
  3. package/dist/adapters/better-sqlite3.d.cts +31 -0
  4. package/dist/adapters/better-sqlite3.d.ts +31 -0
  5. package/dist/adapters/better-sqlite3.js +117 -0
  6. package/dist/adapters/better-sqlite3.js.map +1 -0
  7. package/dist/adapters/indexeddb.cjs +550 -0
  8. package/dist/adapters/indexeddb.cjs.map +1 -0
  9. package/dist/adapters/indexeddb.d.cts +52 -0
  10. package/dist/adapters/indexeddb.d.ts +52 -0
  11. package/dist/adapters/indexeddb.js +205 -0
  12. package/dist/adapters/indexeddb.js.map +1 -0
  13. package/dist/adapters/sqlite-wasm-worker.cjs +215 -0
  14. package/dist/adapters/sqlite-wasm-worker.cjs.map +1 -0
  15. package/dist/adapters/sqlite-wasm-worker.d.cts +2 -0
  16. package/dist/adapters/sqlite-wasm-worker.d.ts +2 -0
  17. package/dist/adapters/sqlite-wasm-worker.js +191 -0
  18. package/dist/adapters/sqlite-wasm-worker.js.map +1 -0
  19. package/dist/adapters/sqlite-wasm.cjs +354 -0
  20. package/dist/adapters/sqlite-wasm.cjs.map +1 -0
  21. package/dist/adapters/sqlite-wasm.d.cts +68 -0
  22. package/dist/adapters/sqlite-wasm.d.ts +68 -0
  23. package/dist/adapters/sqlite-wasm.js +14 -0
  24. package/dist/adapters/sqlite-wasm.js.map +1 -0
  25. package/dist/chunk-DXKLAQ6P.js +111 -0
  26. package/dist/chunk-DXKLAQ6P.js.map +1 -0
  27. package/dist/chunk-LAWV6CFH.js +62 -0
  28. package/dist/chunk-LAWV6CFH.js.map +1 -0
  29. package/dist/chunk-ZP5AXQ3Z.js +179 -0
  30. package/dist/chunk-ZP5AXQ3Z.js.map +1 -0
  31. package/dist/index.cjs +1288 -0
  32. package/dist/index.cjs.map +1 -0
  33. package/dist/index.d.cts +376 -0
  34. package/dist/index.d.ts +376 -0
  35. package/dist/index.js +1194 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/sqlite-wasm-channel-46AOWNPM.js +10 -0
  38. package/dist/sqlite-wasm-channel-46AOWNPM.js.map +1 -0
  39. package/dist/sqlite-wasm-channel-Lakjuk2E.d.cts +104 -0
  40. package/dist/sqlite-wasm-channel-Lakjuk2E.d.ts +104 -0
  41. package/dist/types-DF-KDSK1.d.cts +106 -0
  42. package/dist/types-DF-KDSK1.d.ts +106 -0
  43. package/package.json +95 -0
package/dist/index.cjs ADDED
@@ -0,0 +1,1288 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ AdapterError: () => AdapterError,
34
+ Collection: () => Collection,
35
+ PersistenceError: () => PersistenceError,
36
+ QueryBuilder: () => QueryBuilder,
37
+ QueryError: () => QueryError,
38
+ RecordNotFoundError: () => RecordNotFoundError,
39
+ Store: () => Store,
40
+ StoreNotOpenError: () => StoreNotOpenError,
41
+ SubscriptionManager: () => SubscriptionManager,
42
+ WorkerInitError: () => WorkerInitError,
43
+ WorkerTimeoutError: () => WorkerTimeoutError,
44
+ decodeRichtext: () => decodeRichtext,
45
+ encodeRichtext: () => encodeRichtext,
46
+ pluralize: () => pluralize,
47
+ richtextToPlainText: () => richtextToPlainText,
48
+ singularize: () => singularize
49
+ });
50
+ module.exports = __toCommonJS(index_exports);
51
+
52
+ // src/errors.ts
53
+ var import_core = require("@korajs/core");
54
+ var QueryError = class extends import_core.KoraError {
55
+ constructor(message, context) {
56
+ super(message, "QUERY_ERROR", context);
57
+ this.name = "QueryError";
58
+ }
59
+ };
60
+ var RecordNotFoundError = class extends import_core.KoraError {
61
+ constructor(collection, recordId) {
62
+ super(`Record "${recordId}" not found in collection "${collection}"`, "RECORD_NOT_FOUND", {
63
+ collection,
64
+ recordId
65
+ });
66
+ this.name = "RecordNotFoundError";
67
+ }
68
+ };
69
+ var AdapterError = class extends import_core.KoraError {
70
+ constructor(message, context) {
71
+ super(message, "ADAPTER_ERROR", context);
72
+ this.name = "AdapterError";
73
+ }
74
+ };
75
+ var StoreNotOpenError = class extends import_core.KoraError {
76
+ constructor() {
77
+ super("Store is not open. Call store.open() before performing operations.", "STORE_NOT_OPEN");
78
+ this.name = "StoreNotOpenError";
79
+ }
80
+ };
81
+ var WorkerInitError = class extends import_core.KoraError {
82
+ constructor(message, context) {
83
+ super(`Worker initialization failed: ${message}`, "WORKER_INIT_ERROR", context);
84
+ this.name = "WorkerInitError";
85
+ }
86
+ };
87
+ var WorkerTimeoutError = class extends import_core.KoraError {
88
+ constructor(operation, timeoutMs) {
89
+ super(
90
+ `Worker did not respond within ${timeoutMs}ms for operation "${operation}"`,
91
+ "WORKER_TIMEOUT",
92
+ { operation, timeoutMs }
93
+ );
94
+ this.name = "WorkerTimeoutError";
95
+ }
96
+ };
97
+ var PersistenceError = class extends import_core.KoraError {
98
+ constructor(message, context) {
99
+ super(`Persistence error: ${message}`, "PERSISTENCE_ERROR", context);
100
+ this.name = "PersistenceError";
101
+ }
102
+ };
103
+
104
+ // src/store/store.ts
105
+ var import_core4 = require("@korajs/core");
106
+
107
+ // src/collection/collection.ts
108
+ var import_core3 = require("@korajs/core");
109
+
110
+ // src/query/sql-builder.ts
111
+ function buildSelectQuery(descriptor, fields) {
112
+ const params = [];
113
+ const parts = [`SELECT * FROM ${descriptor.collection}`];
114
+ const whereClause = buildWhereClauseParts(descriptor.where, fields, params);
115
+ const deletedFilter = "_deleted = 0";
116
+ if (whereClause) {
117
+ parts.push(`WHERE ${deletedFilter} AND ${whereClause}`);
118
+ } else {
119
+ parts.push(`WHERE ${deletedFilter}`);
120
+ }
121
+ if (descriptor.orderBy.length > 0) {
122
+ const orderParts = descriptor.orderBy.map((o) => {
123
+ validateFieldName(o.field, fields);
124
+ return `${o.field} ${o.direction.toUpperCase()}`;
125
+ });
126
+ parts.push(`ORDER BY ${orderParts.join(", ")}`);
127
+ }
128
+ if (descriptor.limit !== void 0) {
129
+ parts.push(`LIMIT ${descriptor.limit}`);
130
+ }
131
+ if (descriptor.offset !== void 0) {
132
+ parts.push(`OFFSET ${descriptor.offset}`);
133
+ }
134
+ return { sql: parts.join(" "), params };
135
+ }
136
+ function buildCountQuery(descriptor, fields) {
137
+ const params = [];
138
+ const parts = [`SELECT COUNT(*) as count FROM ${descriptor.collection}`];
139
+ const whereClause = buildWhereClauseParts(descriptor.where, fields, params);
140
+ const deletedFilter = "_deleted = 0";
141
+ if (whereClause) {
142
+ parts.push(`WHERE ${deletedFilter} AND ${whereClause}`);
143
+ } else {
144
+ parts.push(`WHERE ${deletedFilter}`);
145
+ }
146
+ return { sql: parts.join(" "), params };
147
+ }
148
+ function buildInsertQuery(collection, record) {
149
+ const columns = Object.keys(record);
150
+ const placeholders = columns.map(() => "?");
151
+ const params = Object.values(record);
152
+ const sql = `INSERT INTO ${collection} (${columns.join(", ")}) VALUES (${placeholders.join(", ")})`;
153
+ return { sql, params };
154
+ }
155
+ function buildUpdateQuery(collection, id, changes) {
156
+ const setClauses = Object.keys(changes).map((col) => `${col} = ?`);
157
+ const params = [...Object.values(changes), id];
158
+ const sql = `UPDATE ${collection} SET ${setClauses.join(", ")} WHERE id = ?`;
159
+ return { sql, params };
160
+ }
161
+ function buildSoftDeleteQuery(collection, id, updatedAt) {
162
+ return {
163
+ sql: `UPDATE ${collection} SET _deleted = 1, _updated_at = ? WHERE id = ?`,
164
+ params: [updatedAt, id]
165
+ };
166
+ }
167
+ var VALID_OPERATORS = /* @__PURE__ */ new Set(["$eq", "$ne", "$gt", "$gte", "$lt", "$lte", "$in"]);
168
+ function buildWhereClauseParts(where, fields, params) {
169
+ const conditions = [];
170
+ for (const [fieldName, value] of Object.entries(where)) {
171
+ validateFieldName(fieldName, fields);
172
+ const descriptor = fields[fieldName];
173
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
174
+ const ops = value;
175
+ for (const [op, opValue] of Object.entries(ops)) {
176
+ if (!VALID_OPERATORS.has(op)) {
177
+ throw new QueryError(`Unknown operator "${op}" on field "${fieldName}"`, {
178
+ field: fieldName,
179
+ operator: op,
180
+ validOperators: [...VALID_OPERATORS]
181
+ });
182
+ }
183
+ conditions.push(buildOperatorCondition(fieldName, op, opValue, descriptor, params));
184
+ }
185
+ } else {
186
+ conditions.push(buildOperatorCondition(fieldName, "$eq", value, descriptor, params));
187
+ }
188
+ }
189
+ if (conditions.length === 0) return null;
190
+ return conditions.join(" AND ");
191
+ }
192
+ function buildOperatorCondition(fieldName, operator, value, descriptor, params) {
193
+ const sqlValue = descriptor?.kind === "boolean" && typeof value === "boolean" ? value ? 1 : 0 : value;
194
+ switch (operator) {
195
+ case "$eq":
196
+ if (sqlValue === null) {
197
+ return `${fieldName} IS NULL`;
198
+ }
199
+ params.push(sqlValue);
200
+ return `${fieldName} = ?`;
201
+ case "$ne":
202
+ if (sqlValue === null) {
203
+ return `${fieldName} IS NOT NULL`;
204
+ }
205
+ params.push(sqlValue);
206
+ return `${fieldName} != ?`;
207
+ case "$gt":
208
+ params.push(sqlValue);
209
+ return `${fieldName} > ?`;
210
+ case "$gte":
211
+ params.push(sqlValue);
212
+ return `${fieldName} >= ?`;
213
+ case "$lt":
214
+ params.push(sqlValue);
215
+ return `${fieldName} < ?`;
216
+ case "$lte":
217
+ params.push(sqlValue);
218
+ return `${fieldName} <= ?`;
219
+ case "$in": {
220
+ if (!Array.isArray(sqlValue)) {
221
+ throw new QueryError(`$in operator requires an array value for field "${fieldName}"`, {
222
+ field: fieldName,
223
+ received: typeof sqlValue
224
+ });
225
+ }
226
+ const placeholders = sqlValue.map(() => "?");
227
+ for (const item of sqlValue) {
228
+ params.push(
229
+ descriptor?.kind === "boolean" && typeof item === "boolean" ? item ? 1 : 0 : item
230
+ );
231
+ }
232
+ return `${fieldName} IN (${placeholders.join(", ")})`;
233
+ }
234
+ default:
235
+ throw new QueryError(`Unknown operator "${operator}"`, { operator });
236
+ }
237
+ }
238
+ function validateFieldName(fieldName, fields) {
239
+ const allowedFields = /* @__PURE__ */ new Set([
240
+ ...Object.keys(fields),
241
+ "id",
242
+ "createdAt",
243
+ "updatedAt",
244
+ "_created_at",
245
+ "_updated_at"
246
+ ]);
247
+ if (!allowedFields.has(fieldName)) {
248
+ throw new QueryError(
249
+ `Unknown field "${fieldName}" in query. Available fields: ${[...allowedFields].join(", ")}`,
250
+ { field: fieldName }
251
+ );
252
+ }
253
+ }
254
+
255
+ // src/serialization/serializer.ts
256
+ var import_core2 = require("@korajs/core");
257
+
258
+ // src/serialization/richtext-serializer.ts
259
+ var Y = __toESM(require("yjs"), 1);
260
+ var TEXT_KEY = "content";
261
+ function encodeRichtext(value) {
262
+ if (value === null || value === void 0) {
263
+ return null;
264
+ }
265
+ if (typeof value === "string") {
266
+ const doc = new Y.Doc();
267
+ doc.getText(TEXT_KEY).insert(0, value);
268
+ return Y.encodeStateAsUpdate(doc);
269
+ }
270
+ if (value instanceof Uint8Array) {
271
+ return value;
272
+ }
273
+ if (value instanceof ArrayBuffer) {
274
+ return new Uint8Array(value);
275
+ }
276
+ throw new Error("Richtext value must be a string, Uint8Array, ArrayBuffer, null, or undefined.");
277
+ }
278
+ function decodeRichtext(value) {
279
+ if (value === null || value === void 0) {
280
+ return null;
281
+ }
282
+ if (typeof Buffer !== "undefined" && Buffer.isBuffer(value)) {
283
+ return new Uint8Array(value);
284
+ }
285
+ if (value instanceof Uint8Array) {
286
+ return value;
287
+ }
288
+ if (value instanceof ArrayBuffer) {
289
+ return new Uint8Array(value);
290
+ }
291
+ throw new Error("Richtext storage value must be Uint8Array, ArrayBuffer, Buffer, null, or undefined.");
292
+ }
293
+ function richtextToPlainText(value) {
294
+ const encoded = encodeRichtext(value);
295
+ if (!encoded) return "";
296
+ const doc = new Y.Doc();
297
+ Y.applyUpdate(doc, encoded);
298
+ return doc.getText(TEXT_KEY).toString();
299
+ }
300
+
301
+ // src/serialization/serializer.ts
302
+ function serializeRecord(data, fields) {
303
+ const result = {};
304
+ for (const [key, value] of Object.entries(data)) {
305
+ const descriptor = fields[key];
306
+ if (!descriptor) {
307
+ result[key] = value;
308
+ continue;
309
+ }
310
+ result[key] = serializeValue(value, descriptor);
311
+ }
312
+ return result;
313
+ }
314
+ function deserializeRecord(row, fields) {
315
+ const result = {
316
+ id: row.id,
317
+ createdAt: row._created_at,
318
+ updatedAt: row._updated_at
319
+ };
320
+ for (const [key, descriptor] of Object.entries(fields)) {
321
+ const rawValue = row[key];
322
+ if (rawValue === void 0 || rawValue === null) {
323
+ result[key] = rawValue ?? null;
324
+ continue;
325
+ }
326
+ result[key] = deserializeValue(rawValue, descriptor);
327
+ }
328
+ return result;
329
+ }
330
+ function serializeOperation(op) {
331
+ return {
332
+ id: op.id,
333
+ node_id: op.nodeId,
334
+ type: op.type,
335
+ record_id: op.recordId,
336
+ data: op.data ? JSON.stringify(op.data) : null,
337
+ previous_data: op.previousData ? JSON.stringify(op.previousData) : null,
338
+ timestamp: import_core2.HybridLogicalClock.serialize(op.timestamp),
339
+ sequence_number: op.sequenceNumber,
340
+ causal_deps: JSON.stringify(op.causalDeps),
341
+ schema_version: op.schemaVersion
342
+ };
343
+ }
344
+ function deserializeOperation(row) {
345
+ return {
346
+ id: row.id,
347
+ nodeId: row.node_id,
348
+ type: row.type,
349
+ collection: "",
350
+ // Collection name is derived from the table name by the caller
351
+ recordId: row.record_id,
352
+ data: row.data ? JSON.parse(row.data) : null,
353
+ previousData: row.previous_data ? JSON.parse(row.previous_data) : null,
354
+ timestamp: import_core2.HybridLogicalClock.deserialize(row.timestamp),
355
+ sequenceNumber: row.sequence_number,
356
+ causalDeps: JSON.parse(row.causal_deps),
357
+ schemaVersion: row.schema_version
358
+ };
359
+ }
360
+ function deserializeOperationWithCollection(row, collection) {
361
+ const op = deserializeOperation(row);
362
+ return { ...op, collection };
363
+ }
364
+ function serializeValue(value, descriptor) {
365
+ if (value === null || value === void 0) {
366
+ return null;
367
+ }
368
+ switch (descriptor.kind) {
369
+ case "boolean":
370
+ return value ? 1 : 0;
371
+ case "array":
372
+ return JSON.stringify(value);
373
+ case "richtext":
374
+ return encodeRichtext(value);
375
+ default:
376
+ return value;
377
+ }
378
+ }
379
+ function deserializeValue(value, descriptor) {
380
+ switch (descriptor.kind) {
381
+ case "boolean":
382
+ return value === 1 || value === true;
383
+ case "array":
384
+ if (typeof value === "string") {
385
+ return JSON.parse(value);
386
+ }
387
+ return value;
388
+ case "richtext":
389
+ return decodeRichtext(value);
390
+ default:
391
+ return value;
392
+ }
393
+ }
394
+
395
+ // src/collection/collection.ts
396
+ var Collection = class {
397
+ constructor(name, definition, schema, adapter, clock, nodeId, getSequenceNumber, onMutation) {
398
+ this.name = name;
399
+ this.definition = definition;
400
+ this.schema = schema;
401
+ this.adapter = adapter;
402
+ this.clock = clock;
403
+ this.nodeId = nodeId;
404
+ this.getSequenceNumber = getSequenceNumber;
405
+ this.onMutation = onMutation;
406
+ }
407
+ name;
408
+ definition;
409
+ schema;
410
+ adapter;
411
+ clock;
412
+ nodeId;
413
+ getSequenceNumber;
414
+ onMutation;
415
+ /**
416
+ * Insert a new record into the collection.
417
+ * Generates a UUID v7 for the id, validates data, and persists atomically.
418
+ *
419
+ * @param data - The record data (auto fields and defaults are applied automatically)
420
+ * @returns The inserted record with id, createdAt, updatedAt
421
+ */
422
+ async insert(data) {
423
+ const validated = (0, import_core3.validateRecord)(this.name, this.definition, data, "insert");
424
+ const recordId = (0, import_core3.generateUUIDv7)();
425
+ const now = Date.now();
426
+ for (const [fieldName, descriptor] of Object.entries(this.definition.fields)) {
427
+ if (descriptor.auto && descriptor.kind === "timestamp") {
428
+ validated[fieldName] = now;
429
+ }
430
+ }
431
+ const sequenceNumber = this.getSequenceNumber();
432
+ const operation = await (0, import_core3.createOperation)(
433
+ {
434
+ nodeId: this.nodeId,
435
+ type: "insert",
436
+ collection: this.name,
437
+ recordId,
438
+ data: { ...validated },
439
+ previousData: null,
440
+ sequenceNumber,
441
+ causalDeps: [],
442
+ schemaVersion: this.schema.version
443
+ },
444
+ this.clock
445
+ );
446
+ const serializedData = serializeRecord(validated, this.definition.fields);
447
+ const record = {
448
+ id: recordId,
449
+ ...serializedData,
450
+ _created_at: now,
451
+ _updated_at: now
452
+ };
453
+ const insertQuery = buildInsertQuery(this.name, record);
454
+ const opRow = serializeOperation(operation);
455
+ const opInsert = buildInsertQuery(
456
+ `_kora_ops_${this.name}`,
457
+ opRow
458
+ );
459
+ await this.adapter.transaction(async (tx) => {
460
+ await tx.execute(insertQuery.sql, insertQuery.params);
461
+ await tx.execute(opInsert.sql, opInsert.params);
462
+ await tx.execute(
463
+ "INSERT OR REPLACE INTO _kora_version_vector (node_id, sequence_number) VALUES (?, ?)",
464
+ [this.nodeId, sequenceNumber]
465
+ );
466
+ });
467
+ this.onMutation(this.name, operation);
468
+ return {
469
+ id: recordId,
470
+ ...validated,
471
+ createdAt: now,
472
+ updatedAt: now
473
+ };
474
+ }
475
+ /**
476
+ * Find a record by its ID. Returns null if not found or soft-deleted.
477
+ */
478
+ async findById(id) {
479
+ const rows = await this.adapter.query(
480
+ `SELECT * FROM ${this.name} WHERE id = ? AND _deleted = 0`,
481
+ [id]
482
+ );
483
+ const row = rows[0];
484
+ if (!row) return null;
485
+ return deserializeRecord(row, this.definition.fields);
486
+ }
487
+ /**
488
+ * Update an existing record. Only the provided fields are changed.
489
+ *
490
+ * @param id - The record ID to update
491
+ * @param data - Partial data with only the fields to change
492
+ * @returns The updated record
493
+ * @throws {RecordNotFoundError} If the record doesn't exist or is deleted
494
+ */
495
+ async update(id, data) {
496
+ const currentRows = await this.adapter.query(
497
+ `SELECT * FROM ${this.name} WHERE id = ? AND _deleted = 0`,
498
+ [id]
499
+ );
500
+ const currentRow = currentRows[0];
501
+ if (!currentRow) {
502
+ throw new RecordNotFoundError(this.name, id);
503
+ }
504
+ const validated = (0, import_core3.validateRecord)(this.name, this.definition, data, "update");
505
+ const now = Date.now();
506
+ const previousData = {};
507
+ const currentRecord = deserializeRecord(currentRow, this.definition.fields);
508
+ for (const key of Object.keys(validated)) {
509
+ previousData[key] = currentRecord[key];
510
+ }
511
+ const sequenceNumber = this.getSequenceNumber();
512
+ const operation = await (0, import_core3.createOperation)(
513
+ {
514
+ nodeId: this.nodeId,
515
+ type: "update",
516
+ collection: this.name,
517
+ recordId: id,
518
+ data: { ...validated },
519
+ previousData,
520
+ sequenceNumber,
521
+ causalDeps: [],
522
+ schemaVersion: this.schema.version
523
+ },
524
+ this.clock
525
+ );
526
+ const serializedChanges = serializeRecord(validated, this.definition.fields);
527
+ const updateQuery = buildUpdateQuery(this.name, id, {
528
+ ...serializedChanges,
529
+ _updated_at: now
530
+ });
531
+ const opRow = serializeOperation(operation);
532
+ const opInsert = buildInsertQuery(
533
+ `_kora_ops_${this.name}`,
534
+ opRow
535
+ );
536
+ await this.adapter.transaction(async (tx) => {
537
+ await tx.execute(updateQuery.sql, updateQuery.params);
538
+ await tx.execute(opInsert.sql, opInsert.params);
539
+ await tx.execute(
540
+ "INSERT OR REPLACE INTO _kora_version_vector (node_id, sequence_number) VALUES (?, ?)",
541
+ [this.nodeId, sequenceNumber]
542
+ );
543
+ });
544
+ this.onMutation(this.name, operation);
545
+ const updatedRow = await this.findById(id);
546
+ if (!updatedRow) {
547
+ throw new RecordNotFoundError(this.name, id);
548
+ }
549
+ return updatedRow;
550
+ }
551
+ /**
552
+ * Soft-delete a record by its ID.
553
+ *
554
+ * @param id - The record ID to delete
555
+ * @throws {RecordNotFoundError} If the record doesn't exist or is already deleted
556
+ */
557
+ async delete(id) {
558
+ const currentRows = await this.adapter.query(
559
+ `SELECT * FROM ${this.name} WHERE id = ? AND _deleted = 0`,
560
+ [id]
561
+ );
562
+ if (!currentRows[0]) {
563
+ throw new RecordNotFoundError(this.name, id);
564
+ }
565
+ const now = Date.now();
566
+ const sequenceNumber = this.getSequenceNumber();
567
+ const operation = await (0, import_core3.createOperation)(
568
+ {
569
+ nodeId: this.nodeId,
570
+ type: "delete",
571
+ collection: this.name,
572
+ recordId: id,
573
+ data: null,
574
+ previousData: null,
575
+ sequenceNumber,
576
+ causalDeps: [],
577
+ schemaVersion: this.schema.version
578
+ },
579
+ this.clock
580
+ );
581
+ const deleteQuery = buildSoftDeleteQuery(this.name, id, now);
582
+ const opRow = serializeOperation(operation);
583
+ const opInsert = buildInsertQuery(
584
+ `_kora_ops_${this.name}`,
585
+ opRow
586
+ );
587
+ await this.adapter.transaction(async (tx) => {
588
+ await tx.execute(deleteQuery.sql, deleteQuery.params);
589
+ await tx.execute(opInsert.sql, opInsert.params);
590
+ await tx.execute(
591
+ "INSERT OR REPLACE INTO _kora_version_vector (node_id, sequence_number) VALUES (?, ?)",
592
+ [this.nodeId, sequenceNumber]
593
+ );
594
+ });
595
+ this.onMutation(this.name, operation);
596
+ }
597
+ /** Get the collection name */
598
+ getName() {
599
+ return this.name;
600
+ }
601
+ /** Get the collection definition */
602
+ getDefinition() {
603
+ return this.definition;
604
+ }
605
+ };
606
+
607
+ // src/query/pluralize.ts
608
+ function isVowel(char) {
609
+ if (!char) return false;
610
+ return "aeiouAEIOU".includes(char);
611
+ }
612
+ function pluralize(word) {
613
+ if (word.endsWith("s")) return word;
614
+ if (word.endsWith("y") && !isVowel(word[word.length - 2])) {
615
+ return `${word.slice(0, -1)}ies`;
616
+ }
617
+ if (word.endsWith("sh") || word.endsWith("ch") || word.endsWith("x") || word.endsWith("z")) {
618
+ return `${word}es`;
619
+ }
620
+ return `${word}s`;
621
+ }
622
+ function singularize(word) {
623
+ if (word.endsWith("ies") && !isVowel(word[word.length - 4])) {
624
+ return `${word.slice(0, -3)}y`;
625
+ }
626
+ if (word.endsWith("shes") || word.endsWith("ches") || word.endsWith("xes") || word.endsWith("zes")) {
627
+ return word.slice(0, -2);
628
+ }
629
+ if (word.endsWith("ses")) {
630
+ return word.slice(0, -2);
631
+ }
632
+ if (word.endsWith("s") && !word.endsWith("ss")) {
633
+ return word.slice(0, -1);
634
+ }
635
+ return word;
636
+ }
637
+
638
+ // src/query/query-builder.ts
639
+ var QueryBuilder = class _QueryBuilder {
640
+ constructor(collectionName, definition, adapter, subscriptionManager, initialWhere = {}, schema) {
641
+ this.collectionName = collectionName;
642
+ this.definition = definition;
643
+ this.adapter = adapter;
644
+ this.subscriptionManager = subscriptionManager;
645
+ this.schema = schema;
646
+ this.descriptor = {
647
+ collection: collectionName,
648
+ where: { ...initialWhere },
649
+ orderBy: []
650
+ };
651
+ }
652
+ collectionName;
653
+ definition;
654
+ adapter;
655
+ subscriptionManager;
656
+ schema;
657
+ descriptor;
658
+ /**
659
+ * Add WHERE conditions (AND semantics, merged with existing conditions).
660
+ */
661
+ where(conditions) {
662
+ const clone = this.clone();
663
+ clone.descriptor = {
664
+ ...clone.descriptor,
665
+ where: { ...clone.descriptor.where, ...conditions }
666
+ };
667
+ return clone;
668
+ }
669
+ /**
670
+ * Add ORDER BY clause.
671
+ */
672
+ orderBy(field, direction = "asc") {
673
+ const clone = this.clone();
674
+ clone.descriptor = {
675
+ ...clone.descriptor,
676
+ orderBy: [...clone.descriptor.orderBy, { field, direction }]
677
+ };
678
+ return clone;
679
+ }
680
+ /**
681
+ * Set result limit.
682
+ */
683
+ limit(n) {
684
+ const clone = this.clone();
685
+ clone.descriptor = { ...clone.descriptor, limit: n };
686
+ return clone;
687
+ }
688
+ /**
689
+ * Set result offset.
690
+ */
691
+ offset(n) {
692
+ const clone = this.clone();
693
+ clone.descriptor = { ...clone.descriptor, offset: n };
694
+ return clone;
695
+ }
696
+ /**
697
+ * Include related records in the query results.
698
+ * Follows relations defined in the schema to batch-fetch related data.
699
+ *
700
+ * @param targets - Relation target names (collection names or relation names)
701
+ * @returns A new QueryBuilder with include targets added
702
+ *
703
+ * @example
704
+ * ```typescript
705
+ * const todosWithProject = await app.todos
706
+ * .where({ completed: false })
707
+ * .include('project')
708
+ * .exec()
709
+ * ```
710
+ */
711
+ include(...targets) {
712
+ const clone = this.clone();
713
+ const existing = clone.descriptor.include ?? [];
714
+ clone.descriptor = {
715
+ ...clone.descriptor,
716
+ include: [...existing, ...targets]
717
+ };
718
+ return clone;
719
+ }
720
+ /**
721
+ * Execute the query and return results.
722
+ */
723
+ async exec() {
724
+ const { sql, params } = buildSelectQuery(this.descriptor, this.definition.fields);
725
+ const rows = await this.adapter.query(sql, params);
726
+ const records = rows.map((row) => deserializeRecord(row, this.definition.fields));
727
+ if (this.descriptor.include && this.descriptor.include.length > 0 && this.schema) {
728
+ await this.resolveIncludes(records);
729
+ }
730
+ return records;
731
+ }
732
+ /**
733
+ * Execute a COUNT query and return the count.
734
+ */
735
+ async count() {
736
+ const { sql, params } = buildCountQuery(this.descriptor, this.definition.fields);
737
+ const rows = await this.adapter.query(sql, params);
738
+ return rows[0]?.count ?? 0;
739
+ }
740
+ /**
741
+ * Subscribe to query results. Callback is called immediately with current results,
742
+ * then again whenever the results change due to mutations.
743
+ *
744
+ * @returns An unsubscribe function
745
+ */
746
+ subscribe(callback) {
747
+ const executeFn = () => this.exec();
748
+ const descriptorCopy = { ...this.descriptor };
749
+ if (descriptorCopy.include && descriptorCopy.include.length > 0 && this.schema) {
750
+ descriptorCopy.includeCollections = this.resolveIncludeCollections(descriptorCopy.include);
751
+ }
752
+ return this.subscriptionManager.registerAndFetch(
753
+ descriptorCopy,
754
+ callback,
755
+ executeFn
756
+ );
757
+ }
758
+ /** Get the internal descriptor (for testing/debugging) */
759
+ getDescriptor() {
760
+ return { ...this.descriptor };
761
+ }
762
+ clone() {
763
+ const qb = new _QueryBuilder(
764
+ this.collectionName,
765
+ this.definition,
766
+ this.adapter,
767
+ this.subscriptionManager,
768
+ {},
769
+ this.schema
770
+ );
771
+ qb.descriptor = {
772
+ ...this.descriptor,
773
+ where: { ...this.descriptor.where },
774
+ orderBy: [...this.descriptor.orderBy],
775
+ include: this.descriptor.include ? [...this.descriptor.include] : void 0,
776
+ includeCollections: this.descriptor.includeCollections ? [...this.descriptor.includeCollections] : void 0
777
+ };
778
+ return qb;
779
+ }
780
+ /**
781
+ * Resolve include targets to their actual collection names for subscription tracking.
782
+ */
783
+ resolveIncludeCollections(targets) {
784
+ if (!this.schema) return [];
785
+ const collections = [];
786
+ for (const target of targets) {
787
+ const relation = this.findRelation(target);
788
+ if (relation) {
789
+ if (relation.from === this.collectionName) {
790
+ collections.push(relation.to);
791
+ } else {
792
+ collections.push(relation.from);
793
+ }
794
+ }
795
+ }
796
+ return collections;
797
+ }
798
+ /**
799
+ * Resolve includes after primary query, batch-fetching related records.
800
+ */
801
+ async resolveIncludes(records) {
802
+ if (!this.schema || !this.descriptor.include || records.length === 0) return;
803
+ for (const target of this.descriptor.include) {
804
+ const relation = this.findRelation(target);
805
+ if (!relation) {
806
+ throw new QueryError2(
807
+ `No relation found for include target "${target}" on collection "${this.collectionName}". Check that a relation is defined in your schema that connects "${this.collectionName}" to "${target}".`
808
+ );
809
+ }
810
+ if (relation.from === this.collectionName) {
811
+ await this.resolveManyToOneInclude(records, relation, target);
812
+ } else {
813
+ await this.resolveOneToManyInclude(records, relation, target);
814
+ }
815
+ }
816
+ }
817
+ /**
818
+ * Many-to-one: collect FK values from primary results, batch-fetch related, attach as singular.
819
+ */
820
+ async resolveManyToOneInclude(records, relation, target) {
821
+ const fkField = relation.field;
822
+ const fkValues = records.map((r) => r[fkField]).filter((v) => v !== null && v !== void 0 && typeof v === "string");
823
+ if (fkValues.length === 0) {
824
+ const propName2 = singularize(target);
825
+ for (const record of records) {
826
+ ;
827
+ record[propName2] = null;
828
+ }
829
+ return;
830
+ }
831
+ const uniqueFks = [...new Set(fkValues)];
832
+ const relatedCollection = relation.to;
833
+ const relatedDef = this.schema?.collections[relatedCollection];
834
+ if (!relatedDef) return;
835
+ const placeholders = uniqueFks.map(() => "?").join(", ");
836
+ const sql = `SELECT * FROM ${relatedCollection} WHERE id IN (${placeholders}) AND _deleted = 0`;
837
+ const rows = await this.adapter.query(sql, uniqueFks);
838
+ const relatedRecords = rows.map((row) => deserializeRecord(row, relatedDef.fields));
839
+ const lookup = /* @__PURE__ */ new Map();
840
+ for (const r of relatedRecords) {
841
+ lookup.set(r.id, r);
842
+ }
843
+ const propName = singularize(target);
844
+ for (const record of records) {
845
+ const fk = record[fkField];
846
+ record[propName] = fk ? lookup.get(fk) ?? null : null;
847
+ }
848
+ }
849
+ /**
850
+ * One-to-many: collect primary IDs, batch-fetch children, attach as array.
851
+ */
852
+ async resolveOneToManyInclude(records, relation, target) {
853
+ const primaryIds = records.map((r) => r.id);
854
+ const relatedCollection = relation.from;
855
+ const relatedDef = this.schema?.collections[relatedCollection];
856
+ if (!relatedDef) return;
857
+ const fkField = relation.field;
858
+ const placeholders = primaryIds.map(() => "?").join(", ");
859
+ const sql = `SELECT * FROM ${relatedCollection} WHERE ${fkField} IN (${placeholders}) AND _deleted = 0`;
860
+ const rows = await this.adapter.query(sql, primaryIds);
861
+ const relatedRecords = rows.map((row) => deserializeRecord(row, relatedDef.fields));
862
+ const grouped = /* @__PURE__ */ new Map();
863
+ for (const r of relatedRecords) {
864
+ const fk = r[fkField];
865
+ if (!grouped.has(fk)) {
866
+ grouped.set(fk, []);
867
+ }
868
+ grouped.get(fk)?.push(r);
869
+ }
870
+ const propName = pluralize(target);
871
+ for (const record of records) {
872
+ ;
873
+ record[propName] = grouped.get(record.id) ?? [];
874
+ }
875
+ }
876
+ /**
877
+ * Find a relation definition matching the include target.
878
+ * Searches by relation name, target collection name, and singularized/pluralized variants.
879
+ */
880
+ findRelation(target) {
881
+ if (!this.schema) return null;
882
+ for (const [_name, rel] of Object.entries(this.schema.relations)) {
883
+ if (rel.from === this.collectionName && rel.to === target) return rel;
884
+ if (rel.to === this.collectionName && rel.from === target) return rel;
885
+ if (rel.from === this.collectionName && rel.to === pluralize(target)) return rel;
886
+ if (rel.to === this.collectionName && rel.from === pluralize(target)) return rel;
887
+ if (rel.from === this.collectionName && rel.to === singularize(target)) return rel;
888
+ if (rel.to === this.collectionName && rel.from === singularize(target)) return rel;
889
+ }
890
+ return null;
891
+ }
892
+ };
893
+ var QueryError2 = class extends Error {
894
+ constructor(message) {
895
+ super(message);
896
+ this.name = "QueryError";
897
+ }
898
+ };
899
+
900
+ // src/subscription/subscription-manager.ts
901
+ var nextSubId = 0;
902
+ var SubscriptionManager = class {
903
+ subscriptions = /* @__PURE__ */ new Map();
904
+ pendingCollections = /* @__PURE__ */ new Set();
905
+ flushScheduled = false;
906
+ /**
907
+ * Register a new subscription.
908
+ *
909
+ * @param descriptor - The query descriptor defining what this subscription watches
910
+ * @param callback - Called with results whenever they change
911
+ * @param executeFn - Function to re-execute the query and get current results
912
+ * @returns An unsubscribe function
913
+ */
914
+ register(descriptor, callback, executeFn) {
915
+ const id = `sub_${++nextSubId}`;
916
+ const subscription = {
917
+ id,
918
+ descriptor,
919
+ callback,
920
+ executeFn,
921
+ lastResults: []
922
+ };
923
+ this.subscriptions.set(id, subscription);
924
+ return () => {
925
+ this.subscriptions.delete(id);
926
+ };
927
+ }
928
+ /**
929
+ * Register a subscription and immediately execute the query.
930
+ * The initial results are stored as lastResults so subsequent flushes
931
+ * correctly diff against the initial state.
932
+ *
933
+ * @returns An unsubscribe function
934
+ */
935
+ registerAndFetch(descriptor, callback, executeFn) {
936
+ const id = `sub_${++nextSubId}`;
937
+ const subscription = {
938
+ id,
939
+ descriptor,
940
+ callback,
941
+ executeFn,
942
+ lastResults: []
943
+ };
944
+ this.subscriptions.set(id, subscription);
945
+ executeFn().then((results) => {
946
+ if (this.subscriptions.has(id)) {
947
+ subscription.lastResults = results;
948
+ callback(results);
949
+ }
950
+ });
951
+ return () => {
952
+ this.subscriptions.delete(id);
953
+ };
954
+ }
955
+ /**
956
+ * Notify the manager that a mutation occurred on a collection.
957
+ * Schedules a microtask flush to batch multiple mutations in the same tick.
958
+ */
959
+ notify(collection, _operation) {
960
+ this.pendingCollections.add(collection);
961
+ this.scheduleFlush();
962
+ }
963
+ /**
964
+ * Immediately flush all pending notifications.
965
+ * Useful for testing. In production, flushing happens via microtask.
966
+ */
967
+ async flush() {
968
+ if (this.pendingCollections.size === 0) return;
969
+ const collections = new Set(this.pendingCollections);
970
+ this.pendingCollections.clear();
971
+ this.flushScheduled = false;
972
+ const affected = [];
973
+ for (const sub of this.subscriptions.values()) {
974
+ if (collections.has(sub.descriptor.collection)) {
975
+ affected.push(sub);
976
+ } else if (sub.descriptor.includeCollections) {
977
+ for (const incCol of sub.descriptor.includeCollections) {
978
+ if (collections.has(incCol)) {
979
+ affected.push(sub);
980
+ break;
981
+ }
982
+ }
983
+ }
984
+ }
985
+ for (const sub of affected) {
986
+ try {
987
+ const newResults = await sub.executeFn();
988
+ if (!this.resultsEqual(sub.lastResults, newResults)) {
989
+ sub.lastResults = newResults;
990
+ sub.callback(newResults);
991
+ }
992
+ } catch {
993
+ }
994
+ }
995
+ }
996
+ /**
997
+ * Remove all subscriptions. Called on store close.
998
+ */
999
+ clear() {
1000
+ this.subscriptions.clear();
1001
+ this.pendingCollections.clear();
1002
+ this.flushScheduled = false;
1003
+ }
1004
+ /** Number of active subscriptions (for testing/debugging) */
1005
+ get size() {
1006
+ return this.subscriptions.size;
1007
+ }
1008
+ scheduleFlush() {
1009
+ if (this.flushScheduled) return;
1010
+ this.flushScheduled = true;
1011
+ queueMicrotask(() => {
1012
+ this.flush();
1013
+ });
1014
+ }
1015
+ /**
1016
+ * Compare two result sets. Uses length check + JSON comparison as pragmatic approach.
1017
+ * Sufficient for typical query results. Can be optimized to id-based diffing if profiling shows need.
1018
+ */
1019
+ resultsEqual(prev, next) {
1020
+ if (prev.length !== next.length) return false;
1021
+ if (prev.length === 0) return true;
1022
+ return JSON.stringify(prev) === JSON.stringify(next);
1023
+ }
1024
+ };
1025
+
1026
+ // src/store/store.ts
1027
+ var Store = class {
1028
+ opened = false;
1029
+ nodeId = "";
1030
+ sequenceNumber = 0;
1031
+ versionVector = (0, import_core4.createVersionVector)();
1032
+ clock = null;
1033
+ collections = /* @__PURE__ */ new Map();
1034
+ subscriptionManager = new SubscriptionManager();
1035
+ schema;
1036
+ adapter;
1037
+ configNodeId;
1038
+ emitter;
1039
+ constructor(config) {
1040
+ this.schema = config.schema;
1041
+ this.adapter = config.adapter;
1042
+ this.configNodeId = config.nodeId;
1043
+ this.emitter = config.emitter ?? null;
1044
+ }
1045
+ /**
1046
+ * Open the store: initialize the database, load or generate a node ID,
1047
+ * restore the sequence number and version vector, and create Collection instances.
1048
+ */
1049
+ async open() {
1050
+ await this.adapter.open(this.schema);
1051
+ this.nodeId = await this.loadOrGenerateNodeId();
1052
+ this.clock = new import_core4.HybridLogicalClock(this.nodeId);
1053
+ this.sequenceNumber = await this.loadSequenceNumber();
1054
+ this.versionVector = await this.loadVersionVector();
1055
+ for (const [name, definition] of Object.entries(this.schema.collections)) {
1056
+ const col = new Collection(
1057
+ name,
1058
+ definition,
1059
+ this.schema,
1060
+ this.adapter,
1061
+ this.clock,
1062
+ this.nodeId,
1063
+ () => this.nextSequenceNumber(),
1064
+ (collectionName, operation) => {
1065
+ this.subscriptionManager.notify(collectionName, operation);
1066
+ if (this.emitter) {
1067
+ this.emitter.emit({ type: "operation:created", operation });
1068
+ }
1069
+ }
1070
+ );
1071
+ this.collections.set(name, col);
1072
+ }
1073
+ this.opened = true;
1074
+ }
1075
+ /**
1076
+ * Close the store: clear subscriptions and close the adapter.
1077
+ */
1078
+ async close() {
1079
+ this.subscriptionManager.clear();
1080
+ this.collections.clear();
1081
+ this.opened = false;
1082
+ await this.adapter.close();
1083
+ }
1084
+ /**
1085
+ * Get a Collection instance for CRUD operations.
1086
+ * @throws {StoreNotOpenError} If the store is not open
1087
+ * @throws {Error} If the collection name is not in the schema
1088
+ */
1089
+ collection(name) {
1090
+ this.ensureOpen();
1091
+ const col = this.collections.get(name);
1092
+ if (!col) {
1093
+ throw new Error(
1094
+ `Unknown collection "${name}". Available: ${[...this.collections.keys()].join(", ")}`
1095
+ );
1096
+ }
1097
+ const definition = this.schema.collections[name];
1098
+ if (!definition) {
1099
+ throw new Error(`Collection definition not found for "${name}"`);
1100
+ }
1101
+ return {
1102
+ insert: (data) => col.insert(data),
1103
+ findById: (id) => col.findById(id),
1104
+ update: (id, data) => col.update(id, data),
1105
+ delete: (id) => col.delete(id),
1106
+ where: (conditions) => new QueryBuilder(name, definition, this.adapter, this.subscriptionManager, conditions, this.schema)
1107
+ };
1108
+ }
1109
+ /**
1110
+ * Get the current version vector.
1111
+ */
1112
+ getVersionVector() {
1113
+ this.ensureOpen();
1114
+ return new Map(this.versionVector);
1115
+ }
1116
+ /**
1117
+ * Get the node ID for this store instance.
1118
+ */
1119
+ getNodeId() {
1120
+ this.ensureOpen();
1121
+ return this.nodeId;
1122
+ }
1123
+ /**
1124
+ * Apply a remote operation received from sync.
1125
+ * Checks for duplicates, applies to the data table, persists the operation,
1126
+ * and updates the version vector.
1127
+ */
1128
+ async applyRemoteOperation(op) {
1129
+ this.ensureOpen();
1130
+ const collection = op.collection;
1131
+ const definition = this.schema.collections[collection];
1132
+ if (!definition) {
1133
+ return "skipped";
1134
+ }
1135
+ const existing = await this.adapter.query(
1136
+ `SELECT id FROM _kora_ops_${collection} WHERE id = ?`,
1137
+ [op.id]
1138
+ );
1139
+ if (existing.length > 0) {
1140
+ return "duplicate";
1141
+ }
1142
+ if (this.clock) {
1143
+ this.clock.receive(op.timestamp);
1144
+ }
1145
+ await this.adapter.transaction(async (tx) => {
1146
+ if (op.type === "insert" && op.data) {
1147
+ const serializedData = serializeRecord(op.data, definition.fields);
1148
+ const now = op.timestamp.wallTime;
1149
+ const record = {
1150
+ id: op.recordId,
1151
+ ...serializedData,
1152
+ _created_at: now,
1153
+ _updated_at: now
1154
+ };
1155
+ const insertQuery = buildInsertQuery(collection, record);
1156
+ await tx.execute(insertQuery.sql, insertQuery.params);
1157
+ } else if (op.type === "update" && op.data) {
1158
+ const serializedChanges = serializeRecord(op.data, definition.fields);
1159
+ const updateQuery = buildUpdateQuery(collection, op.recordId, {
1160
+ ...serializedChanges,
1161
+ _updated_at: op.timestamp.wallTime
1162
+ });
1163
+ await tx.execute(updateQuery.sql, updateQuery.params);
1164
+ } else if (op.type === "delete") {
1165
+ const deleteQuery = buildSoftDeleteQuery(collection, op.recordId, op.timestamp.wallTime);
1166
+ await tx.execute(deleteQuery.sql, deleteQuery.params);
1167
+ }
1168
+ const opRow = serializeOperation(op);
1169
+ const opInsert = buildInsertQuery(
1170
+ `_kora_ops_${collection}`,
1171
+ opRow
1172
+ );
1173
+ await tx.execute(opInsert.sql, opInsert.params);
1174
+ const currentSeq = this.versionVector.get(op.nodeId) ?? 0;
1175
+ if (op.sequenceNumber > currentSeq) {
1176
+ this.versionVector.set(op.nodeId, op.sequenceNumber);
1177
+ await tx.execute(
1178
+ "INSERT OR REPLACE INTO _kora_version_vector (node_id, sequence_number) VALUES (?, ?)",
1179
+ [op.nodeId, op.sequenceNumber]
1180
+ );
1181
+ }
1182
+ });
1183
+ this.subscriptionManager.notify(collection, op);
1184
+ return "applied";
1185
+ }
1186
+ /**
1187
+ * Get operations from a node within a sequence number range.
1188
+ * Implements the OperationLog interface for computeDelta.
1189
+ */
1190
+ getRange(nodeId, fromSeq, toSeq) {
1191
+ return [];
1192
+ }
1193
+ /**
1194
+ * Async version of getRange for use by the sync layer.
1195
+ */
1196
+ async getOperationRange(nodeId, fromSeq, toSeq) {
1197
+ this.ensureOpen();
1198
+ const allOps = [];
1199
+ for (const collectionName of Object.keys(this.schema.collections)) {
1200
+ const rows = await this.adapter.query(
1201
+ `SELECT * FROM _kora_ops_${collectionName} WHERE node_id = ? AND sequence_number >= ? AND sequence_number <= ? ORDER BY sequence_number ASC`,
1202
+ [nodeId, fromSeq, toSeq]
1203
+ );
1204
+ for (const row of rows) {
1205
+ allOps.push(deserializeOperationWithCollection(row, collectionName));
1206
+ }
1207
+ }
1208
+ allOps.sort((a, b) => a.sequenceNumber - b.sequenceNumber);
1209
+ return allOps;
1210
+ }
1211
+ /**
1212
+ * Get the schema definition.
1213
+ */
1214
+ getSchema() {
1215
+ return this.schema;
1216
+ }
1217
+ /** Expose the subscription manager for direct access (e.g., by QueryBuilder) */
1218
+ getSubscriptionManager() {
1219
+ return this.subscriptionManager;
1220
+ }
1221
+ nextSequenceNumber() {
1222
+ this.sequenceNumber++;
1223
+ this.versionVector.set(this.nodeId, this.sequenceNumber);
1224
+ return this.sequenceNumber;
1225
+ }
1226
+ async loadOrGenerateNodeId() {
1227
+ if (this.configNodeId) {
1228
+ await this.adapter.execute(
1229
+ "INSERT OR REPLACE INTO _kora_meta (key, value) VALUES ('node_id', ?)",
1230
+ [this.configNodeId]
1231
+ );
1232
+ return this.configNodeId;
1233
+ }
1234
+ const rows = await this.adapter.query(
1235
+ "SELECT value FROM _kora_meta WHERE key = 'node_id'"
1236
+ );
1237
+ if (rows[0]) {
1238
+ return rows[0].value;
1239
+ }
1240
+ const newNodeId = (0, import_core4.generateUUIDv7)();
1241
+ await this.adapter.execute("INSERT INTO _kora_meta (key, value) VALUES ('node_id', ?)", [
1242
+ newNodeId
1243
+ ]);
1244
+ return newNodeId;
1245
+ }
1246
+ async loadSequenceNumber() {
1247
+ const rows = await this.adapter.query(
1248
+ "SELECT sequence_number FROM _kora_version_vector WHERE node_id = ?",
1249
+ [this.nodeId]
1250
+ );
1251
+ return rows[0]?.sequence_number ?? 0;
1252
+ }
1253
+ async loadVersionVector() {
1254
+ const rows = await this.adapter.query(
1255
+ "SELECT node_id, sequence_number FROM _kora_version_vector"
1256
+ );
1257
+ const vector = (0, import_core4.createVersionVector)();
1258
+ for (const row of rows) {
1259
+ vector.set(row.node_id, row.sequence_number);
1260
+ }
1261
+ return vector;
1262
+ }
1263
+ ensureOpen() {
1264
+ if (!this.opened) {
1265
+ throw new StoreNotOpenError();
1266
+ }
1267
+ }
1268
+ };
1269
+ // Annotate the CommonJS export names for ESM import in node:
1270
+ 0 && (module.exports = {
1271
+ AdapterError,
1272
+ Collection,
1273
+ PersistenceError,
1274
+ QueryBuilder,
1275
+ QueryError,
1276
+ RecordNotFoundError,
1277
+ Store,
1278
+ StoreNotOpenError,
1279
+ SubscriptionManager,
1280
+ WorkerInitError,
1281
+ WorkerTimeoutError,
1282
+ decodeRichtext,
1283
+ encodeRichtext,
1284
+ pluralize,
1285
+ richtextToPlainText,
1286
+ singularize
1287
+ });
1288
+ //# sourceMappingURL=index.cjs.map