@korajs/store 1.0.0-beta.11 → 1.0.0-beta.13

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 (92) hide show
  1. package/README.md +29 -37
  2. package/dist/adapters/better-sqlite3.cjs +90 -4
  3. package/dist/adapters/better-sqlite3.cjs.map +1 -1
  4. package/dist/adapters/better-sqlite3.d.cts +21 -12
  5. package/dist/adapters/better-sqlite3.d.ts +21 -12
  6. package/dist/adapters/better-sqlite3.js +69 -5
  7. package/dist/adapters/better-sqlite3.js.map +1 -1
  8. package/dist/adapters/indexeddb.cjs +1316 -610
  9. package/dist/adapters/indexeddb.cjs.map +1 -1
  10. package/dist/adapters/indexeddb.d.cts +29 -3
  11. package/dist/adapters/indexeddb.d.ts +29 -3
  12. package/dist/adapters/indexeddb.js +247 -221
  13. package/dist/adapters/indexeddb.js.map +1 -1
  14. package/dist/adapters/sqlite-wasm-worker.cjs +803 -62
  15. package/dist/adapters/sqlite-wasm-worker.cjs.map +1 -1
  16. package/dist/adapters/sqlite-wasm-worker.js +480 -62
  17. package/dist/adapters/sqlite-wasm-worker.js.map +1 -1
  18. package/dist/adapters/sqlite-wasm.cjs +1032 -344
  19. package/dist/adapters/sqlite-wasm.cjs.map +1 -1
  20. package/dist/adapters/sqlite-wasm.d.cts +239 -18
  21. package/dist/adapters/sqlite-wasm.d.ts +239 -18
  22. package/dist/adapters/sqlite-wasm.js +111 -5
  23. package/dist/adapters/sqlite-wasm.js.map +1 -1
  24. package/dist/backup-4NUPDEMB.js +25 -0
  25. package/dist/chunk-32EDKWMI.js +876 -0
  26. package/dist/chunk-32EDKWMI.js.map +1 -0
  27. package/dist/{chunk-CYO3YKFL.js → chunk-5ZRRF6R2.js} +2 -2
  28. package/dist/chunk-A6GFH5IG.js +36 -0
  29. package/dist/chunk-A6GFH5IG.js.map +1 -0
  30. package/dist/chunk-DUKTWVZW.js +175 -0
  31. package/dist/chunk-DUKTWVZW.js.map +1 -0
  32. package/dist/chunk-HZTDBLDB.js +607 -0
  33. package/dist/chunk-HZTDBLDB.js.map +1 -0
  34. package/dist/{chunk-GSJ7EEUC.js → chunk-J5AGXF3D.js} +58 -9
  35. package/dist/chunk-J5AGXF3D.js.map +1 -0
  36. package/dist/chunk-MCQKEHOD.js +112 -0
  37. package/dist/chunk-MCQKEHOD.js.map +1 -0
  38. package/dist/chunk-P4V375FY.js +810 -0
  39. package/dist/chunk-P4V375FY.js.map +1 -0
  40. package/dist/chunk-QDUSJMFV.js +1095 -0
  41. package/dist/chunk-QDUSJMFV.js.map +1 -0
  42. package/dist/chunk-SJP2HTYZ.js +1003 -0
  43. package/dist/chunk-SJP2HTYZ.js.map +1 -0
  44. package/dist/chunk-W5F2LWG6.js +591 -0
  45. package/dist/chunk-W5F2LWG6.js.map +1 -0
  46. package/dist/chunk-Y5LRH7KI.js +5341 -0
  47. package/dist/chunk-Y5LRH7KI.js.map +1 -0
  48. package/dist/{export-audit-ODGQTQIK.js → export-audit-J2QXRHD2.js} +3 -3
  49. package/dist/index.cjs +9354 -4157
  50. package/dist/index.cjs.map +1 -1
  51. package/dist/index.d.cts +1064 -108
  52. package/dist/index.d.ts +1064 -108
  53. package/dist/index.js +249 -3092
  54. package/dist/index.js.map +1 -1
  55. package/dist/internal.cjs +580 -264
  56. package/dist/internal.cjs.map +1 -1
  57. package/dist/internal.d.cts +15 -85
  58. package/dist/internal.d.ts +15 -85
  59. package/dist/internal.js +10 -8
  60. package/dist/restore-FR7XIKT2.js +299 -0
  61. package/dist/restore-FR7XIKT2.js.map +1 -0
  62. package/dist/sql-builder-C4u55jKW.d.cts +608 -0
  63. package/dist/sql-builder-CGckdBDL.d.ts +608 -0
  64. package/dist/sqlite-wasm-channel-DVX93Uiq.d.cts +141 -0
  65. package/dist/sqlite-wasm-channel-DVX93Uiq.d.ts +141 -0
  66. package/dist/sqlite-wasm-channel-MLHLKEED.js +12 -0
  67. package/dist/store-IMWKZLWB.js +16 -0
  68. package/dist/store-IMWKZLWB.js.map +1 -0
  69. package/dist/{types-BslDOTk0.d.ts → types-Dh-xyUjP.d.cts} +99 -26
  70. package/dist/{types-BslDOTk0.d.cts → types-Dh-xyUjP.d.ts} +99 -26
  71. package/package.json +3 -2
  72. package/dist/backup-5S7K2Z35.js +0 -14
  73. package/dist/chunk-GSJ7EEUC.js.map +0 -1
  74. package/dist/chunk-HS6DBMST.js +0 -383
  75. package/dist/chunk-HS6DBMST.js.map +0 -1
  76. package/dist/chunk-RJIEKEN4.js +0 -751
  77. package/dist/chunk-RJIEKEN4.js.map +0 -1
  78. package/dist/chunk-TUR7RGEP.js +0 -84
  79. package/dist/chunk-TUR7RGEP.js.map +0 -1
  80. package/dist/chunk-VDBJQ75K.js +0 -78
  81. package/dist/chunk-VDBJQ75K.js.map +0 -1
  82. package/dist/chunk-YT7MRFCP.js +0 -687
  83. package/dist/chunk-YT7MRFCP.js.map +0 -1
  84. package/dist/sqlite-wasm-channel-GGHA2BXU.js +0 -12
  85. package/dist/sqlite-wasm-channel-L6o3bWQK.d.cts +0 -92
  86. package/dist/sqlite-wasm-channel-L6o3bWQK.d.ts +0 -92
  87. package/dist/types-BOO9EPt1.d.ts +0 -170
  88. package/dist/types-C_GDavG-.d.cts +0 -170
  89. /package/dist/{backup-5S7K2Z35.js.map → backup-4NUPDEMB.js.map} +0 -0
  90. /package/dist/{chunk-CYO3YKFL.js.map → chunk-5ZRRF6R2.js.map} +0 -0
  91. /package/dist/{export-audit-ODGQTQIK.js.map → export-audit-J2QXRHD2.js.map} +0 -0
  92. /package/dist/{sqlite-wasm-channel-GGHA2BXU.js.map → sqlite-wasm-channel-MLHLKEED.js.map} +0 -0
@@ -0,0 +1,1003 @@
1
+ import {
2
+ encodeStoredFilterValue
3
+ } from "./chunk-J5AGXF3D.js";
4
+ import {
5
+ QueryError
6
+ } from "./chunk-DUKTWVZW.js";
7
+
8
+ // src/lww/row-version.ts
9
+ import { HybridLogicalClock } from "@korajs/core";
10
+ var EMPTY_ROW_VERSION = "";
11
+ function serializeRowVersion(timestamp) {
12
+ return HybridLogicalClock.serialize(timestamp);
13
+ }
14
+ function rowVersionFromRecord(row) {
15
+ const version = row._version;
16
+ if (typeof version === "string" && version.length > 0) {
17
+ return HybridLogicalClock.deserialize(version);
18
+ }
19
+ return { wallTime: row._updated_at, logical: 0, nodeId: "" };
20
+ }
21
+ function isIncomingNewerThanRow(incoming, row) {
22
+ return HybridLogicalClock.compare(incoming, rowVersionFromRecord(row)) > 0;
23
+ }
24
+ function lwwVersionWhereClause(remoteVersion) {
25
+ return {
26
+ sql: "(_version = ? OR _version < ?)",
27
+ params: [EMPTY_ROW_VERSION, remoteVersion]
28
+ };
29
+ }
30
+
31
+ // src/query/sql-builder.ts
32
+ import { quoteIdent } from "@korajs/core";
33
+ function buildSelectQuery(descriptor, fields) {
34
+ const params = [];
35
+ const parts = [`SELECT * FROM ${quoteIdent(descriptor.collection)}`];
36
+ const whereClause = buildWhereClauseParts(descriptor.where, fields, params);
37
+ const deletedFilter = "_deleted = 0";
38
+ if (whereClause) {
39
+ parts.push(`WHERE ${deletedFilter} AND ${whereClause}`);
40
+ } else {
41
+ parts.push(`WHERE ${deletedFilter}`);
42
+ }
43
+ if (descriptor.orderBy.length > 0) {
44
+ const orderParts = descriptor.orderBy.map((o) => {
45
+ validateFieldName(o.field, fields);
46
+ return `${quoteIdent(resolveColumnName(o.field, fields))} ${sqlSortDirection(o.direction, o.field)}`;
47
+ });
48
+ parts.push(`ORDER BY ${orderParts.join(", ")}`);
49
+ }
50
+ const limit = descriptor.limit !== void 0 ? assertQueryCount(descriptor.limit, "limit") : null;
51
+ const offset = descriptor.offset !== void 0 ? assertQueryCount(descriptor.offset, "offset") : null;
52
+ if (limit !== null) {
53
+ parts.push("LIMIT ?");
54
+ params.push(limit);
55
+ }
56
+ if (offset !== null) {
57
+ if (limit === null) parts.push("LIMIT -1");
58
+ parts.push("OFFSET ?");
59
+ params.push(offset);
60
+ }
61
+ return { sql: parts.join(" "), params };
62
+ }
63
+ function buildCountQuery(descriptor, fields) {
64
+ const params = [];
65
+ const parts = [`SELECT COUNT(*) as count FROM ${quoteIdent(descriptor.collection)}`];
66
+ const whereClause = buildWhereClauseParts(descriptor.where, fields, params);
67
+ const deletedFilter = "_deleted = 0";
68
+ if (whereClause) {
69
+ parts.push(`WHERE ${deletedFilter} AND ${whereClause}`);
70
+ } else {
71
+ parts.push(`WHERE ${deletedFilter}`);
72
+ }
73
+ return { sql: parts.join(" "), params };
74
+ }
75
+ function buildInsertQuery(collection, record) {
76
+ const columns = Object.keys(record);
77
+ const placeholders = columns.map(() => "?");
78
+ const params = Object.values(record);
79
+ const quotedColumns = columns.map((col) => quoteIdent(col));
80
+ const sql = `INSERT INTO ${quoteIdent(collection)} (${quotedColumns.join(", ")}) VALUES (${placeholders.join(", ")})`;
81
+ return { sql, params };
82
+ }
83
+ function buildUpdateQuery(collection, id, changes) {
84
+ const setClauses = Object.keys(changes).map((col) => `${quoteIdent(col)} = ?`);
85
+ const params = [...Object.values(changes), id];
86
+ const sql = `UPDATE ${quoteIdent(collection)} SET ${setClauses.join(", ")} WHERE id = ?`;
87
+ return { sql, params };
88
+ }
89
+ function buildFieldFastForwardUpdateQuery(collection, id, fieldChanges, remoteVersion, wallTime, options) {
90
+ const fieldClauses = Object.keys(fieldChanges).map((col) => `${quoteIdent(col)} = ?`);
91
+ const setClauses = [
92
+ ...fieldClauses,
93
+ "_version = CASE WHEN _version >= ? THEN _version ELSE ? END",
94
+ "_updated_at = CASE WHEN _updated_at >= ? THEN _updated_at ELSE ? END"
95
+ ];
96
+ const params = [
97
+ ...Object.values(fieldChanges),
98
+ remoteVersion,
99
+ remoteVersion,
100
+ wallTime,
101
+ wallTime
102
+ ];
103
+ if (options?.maxCreatedAt !== void 0) {
104
+ setClauses.push("_created_at = CASE WHEN _created_at >= ? THEN _created_at ELSE ? END");
105
+ params.push(options.maxCreatedAt, options.maxCreatedAt);
106
+ }
107
+ params.push(id);
108
+ const sql = `UPDATE ${quoteIdent(collection)} SET ${setClauses.join(", ")} WHERE id = ?`;
109
+ return { sql, params };
110
+ }
111
+ function buildSoftDeleteQuery(collection, id, updatedAt, version) {
112
+ if (version !== void 0) {
113
+ return {
114
+ sql: `UPDATE ${quoteIdent(collection)} SET _deleted = 1, _updated_at = ?, _version = ? WHERE id = ?`,
115
+ params: [updatedAt, version, id]
116
+ };
117
+ }
118
+ return {
119
+ sql: `UPDATE ${quoteIdent(collection)} SET _deleted = 1, _updated_at = ? WHERE id = ?`,
120
+ params: [updatedAt, id]
121
+ };
122
+ }
123
+ function buildLwwUpdateQuery(collection, id, changes, remoteVersion) {
124
+ const setClauses = Object.keys(changes).map((col) => `${quoteIdent(col)} = ?`);
125
+ const lww = lwwVersionWhereClause(remoteVersion);
126
+ const sql = `UPDATE ${quoteIdent(collection)} SET ${setClauses.join(", ")} WHERE id = ? AND ${lww.sql}`;
127
+ const params = [...Object.values(changes), id, ...lww.params];
128
+ return { sql, params };
129
+ }
130
+ function buildLwwSoftDeleteQuery(collection, id, updatedAt, version) {
131
+ const lww = lwwVersionWhereClause(version);
132
+ return {
133
+ sql: `UPDATE ${quoteIdent(collection)} SET _deleted = 1, _updated_at = ?, _version = ? WHERE id = ? AND ${lww.sql}`,
134
+ params: [updatedAt, version, id, ...lww.params]
135
+ };
136
+ }
137
+ function buildWhereClause(where, fields) {
138
+ const params = [];
139
+ const result = buildWhereClauseParts(where, fields, params);
140
+ if (!result) return null;
141
+ return { sql: result, params };
142
+ }
143
+ var VALID_OPERATORS = /* @__PURE__ */ new Set(["$eq", "$ne", "$gt", "$gte", "$lt", "$lte", "$in"]);
144
+ function buildWhereClauseParts(where, fields, params) {
145
+ const conditions = [];
146
+ for (const [fieldName, value] of Object.entries(where)) {
147
+ validateFieldName(fieldName, fields);
148
+ const descriptor = fields[fieldName];
149
+ const column = resolveColumnName(fieldName, fields);
150
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
151
+ const ops = value;
152
+ for (const [op, opValue] of Object.entries(ops)) {
153
+ if (!VALID_OPERATORS.has(op)) {
154
+ throw new QueryError(`Unknown operator "${op}" on field "${fieldName}"`, {
155
+ field: fieldName,
156
+ operator: op,
157
+ validOperators: [...VALID_OPERATORS]
158
+ });
159
+ }
160
+ conditions.push(buildOperatorCondition(fieldName, column, op, opValue, descriptor, params));
161
+ }
162
+ } else {
163
+ conditions.push(buildOperatorCondition(fieldName, column, "$eq", value, descriptor, params));
164
+ }
165
+ }
166
+ if (conditions.length === 0) return null;
167
+ return conditions.join(" AND ");
168
+ }
169
+ function buildOperatorCondition(fieldName, columnName, operator, value, descriptor, params) {
170
+ const sqlValue = toSqlFilterValue(value, descriptor);
171
+ const column = quoteIdent(columnName);
172
+ switch (operator) {
173
+ case "$eq":
174
+ if (sqlValue === null) {
175
+ return `${column} IS NULL`;
176
+ }
177
+ params.push(sqlValue);
178
+ return `${column} = ?`;
179
+ case "$ne":
180
+ if (sqlValue === null) {
181
+ return `${column} IS NOT NULL`;
182
+ }
183
+ params.push(sqlValue);
184
+ return `${column} != ?`;
185
+ case "$gt":
186
+ params.push(sqlValue);
187
+ return `${column} > ?`;
188
+ case "$gte":
189
+ params.push(sqlValue);
190
+ return `${column} >= ?`;
191
+ case "$lt":
192
+ params.push(sqlValue);
193
+ return `${column} < ?`;
194
+ case "$lte":
195
+ params.push(sqlValue);
196
+ return `${column} <= ?`;
197
+ case "$in": {
198
+ if (!Array.isArray(sqlValue)) {
199
+ throw new QueryError(`$in operator requires an array value for field "${fieldName}"`, {
200
+ field: fieldName,
201
+ received: typeof sqlValue
202
+ });
203
+ }
204
+ const placeholders = sqlValue.map(() => "?");
205
+ for (const item of sqlValue) {
206
+ params.push(toSqlFilterValue(item, descriptor));
207
+ }
208
+ return `${column} IN (${placeholders.join(", ")})`;
209
+ }
210
+ default:
211
+ throw new QueryError(`Unknown operator "${operator}"`, { operator });
212
+ }
213
+ }
214
+ function toSqlFilterValue(value, descriptor) {
215
+ if (descriptor?.kind === "boolean" && typeof value === "boolean") return value ? 1 : 0;
216
+ return encodeStoredFilterValue(value, descriptor);
217
+ }
218
+ var VIRTUAL_TIMESTAMP_FIELDS = Object.freeze({
219
+ createdAt: "_created_at",
220
+ updatedAt: "_updated_at"
221
+ });
222
+ var VIRTUAL_TIMESTAMP_FIELD_NAMES = Object.freeze([
223
+ "createdAt",
224
+ "updatedAt"
225
+ ]);
226
+ var SORT_DIRECTIONS = Object.freeze({
227
+ asc: "ASC",
228
+ desc: "DESC"
229
+ });
230
+ function resolveColumnName(fieldName, fields) {
231
+ if (Object.hasOwn(fields, fieldName)) return fieldName;
232
+ if (fieldName === "createdAt" || fieldName === "updatedAt") {
233
+ return VIRTUAL_TIMESTAMP_FIELDS[fieldName];
234
+ }
235
+ return fieldName;
236
+ }
237
+ function sqlSortDirection(direction, field) {
238
+ const key = typeof direction === "string" ? direction.toLowerCase() : "";
239
+ const keyword = Object.hasOwn(SORT_DIRECTIONS, key) ? SORT_DIRECTIONS[key] : void 0;
240
+ if (keyword === void 0) {
241
+ throw new QueryError(`Invalid sort direction for field "${field}". Use 'asc' or 'desc'.`, {
242
+ field,
243
+ direction: typeof direction === "string" ? direction.slice(0, 64) : typeof direction
244
+ });
245
+ }
246
+ return keyword;
247
+ }
248
+ function assertQueryCount(value, clause) {
249
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
250
+ throw new QueryError(
251
+ `Invalid ${clause}: expected a non-negative safe integer. Pass a whole number, for example .${clause}(10).`,
252
+ { clause, received: typeof value === "number" ? value : typeof value }
253
+ );
254
+ }
255
+ return value;
256
+ }
257
+ function validateFieldName(fieldName, fields) {
258
+ const allowedFields = /* @__PURE__ */ new Set([
259
+ ...Object.keys(fields),
260
+ "id",
261
+ ...VIRTUAL_TIMESTAMP_FIELD_NAMES,
262
+ "_created_at",
263
+ "_updated_at"
264
+ ]);
265
+ if (!allowedFields.has(fieldName)) {
266
+ throw new QueryError(
267
+ `Unknown field "${fieldName}" in query. Available fields: ${[...allowedFields].join(", ")}`,
268
+ { field: fieldName }
269
+ );
270
+ }
271
+ }
272
+
273
+ // src/sync/sync-state.ts
274
+ import { createVersionVector } from "@korajs/core";
275
+ var LAST_ACKED_SERVER_VECTOR_META_KEY = "last_acked_server_vector";
276
+ var DELTA_CURSOR_META_KEY = "delta_cursor";
277
+ var DELIVERY_WATERMARK_META_KEY = "delivery_watermark";
278
+ var NODE_TOKEN_META_KEY = "sync_node_token";
279
+ var AUTHORITATIVE_NODE_IDS_META_KEY = "sync_authoritative_node_ids";
280
+ function serializeVersionVectorToMeta(vector) {
281
+ const record = {};
282
+ for (const [nodeId, seq] of vector) {
283
+ record[nodeId] = seq;
284
+ }
285
+ return JSON.stringify(record);
286
+ }
287
+ function deserializeVersionVectorFromMeta(value) {
288
+ const parsed = JSON.parse(value);
289
+ const vector = createVersionVector();
290
+ for (const [nodeId, seq] of Object.entries(parsed)) {
291
+ if (typeof seq === "number" && seq >= 0) {
292
+ vector.set(nodeId, seq);
293
+ }
294
+ }
295
+ return vector;
296
+ }
297
+ function mergeVersionVectors(a, b) {
298
+ const merged = new Map(a);
299
+ for (const [nodeId, seq] of b) {
300
+ merged.set(nodeId, Math.max(merged.get(nodeId) ?? 0, seq));
301
+ }
302
+ return merged;
303
+ }
304
+ async function collectOperationsAheadOfServer(localVector, serverVector, fetchRange) {
305
+ const missing = [];
306
+ for (const [nodeId, localSeq] of localVector) {
307
+ const serverSeq = serverVector.get(nodeId) ?? 0;
308
+ if (localSeq > serverSeq) {
309
+ const ops = await fetchRange(nodeId, serverSeq + 1, localSeq);
310
+ missing.push(...ops);
311
+ }
312
+ }
313
+ missing.sort((a, b) => a.sequenceNumber - b.sequenceNumber);
314
+ return missing;
315
+ }
316
+ async function loadLastAckedServerVector(adapter) {
317
+ const rows = await adapter.query("SELECT value FROM _kora_meta WHERE key = ?", [
318
+ LAST_ACKED_SERVER_VECTOR_META_KEY
319
+ ]);
320
+ const row = rows[0];
321
+ if (!row?.value) {
322
+ return createVersionVector();
323
+ }
324
+ try {
325
+ return deserializeVersionVectorFromMeta(row.value);
326
+ } catch {
327
+ return createVersionVector();
328
+ }
329
+ }
330
+ async function saveLastAckedServerVector(adapter, vector) {
331
+ await adapter.execute("INSERT OR REPLACE INTO _kora_meta (key, value) VALUES (?, ?)", [
332
+ LAST_ACKED_SERVER_VECTOR_META_KEY,
333
+ serializeVersionVectorToMeta(vector)
334
+ ]);
335
+ }
336
+ async function loadNodeToken(adapter, nodeId) {
337
+ if (nodeId !== void 0) {
338
+ const own = await adapter.query("SELECT value FROM _kora_meta WHERE key = ?", [
339
+ nodeTokenKey(nodeId)
340
+ ]);
341
+ if (own[0]) return own[0].value;
342
+ const meta = await adapter.query("SELECT value FROM _kora_meta WHERE key = 'node_id'");
343
+ if (meta[0]?.value !== nodeId) return null;
344
+ }
345
+ const rows = await adapter.query("SELECT value FROM _kora_meta WHERE key = ?", [
346
+ NODE_TOKEN_META_KEY
347
+ ]);
348
+ return rows[0]?.value ?? null;
349
+ }
350
+ function nodeTokenKey(nodeId) {
351
+ return `${NODE_TOKEN_META_KEY}:${nodeId}`;
352
+ }
353
+ async function saveNodeToken(adapter, token, nodeId) {
354
+ await adapter.execute("INSERT OR REPLACE INTO _kora_meta (key, value) VALUES (?, ?)", [
355
+ nodeId === void 0 ? NODE_TOKEN_META_KEY : nodeTokenKey(nodeId),
356
+ token
357
+ ]);
358
+ }
359
+ async function loadDeltaCursor(adapter) {
360
+ const rows = await adapter.query("SELECT value FROM _kora_meta WHERE key = ?", [
361
+ DELTA_CURSOR_META_KEY
362
+ ]);
363
+ return rows[0]?.value ?? null;
364
+ }
365
+ async function saveDeltaCursor(adapter, cursor) {
366
+ if (cursor === null) {
367
+ await adapter.execute("DELETE FROM _kora_meta WHERE key = ?", [DELTA_CURSOR_META_KEY]);
368
+ return;
369
+ }
370
+ await adapter.execute("INSERT OR REPLACE INTO _kora_meta (key, value) VALUES (?, ?)", [
371
+ DELTA_CURSOR_META_KEY,
372
+ cursor
373
+ ]);
374
+ }
375
+ function deliveryWatermarkKey(signature) {
376
+ return signature === "" ? DELIVERY_WATERMARK_META_KEY : `${DELIVERY_WATERMARK_META_KEY}:${signature}`;
377
+ }
378
+ async function loadDeliveryWatermark(adapter, signature) {
379
+ const rows = await adapter.query("SELECT value FROM _kora_meta WHERE key = ?", [
380
+ deliveryWatermarkKey(signature)
381
+ ]);
382
+ const value = rows[0]?.value;
383
+ if (value === void 0 || value === null) {
384
+ return 0;
385
+ }
386
+ const parsed = Number(value);
387
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
388
+ }
389
+ async function saveDeliveryWatermark(adapter, signature, watermark) {
390
+ await adapter.execute("INSERT OR REPLACE INTO _kora_meta (key, value) VALUES (?, ?)", [
391
+ deliveryWatermarkKey(signature),
392
+ String(watermark)
393
+ ]);
394
+ }
395
+ async function deleteDeliveryWatermark(adapter, signature) {
396
+ await adapter.execute("DELETE FROM _kora_meta WHERE key = ?", [deliveryWatermarkKey(signature)]);
397
+ }
398
+ async function loadAllDeliveryWatermarks(adapter) {
399
+ const rows = await adapter.query(
400
+ "SELECT key, value FROM _kora_meta WHERE key = ? OR key LIKE ?",
401
+ [DELIVERY_WATERMARK_META_KEY, `${DELIVERY_WATERMARK_META_KEY}:%`]
402
+ );
403
+ const result = {};
404
+ const prefix = `${DELIVERY_WATERMARK_META_KEY}:`;
405
+ for (const row of rows) {
406
+ const signature = row.key === DELIVERY_WATERMARK_META_KEY ? "" : row.key.slice(prefix.length);
407
+ const parsed = Number(row.value);
408
+ if (Number.isFinite(parsed) && parsed >= 0) {
409
+ result[signature] = parsed;
410
+ }
411
+ }
412
+ return result;
413
+ }
414
+ async function saveAuthoritativeNodeIds(adapter, nodeIds) {
415
+ await adapter.execute("INSERT OR REPLACE INTO _kora_meta (key, value) VALUES (?, ?)", [
416
+ AUTHORITATIVE_NODE_IDS_META_KEY,
417
+ JSON.stringify(nodeIds)
418
+ ]);
419
+ }
420
+ var REVOKED_AUTHORITATIVE_NODE_IDS_META_KEY = "sync_revoked_authoritative_node_ids";
421
+ async function saveRevokedAuthoritativeNodeIds(adapter, nodeIds) {
422
+ await adapter.execute("INSERT OR REPLACE INTO _kora_meta (key, value) VALUES (?, ?)", [
423
+ REVOKED_AUTHORITATIVE_NODE_IDS_META_KEY,
424
+ JSON.stringify(nodeIds)
425
+ ]);
426
+ }
427
+ async function loadRevokedAuthoritativeNodeIds(adapter) {
428
+ const rows = await adapter.query("SELECT value FROM _kora_meta WHERE key = ?", [
429
+ REVOKED_AUTHORITATIVE_NODE_IDS_META_KEY
430
+ ]);
431
+ const raw = rows[0]?.value;
432
+ if (raw === void 0) return [];
433
+ try {
434
+ const parsed = JSON.parse(raw);
435
+ if (Array.isArray(parsed)) return parsed.filter((id) => typeof id === "string");
436
+ } catch {
437
+ }
438
+ return [];
439
+ }
440
+ async function loadAuthoritativeNodeIds(adapter) {
441
+ const rows = await adapter.query("SELECT value FROM _kora_meta WHERE key = ?", [
442
+ AUTHORITATIVE_NODE_IDS_META_KEY
443
+ ]);
444
+ const raw = rows[0]?.value;
445
+ if (raw === void 0) return null;
446
+ try {
447
+ const parsed = JSON.parse(raw);
448
+ if (Array.isArray(parsed) && parsed.every((id) => typeof id === "string")) {
449
+ return parsed;
450
+ }
451
+ } catch {
452
+ }
453
+ return null;
454
+ }
455
+
456
+ // src/sync/sync-durability.ts
457
+ var OWN_ACKED_THROUGH_META_KEY = "own_acked_through";
458
+ var ACCEPTED_DOWNLINK_SCOPE_META_KEY = "accepted_downlink_scope";
459
+ var UNAPPLIED_OPS_DDL = "CREATE TABLE IF NOT EXISTS _kora_unapplied_ops (\n op_id TEXT PRIMARY KEY NOT NULL,\n collection TEXT NOT NULL,\n delivery_seq INTEGER,\n code TEXT NOT NULL,\n message TEXT NOT NULL,\n payload TEXT NOT NULL,\n quarantined_at INTEGER NOT NULL\n)";
460
+ var ensuredAdapters = /* @__PURE__ */ new WeakSet();
461
+ async function ensureUnappliedTable(adapter) {
462
+ if (ensuredAdapters.has(adapter)) return;
463
+ await adapter.execute(UNAPPLIED_OPS_DDL);
464
+ ensuredAdapters.add(adapter);
465
+ }
466
+ async function saveUnappliedOperations(adapter, entries, watermark) {
467
+ await ensureUnappliedTable(adapter);
468
+ await adapter.transaction(async (tx) => {
469
+ for (const entry of entries) {
470
+ await tx.execute(
471
+ "INSERT OR REPLACE INTO _kora_unapplied_ops (op_id, collection, delivery_seq, code, message, payload, quarantined_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
472
+ [
473
+ entry.operation.id,
474
+ entry.operation.collection,
475
+ entry.deliverySequence,
476
+ entry.code,
477
+ entry.message,
478
+ JSON.stringify(entry.operation),
479
+ entry.quarantinedAt
480
+ ]
481
+ );
482
+ }
483
+ if (watermark) {
484
+ await tx.execute("INSERT OR REPLACE INTO _kora_meta (key, value) VALUES (?, ?)", [
485
+ deliveryWatermarkKey(watermark.signature),
486
+ String(watermark.watermark)
487
+ ]);
488
+ }
489
+ });
490
+ }
491
+ async function loadUnappliedOperations(adapter) {
492
+ await ensureUnappliedTable(adapter);
493
+ const rows = await adapter.query(
494
+ "SELECT op_id, delivery_seq, code, message, payload, quarantined_at FROM _kora_unapplied_ops ORDER BY delivery_seq ASC, quarantined_at ASC"
495
+ );
496
+ return rows.map((row) => ({
497
+ operation: JSON.parse(row.payload),
498
+ deliverySequence: row.delivery_seq === null ? null : Number(row.delivery_seq),
499
+ code: row.code,
500
+ message: row.message,
501
+ quarantinedAt: Number(row.quarantined_at)
502
+ }));
503
+ }
504
+ async function removeUnappliedOperations(adapter, operationIds) {
505
+ if (operationIds.length === 0) return;
506
+ await ensureUnappliedTable(adapter);
507
+ const placeholders = operationIds.map(() => "?").join(", ");
508
+ await adapter.execute(
509
+ `DELETE FROM _kora_unapplied_ops WHERE op_id IN (${placeholders})`,
510
+ operationIds
511
+ );
512
+ }
513
+ function ownAckedThroughKey(nodeId) {
514
+ return `${OWN_ACKED_THROUGH_META_KEY}:${nodeId}`;
515
+ }
516
+ function parseLegacyPrefix(value) {
517
+ if (value === void 0 || value === null) return null;
518
+ try {
519
+ const parsed = JSON.parse(value);
520
+ if (typeof parsed.nodeId !== "string") return null;
521
+ const sequence = typeof parsed.sequence === "number" && parsed.sequence >= 0 ? parsed.sequence : null;
522
+ return sequence === null ? null : { nodeId: parsed.nodeId, sequence };
523
+ } catch {
524
+ return null;
525
+ }
526
+ }
527
+ async function loadOwnAckedThrough(adapter, nodeId) {
528
+ const rows = await adapter.query("SELECT value FROM _kora_meta WHERE key = ?", [
529
+ OWN_ACKED_THROUGH_META_KEY
530
+ ]);
531
+ const legacy = parseLegacyPrefix(rows[0]?.value);
532
+ if (legacy && legacy.nodeId === nodeId) return legacy.sequence;
533
+ const own = await adapter.query("SELECT value FROM _kora_meta WHERE key = ?", [
534
+ ownAckedThroughKey(nodeId)
535
+ ]);
536
+ const value = own[0]?.value;
537
+ if (value === void 0 || value === null) return null;
538
+ const sequence = Number(value);
539
+ return Number.isInteger(sequence) && sequence >= 0 ? sequence : null;
540
+ }
541
+ async function saveOwnAckedThrough(adapter, nodeId, sequence) {
542
+ await adapter.transaction(async (tx) => {
543
+ const rows = await tx.query("SELECT value FROM _kora_meta WHERE key = ?", [
544
+ OWN_ACKED_THROUGH_META_KEY
545
+ ]);
546
+ const legacy = parseLegacyPrefix(rows[0]?.value);
547
+ if (legacy === null || legacy.nodeId === nodeId) {
548
+ await tx.execute("INSERT OR REPLACE INTO _kora_meta (key, value) VALUES (?, ?)", [
549
+ OWN_ACKED_THROUGH_META_KEY,
550
+ JSON.stringify({ nodeId, sequence })
551
+ ]);
552
+ return;
553
+ }
554
+ await tx.execute("INSERT OR REPLACE INTO _kora_meta (key, value) VALUES (?, ?)", [
555
+ ownAckedThroughKey(nodeId),
556
+ String(sequence)
557
+ ]);
558
+ });
559
+ }
560
+ async function loadAcceptedDownlinkScope(adapter) {
561
+ const rows = await adapter.query("SELECT value FROM _kora_meta WHERE key = ?", [
562
+ ACCEPTED_DOWNLINK_SCOPE_META_KEY
563
+ ]);
564
+ const value = rows[0]?.value;
565
+ if (value === void 0 || value === null) return null;
566
+ try {
567
+ const parsed = JSON.parse(value);
568
+ return parsed !== null && typeof parsed === "object" ? parsed : null;
569
+ } catch {
570
+ return null;
571
+ }
572
+ }
573
+ async function saveAcceptedDownlinkScope(adapter, scope) {
574
+ if (scope === null) {
575
+ await adapter.execute("DELETE FROM _kora_meta WHERE key = ?", [
576
+ ACCEPTED_DOWNLINK_SCOPE_META_KEY
577
+ ]);
578
+ return;
579
+ }
580
+ await adapter.execute("INSERT OR REPLACE INTO _kora_meta (key, value) VALUES (?, ?)", [
581
+ ACCEPTED_DOWNLINK_SCOPE_META_KEY,
582
+ JSON.stringify(scope)
583
+ ]);
584
+ }
585
+
586
+ // src/sync/local-sync-records.ts
587
+ var TERMINAL_REJECTIONS_TABLE = "_kora_terminal_rejections";
588
+ var LOCAL_NODES_TABLE = "_kora_local_nodes";
589
+ var ACCEPTED_CYCLE_META_KEY = "sync_accepted_cycle";
590
+ var TERMINAL_SEED_META_KEY = "terminal_rejections_seeded_v1";
591
+ var NON_TERMINAL_REJECTION_CODES = /* @__PURE__ */ new Set([
592
+ "SEQUENCE_CONFLICT",
593
+ "NODE_ID_MISMATCH",
594
+ "OUT_OF_UPLINK_SCOPE"
595
+ ]);
596
+ var TERMINAL_REJECTIONS_DDL = `CREATE TABLE IF NOT EXISTS ${TERMINAL_REJECTIONS_TABLE} (
597
+ operation_id TEXT PRIMARY KEY NOT NULL,
598
+ node_id TEXT,
599
+ sequence_number INTEGER,
600
+ code TEXT NOT NULL,
601
+ rejected_at INTEGER NOT NULL
602
+ )`;
603
+ var LOCAL_NODES_DDL = `CREATE TABLE IF NOT EXISTS ${LOCAL_NODES_TABLE} (
604
+ node_id TEXT PRIMARY KEY NOT NULL,
605
+ created_at INTEGER NOT NULL,
606
+ accepted INTEGER NOT NULL DEFAULT 0,
607
+ held INTEGER NOT NULL DEFAULT 0,
608
+ refused_cycle INTEGER,
609
+ principal TEXT,
610
+ binding TEXT,
611
+ refused_principals TEXT
612
+ )`;
613
+ var LOCAL_NODE_BINDINGS = ["fresh", "server", "app"];
614
+ var MAX_REFUSED_PRINCIPALS = 32;
615
+ var ADOPTION_SCHEDULE_META_KEY = "sync_adoption_schedule_v1";
616
+ var migratedAdapters = /* @__PURE__ */ new WeakSet();
617
+ async function ensureLocalSyncRecordTables(adapter) {
618
+ await adapter.execute(TERMINAL_REJECTIONS_DDL);
619
+ await adapter.execute(LOCAL_NODES_DDL);
620
+ if (migratedAdapters.has(adapter)) return;
621
+ const columns = await adapter.query(`PRAGMA table_info(${LOCAL_NODES_TABLE})`);
622
+ for (const column of ["principal", "binding", "refused_principals"]) {
623
+ if (!columns.some((existing) => existing.name === column)) {
624
+ await adapter.execute(`ALTER TABLE ${LOCAL_NODES_TABLE} ADD COLUMN ${column} TEXT`);
625
+ }
626
+ }
627
+ migratedAdapters.add(adapter);
628
+ }
629
+ async function recordTerminalRejections(adapter, entries) {
630
+ const recordable = entries.filter((entry) => !NON_TERMINAL_REJECTION_CODES.has(entry.code));
631
+ if (recordable.length === 0) return;
632
+ await ensureLocalSyncRecordTables(adapter);
633
+ await adapter.transaction(async (tx) => {
634
+ for (const entry of recordable) {
635
+ await tx.execute(
636
+ `INSERT OR IGNORE INTO ${TERMINAL_REJECTIONS_TABLE} (operation_id, node_id, sequence_number, code, rejected_at) VALUES (?, ?, ?, ?, ?)`,
637
+ [entry.operationId, entry.nodeId, entry.sequenceNumber, entry.code, entry.rejectedAt]
638
+ );
639
+ }
640
+ });
641
+ }
642
+ async function findTerminalRejections(adapter, operationIds) {
643
+ const found = /* @__PURE__ */ new Set();
644
+ if (operationIds.length === 0) return found;
645
+ const CHUNK = 500;
646
+ for (let i = 0; i < operationIds.length; i += CHUNK) {
647
+ const chunk = operationIds.slice(i, i + CHUNK);
648
+ const rows = await adapter.query(
649
+ `SELECT operation_id FROM ${TERMINAL_REJECTIONS_TABLE} WHERE operation_id IN (${chunk.map(() => "?").join(", ")})`,
650
+ chunk
651
+ );
652
+ for (const row of rows) found.add(row.operation_id);
653
+ }
654
+ return found;
655
+ }
656
+ async function seedTerminalRejectionsOnce(adapter) {
657
+ await ensureLocalSyncRecordTables(adapter);
658
+ const done = await adapter.query("SELECT value FROM _kora_meta WHERE key = ?", [
659
+ TERMINAL_SEED_META_KEY
660
+ ]);
661
+ if (done.length > 0) return;
662
+ const tables = await adapter.query(
663
+ "SELECT name FROM sqlite_master WHERE type = 'table' AND name = '_kora_sync_rejected'"
664
+ );
665
+ await adapter.transaction(async (tx) => {
666
+ if (tables.length > 0) {
667
+ const placeholders = [...NON_TERMINAL_REJECTION_CODES].map(() => "?").join(", ");
668
+ await tx.execute(
669
+ `INSERT OR IGNORE INTO ${TERMINAL_REJECTIONS_TABLE} (operation_id, node_id, sequence_number, code, rejected_at)
670
+ SELECT operation_id, NULL, NULL, code, rejected_at FROM _kora_sync_rejected
671
+ WHERE retriable = 0 AND code NOT IN (${placeholders})`,
672
+ [...NON_TERMINAL_REJECTION_CODES]
673
+ );
674
+ }
675
+ await tx.execute("INSERT OR REPLACE INTO _kora_meta (key, value) VALUES (?, ?)", [
676
+ TERMINAL_SEED_META_KEY,
677
+ String(Date.now())
678
+ ]);
679
+ });
680
+ }
681
+ function rowToRecord(row) {
682
+ return {
683
+ nodeId: row.node_id,
684
+ createdAt: Number(row.created_at),
685
+ accepted: Number(row.accepted) === 1,
686
+ held: Number(row.held) === 1,
687
+ refusedCycle: row.refused_cycle === null ? null : Number(row.refused_cycle),
688
+ principal: typeof row.principal === "string" ? row.principal : null,
689
+ binding: typeof row.principal === "string" && LOCAL_NODE_BINDINGS.includes(row.binding) ? row.binding : null,
690
+ refusedPrincipals: parsePrincipalList(row.refused_principals)
691
+ };
692
+ }
693
+ function parsePrincipalList(value) {
694
+ if (typeof value !== "string" || value.length === 0) return [];
695
+ try {
696
+ const parsed = JSON.parse(value);
697
+ return Array.isArray(parsed) ? parsed.filter((entry) => typeof entry === "string") : [];
698
+ } catch {
699
+ return [];
700
+ }
701
+ }
702
+ async function registerLocalNode(adapter, nodeId) {
703
+ await ensureLocalSyncRecordTables(adapter);
704
+ const existing = await adapter.query(
705
+ `SELECT node_id FROM ${LOCAL_NODES_TABLE} WHERE node_id = ?`,
706
+ [nodeId]
707
+ );
708
+ if (existing.length > 0) return;
709
+ const accepted = await hasSyncHistory(adapter, nodeId);
710
+ await adapter.execute(
711
+ `INSERT OR IGNORE INTO ${LOCAL_NODES_TABLE} (node_id, created_at, accepted, held, refused_cycle) VALUES (?, ?, ?, 0, NULL)`,
712
+ [nodeId, Date.now(), accepted ? 1 : 0]
713
+ );
714
+ }
715
+ async function hasSyncHistory(adapter, nodeId) {
716
+ const rows = await adapter.query(
717
+ "SELECT key, value FROM _kora_meta WHERE key IN (?, ?, ?)",
718
+ [NODE_TOKEN_META_KEY, nodeTokenKey(nodeId), OWN_ACKED_THROUGH_META_KEY]
719
+ );
720
+ const nodeIdRows = await adapter.query(
721
+ "SELECT value FROM _kora_meta WHERE key = 'node_id'"
722
+ );
723
+ const isMetaNode = nodeIdRows[0]?.value === nodeId;
724
+ for (const row of rows) {
725
+ if (row.key === nodeTokenKey(nodeId)) return true;
726
+ if (row.key === NODE_TOKEN_META_KEY) {
727
+ if (isMetaNode) return true;
728
+ continue;
729
+ }
730
+ try {
731
+ const parsed = JSON.parse(row.value);
732
+ if (row.key === OWN_ACKED_THROUGH_META_KEY) {
733
+ if (parsed.nodeId === nodeId && typeof parsed.sequence === "number" && parsed.sequence > 0)
734
+ return true;
735
+ }
736
+ } catch {
737
+ }
738
+ }
739
+ return false;
740
+ }
741
+ async function listLocalNodes(adapter) {
742
+ const rows = await adapter.query(
743
+ `SELECT node_id, created_at, accepted, held, refused_cycle, principal, binding, refused_principals FROM ${LOCAL_NODES_TABLE} ORDER BY created_at ASC, node_id ASC`
744
+ );
745
+ return rows.map(rowToRecord);
746
+ }
747
+ async function readAcceptedCycle(tx) {
748
+ const rows = await tx.query("SELECT value FROM _kora_meta WHERE key = ?", [
749
+ ACCEPTED_CYCLE_META_KEY
750
+ ]);
751
+ const value = Number(rows[0]?.value ?? 0);
752
+ return Number.isFinite(value) ? value : 0;
753
+ }
754
+ async function markLocalNodeAccepted(adapter, nodeId) {
755
+ await ensureLocalSyncRecordTables(adapter);
756
+ await adapter.transaction(async (tx) => {
757
+ const cycle = await readAcceptedCycle(tx);
758
+ await tx.execute(
759
+ `INSERT INTO ${LOCAL_NODES_TABLE} (node_id, created_at, accepted, held, refused_cycle) VALUES (?, ?, 1, 0, NULL)
760
+ ON CONFLICT(node_id) DO UPDATE SET accepted = 1, held = 0, refused_cycle = NULL`,
761
+ [nodeId, Date.now()]
762
+ );
763
+ await tx.execute("INSERT OR REPLACE INTO _kora_meta (key, value) VALUES (?, ?)", [
764
+ ACCEPTED_CYCLE_META_KEY,
765
+ String(cycle + 1)
766
+ ]);
767
+ });
768
+ }
769
+ async function markLocalNodeRefused(adapter, nodeId, held) {
770
+ await ensureLocalSyncRecordTables(adapter);
771
+ await adapter.transaction(async (tx) => {
772
+ const cycle = await readAcceptedCycle(tx);
773
+ await tx.execute(
774
+ `INSERT INTO ${LOCAL_NODES_TABLE} (node_id, created_at, accepted, held, refused_cycle) VALUES (?, ?, 0, ?, ?)
775
+ ON CONFLICT(node_id) DO UPDATE SET held = MAX(held, excluded.held), refused_cycle = excluded.refused_cycle`,
776
+ [nodeId, Date.now(), held ? 1 : 0, cycle]
777
+ );
778
+ });
779
+ }
780
+ async function forgetLocalNode(adapter, nodeId) {
781
+ await ensureLocalSyncRecordTables(adapter);
782
+ await adapter.execute(
783
+ `DELETE FROM ${LOCAL_NODES_TABLE} WHERE node_id = ? AND held = 0 AND node_id NOT IN (SELECT value FROM _kora_meta WHERE key = 'node_id')`,
784
+ [nodeId]
785
+ );
786
+ }
787
+ async function setLocalNodePrincipal(adapter, nodeId, principal, binding) {
788
+ await ensureLocalSyncRecordTables(adapter);
789
+ await adapter.execute(
790
+ `INSERT INTO ${LOCAL_NODES_TABLE} (node_id, created_at, accepted, held, refused_cycle, principal, binding) VALUES (?, ?, 0, 0, NULL, ?, ?)
791
+ ON CONFLICT(node_id) DO UPDATE SET principal = excluded.principal, binding = excluded.binding`,
792
+ [nodeId, Date.now(), principal, binding]
793
+ );
794
+ }
795
+ function isGuessedBinding(node) {
796
+ return node.principal !== null && (node.binding === null || node.binding === "app");
797
+ }
798
+ async function findLocalNode(adapter, nodeId) {
799
+ const rows = await adapter.query(
800
+ `SELECT node_id, created_at, accepted, held, refused_cycle, principal, binding, refused_principals FROM ${LOCAL_NODES_TABLE} WHERE node_id = ?`,
801
+ [nodeId]
802
+ );
803
+ const row = rows[0];
804
+ return row ? rowToRecord(row) : null;
805
+ }
806
+ async function confirmLocalNodePrincipal(adapter, nodeId, principal) {
807
+ await ensureLocalSyncRecordTables(adapter);
808
+ await adapter.transaction(async (tx) => {
809
+ const node = await findLocalNode(tx, nodeId);
810
+ if (node && node.principal !== null && node.principal !== principal && !isGuessedBinding(node))
811
+ return;
812
+ await tx.execute(
813
+ `INSERT INTO ${LOCAL_NODES_TABLE} (node_id, created_at, accepted, held, refused_cycle, principal, binding) VALUES (?, ?, 0, 0, NULL, ?, 'server')
814
+ ON CONFLICT(node_id) DO UPDATE SET principal = excluded.principal, binding = 'server', refused_principals = NULL`,
815
+ [nodeId, Date.now(), principal]
816
+ );
817
+ });
818
+ }
819
+ async function recordLocalNodeRefusedFor(adapter, nodeId, principal) {
820
+ await ensureLocalSyncRecordTables(adapter);
821
+ await adapter.transaction(async (tx) => {
822
+ const node = await findLocalNode(tx, nodeId);
823
+ if (!node) return;
824
+ if (node.principal !== null && !isGuessedBinding(node)) return;
825
+ const refused = node.refusedPrincipals.filter((entry) => entry !== principal);
826
+ refused.push(principal);
827
+ await tx.execute(
828
+ `UPDATE ${LOCAL_NODES_TABLE} SET principal = NULL, binding = NULL, refused_principals = ? WHERE node_id = ?`,
829
+ [JSON.stringify(refused.slice(-MAX_REFUSED_PRINCIPALS)), nodeId]
830
+ );
831
+ });
832
+ }
833
+ async function assignLocalNodePrincipal(adapter, nodeId, principal) {
834
+ await ensureLocalSyncRecordTables(adapter);
835
+ let assigned = false;
836
+ await adapter.transaction(async (tx) => {
837
+ const node = await findLocalNode(tx, nodeId);
838
+ if (!node || node.principal !== null || node.refusedPrincipals.includes(principal)) return;
839
+ await tx.execute(
840
+ `UPDATE ${LOCAL_NODES_TABLE} SET principal = ?, binding = 'app', held = 0, refused_cycle = NULL WHERE node_id = ?`,
841
+ [principal, nodeId]
842
+ );
843
+ assigned = true;
844
+ });
845
+ return assigned;
846
+ }
847
+ async function dropLocalNode(adapter, nodeId) {
848
+ await ensureLocalSyncRecordTables(adapter);
849
+ await adapter.execute(
850
+ `DELETE FROM ${LOCAL_NODES_TABLE} WHERE node_id = ? AND node_id NOT IN (SELECT value FROM _kora_meta WHERE key = 'node_id')`,
851
+ [nodeId]
852
+ );
853
+ }
854
+ async function loadAdoptionSchedule(adapter) {
855
+ const rows = await adapter.query("SELECT value FROM _kora_meta WHERE key = ?", [
856
+ ADOPTION_SCHEDULE_META_KEY
857
+ ]);
858
+ const empty = { progress: 0, parked: {} };
859
+ const raw = rows[0]?.value;
860
+ if (!raw) return empty;
861
+ try {
862
+ const parsed = JSON.parse(raw);
863
+ return {
864
+ progress: typeof parsed.progress === "number" ? parsed.progress : 0,
865
+ parked: parsed.parked && typeof parsed.parked === "object" ? parsed.parked : {}
866
+ };
867
+ } catch {
868
+ return empty;
869
+ }
870
+ }
871
+ async function saveAdoptionSchedule(adapter, schedule) {
872
+ await adapter.execute("INSERT OR REPLACE INTO _kora_meta (key, value) VALUES (?, ?)", [
873
+ ADOPTION_SCHEDULE_META_KEY,
874
+ JSON.stringify(schedule)
875
+ ]);
876
+ }
877
+ async function loadAcceptedCycle(adapter) {
878
+ const rows = await adapter.query("SELECT value FROM _kora_meta WHERE key = ?", [
879
+ ACCEPTED_CYCLE_META_KEY
880
+ ]);
881
+ const value = Number(rows[0]?.value ?? 0);
882
+ return Number.isFinite(value) ? value : 0;
883
+ }
884
+ async function hasUnsyncedOwnOperations(db) {
885
+ const tables = new Set(
886
+ (await db.query("SELECT name FROM sqlite_master WHERE type = 'table'")).map(
887
+ (row) => row.name
888
+ )
889
+ );
890
+ const opsTables = [...tables].filter((name) => name.startsWith("_kora_ops_"));
891
+ if (opsTables.length === 0 || !tables.has("_kora_meta")) return false;
892
+ const nodes = /* @__PURE__ */ new Set();
893
+ if (tables.has(LOCAL_NODES_TABLE)) {
894
+ for (const row of await db.query(
895
+ `SELECT node_id FROM ${LOCAL_NODES_TABLE}`
896
+ )) {
897
+ nodes.add(row.node_id);
898
+ }
899
+ }
900
+ const meta = await db.query(
901
+ "SELECT key, value FROM _kora_meta WHERE key = 'node_id' OR key = ? OR key LIKE ?",
902
+ [OWN_ACKED_THROUGH_META_KEY, `${OWN_ACKED_THROUGH_META_KEY}:%`]
903
+ );
904
+ const prefixes = /* @__PURE__ */ new Map();
905
+ for (const row of meta) {
906
+ if (row.key === "node_id") {
907
+ nodes.add(row.value);
908
+ } else if (row.key === OWN_ACKED_THROUGH_META_KEY) {
909
+ try {
910
+ const parsed = JSON.parse(row.value);
911
+ if (typeof parsed.nodeId === "string") {
912
+ nodes.add(parsed.nodeId);
913
+ if (typeof parsed.sequence === "number") prefixes.set(parsed.nodeId, parsed.sequence);
914
+ }
915
+ } catch {
916
+ }
917
+ } else {
918
+ const nodeId = row.key.slice(OWN_ACKED_THROUGH_META_KEY.length + 1);
919
+ const sequence = Number(row.value);
920
+ if (!prefixes.has(nodeId) && Number.isInteger(sequence)) prefixes.set(nodeId, sequence);
921
+ }
922
+ }
923
+ const exclude = tables.has(TERMINAL_REJECTIONS_TABLE) ? ` AND id NOT IN (SELECT operation_id FROM ${TERMINAL_REJECTIONS_TABLE})` : "";
924
+ for (const nodeId of nodes) {
925
+ const prefix = prefixes.get(nodeId) ?? 0;
926
+ for (const table of opsTables) {
927
+ const rows = await db.query(
928
+ `SELECT COUNT(*) AS n FROM "${table.replace(/"/g, '""')}" WHERE node_id = ? AND sequence_number > ?${exclude}`,
929
+ [nodeId, prefix]
930
+ );
931
+ if (Number(rows[0]?.n ?? 0) > 0) return true;
932
+ }
933
+ }
934
+ return false;
935
+ }
936
+
937
+ export {
938
+ EMPTY_ROW_VERSION,
939
+ serializeRowVersion,
940
+ rowVersionFromRecord,
941
+ isIncomingNewerThanRow,
942
+ buildSelectQuery,
943
+ buildCountQuery,
944
+ buildInsertQuery,
945
+ buildUpdateQuery,
946
+ buildFieldFastForwardUpdateQuery,
947
+ buildSoftDeleteQuery,
948
+ buildLwwUpdateQuery,
949
+ buildLwwSoftDeleteQuery,
950
+ buildWhereClause,
951
+ VIRTUAL_TIMESTAMP_FIELDS,
952
+ VIRTUAL_TIMESTAMP_FIELD_NAMES,
953
+ LAST_ACKED_SERVER_VECTOR_META_KEY,
954
+ DELTA_CURSOR_META_KEY,
955
+ DELIVERY_WATERMARK_META_KEY,
956
+ NODE_TOKEN_META_KEY,
957
+ serializeVersionVectorToMeta,
958
+ deserializeVersionVectorFromMeta,
959
+ mergeVersionVectors,
960
+ collectOperationsAheadOfServer,
961
+ loadLastAckedServerVector,
962
+ saveLastAckedServerVector,
963
+ loadNodeToken,
964
+ nodeTokenKey,
965
+ saveNodeToken,
966
+ loadDeltaCursor,
967
+ saveDeltaCursor,
968
+ loadDeliveryWatermark,
969
+ saveDeliveryWatermark,
970
+ deleteDeliveryWatermark,
971
+ loadAllDeliveryWatermarks,
972
+ saveAuthoritativeNodeIds,
973
+ saveRevokedAuthoritativeNodeIds,
974
+ loadRevokedAuthoritativeNodeIds,
975
+ loadAuthoritativeNodeIds,
976
+ saveUnappliedOperations,
977
+ loadUnappliedOperations,
978
+ removeUnappliedOperations,
979
+ loadOwnAckedThrough,
980
+ saveOwnAckedThrough,
981
+ loadAcceptedDownlinkScope,
982
+ saveAcceptedDownlinkScope,
983
+ TERMINAL_REJECTIONS_TABLE,
984
+ ensureLocalSyncRecordTables,
985
+ recordTerminalRejections,
986
+ findTerminalRejections,
987
+ seedTerminalRejectionsOnce,
988
+ registerLocalNode,
989
+ listLocalNodes,
990
+ markLocalNodeAccepted,
991
+ markLocalNodeRefused,
992
+ forgetLocalNode,
993
+ setLocalNodePrincipal,
994
+ confirmLocalNodePrincipal,
995
+ recordLocalNodeRefusedFor,
996
+ assignLocalNodePrincipal,
997
+ dropLocalNode,
998
+ loadAdoptionSchedule,
999
+ saveAdoptionSchedule,
1000
+ loadAcceptedCycle,
1001
+ hasUnsyncedOwnOperations
1002
+ };
1003
+ //# sourceMappingURL=chunk-SJP2HTYZ.js.map