@korajs/store 1.0.0-beta.0 → 1.0.0-beta.10

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 (81) hide show
  1. package/dist/adapters/better-sqlite3.cjs.map +1 -1
  2. package/dist/adapters/better-sqlite3.d.cts +1 -1
  3. package/dist/adapters/better-sqlite3.d.ts +1 -1
  4. package/dist/adapters/better-sqlite3.js +1 -1
  5. package/dist/adapters/indexeddb.cjs +523 -148
  6. package/dist/adapters/indexeddb.cjs.map +1 -1
  7. package/dist/adapters/indexeddb.d.cts +4 -2
  8. package/dist/adapters/indexeddb.d.ts +4 -2
  9. package/dist/adapters/indexeddb.js +55 -11
  10. package/dist/adapters/indexeddb.js.map +1 -1
  11. package/dist/adapters/sqlite-wasm-worker.cjs +207 -187
  12. package/dist/adapters/sqlite-wasm-worker.cjs.map +1 -1
  13. package/dist/adapters/sqlite-wasm-worker.js +208 -188
  14. package/dist/adapters/sqlite-wasm-worker.js.map +1 -1
  15. package/dist/adapters/sqlite-wasm.cjs +474 -140
  16. package/dist/adapters/sqlite-wasm.cjs.map +1 -1
  17. package/dist/adapters/sqlite-wasm.d.cts +45 -8
  18. package/dist/adapters/sqlite-wasm.d.ts +45 -8
  19. package/dist/adapters/sqlite-wasm.js +3 -3
  20. package/dist/{backup-5XZ7THNJ.js → backup-5S7K2Z35.js} +3 -3
  21. package/dist/blob-fs.cjs +166 -0
  22. package/dist/blob-fs.cjs.map +1 -0
  23. package/dist/blob-fs.d.cts +29 -0
  24. package/dist/blob-fs.d.ts +29 -0
  25. package/dist/blob-fs.js +128 -0
  26. package/dist/blob-fs.js.map +1 -0
  27. package/dist/{chunk-EIVXWF4B.js → chunk-CYO3YKFL.js} +4 -3
  28. package/dist/chunk-CYO3YKFL.js.map +1 -0
  29. package/dist/{chunk-ETDJEH2I.js → chunk-GSJ7EEUC.js} +12 -1
  30. package/dist/chunk-GSJ7EEUC.js.map +1 -0
  31. package/dist/{chunk-5R4QMY3Z.js → chunk-HS6DBMST.js} +13 -10
  32. package/dist/chunk-HS6DBMST.js.map +1 -0
  33. package/dist/chunk-RJIEKEN4.js +751 -0
  34. package/dist/chunk-RJIEKEN4.js.map +1 -0
  35. package/dist/{chunk-QL2XVZRN.js → chunk-TUR7RGEP.js} +12 -1
  36. package/dist/{chunk-QL2XVZRN.js.map → chunk-TUR7RGEP.js.map} +1 -1
  37. package/dist/{chunk-IOEFNHHP.js → chunk-VDBJQ75K.js} +2 -2
  38. package/dist/chunk-VDBJQ75K.js.map +1 -0
  39. package/dist/chunk-W4MTTNEZ.js +55 -0
  40. package/dist/chunk-W4MTTNEZ.js.map +1 -0
  41. package/dist/{chunk-OUQDC5Z6.js → chunk-YT7MRFCP.js} +55 -35
  42. package/dist/chunk-YT7MRFCP.js.map +1 -0
  43. package/dist/content-addressed-blob-store-Dcj3SB9r.d.cts +60 -0
  44. package/dist/content-addressed-blob-store-Dcj3SB9r.d.ts +60 -0
  45. package/dist/{export-audit-VPGQWNR6.js → export-audit-ODGQTQIK.js} +3 -3
  46. package/dist/index.cjs +923 -131
  47. package/dist/index.cjs.map +1 -1
  48. package/dist/index.d.cts +443 -6
  49. package/dist/index.d.ts +443 -6
  50. package/dist/index.js +737 -49
  51. package/dist/index.js.map +1 -1
  52. package/dist/internal.cjs +69 -43
  53. package/dist/internal.cjs.map +1 -1
  54. package/dist/internal.d.cts +3 -3
  55. package/dist/internal.d.ts +3 -3
  56. package/dist/internal.js +3 -3
  57. package/dist/sqlite-wasm-channel-GGHA2BXU.js +12 -0
  58. package/dist/{sqlite-wasm-channel-DUDPclTE.d.cts → sqlite-wasm-channel-L6o3bWQK.d.cts} +1 -1
  59. package/dist/{sqlite-wasm-channel-DUDPclTE.d.ts → sqlite-wasm-channel-L6o3bWQK.d.ts} +1 -1
  60. package/dist/{types-CyjXwrjW.d.cts → types-BOO9EPt1.d.ts} +10 -3
  61. package/dist/{types-CJOVKxLw.d.ts → types-BslDOTk0.d.cts} +32 -2
  62. package/dist/{types-CJOVKxLw.d.cts → types-BslDOTk0.d.ts} +32 -2
  63. package/dist/{types-DAxCladl.d.ts → types-C_GDavG-.d.cts} +10 -3
  64. package/package.json +13 -12
  65. package/dist/adapters/sqlite-wasm-shared-host.cjs +0 -68
  66. package/dist/adapters/sqlite-wasm-shared-host.cjs.map +0 -1
  67. package/dist/adapters/sqlite-wasm-shared-host.d.cts +0 -2
  68. package/dist/adapters/sqlite-wasm-shared-host.d.ts +0 -2
  69. package/dist/adapters/sqlite-wasm-shared-host.js +0 -66
  70. package/dist/adapters/sqlite-wasm-shared-host.js.map +0 -1
  71. package/dist/chunk-5R4QMY3Z.js.map +0 -1
  72. package/dist/chunk-EIVXWF4B.js.map +0 -1
  73. package/dist/chunk-ETDJEH2I.js.map +0 -1
  74. package/dist/chunk-IOEFNHHP.js.map +0 -1
  75. package/dist/chunk-OUQDC5Z6.js.map +0 -1
  76. package/dist/chunk-RBPVEAVM.js +0 -425
  77. package/dist/chunk-RBPVEAVM.js.map +0 -1
  78. package/dist/sqlite-wasm-channel-H43KEZLM.js +0 -12
  79. /package/dist/{backup-5XZ7THNJ.js.map → backup-5S7K2Z35.js.map} +0 -0
  80. /package/dist/{export-audit-VPGQWNR6.js.map → export-audit-ODGQTQIK.js.map} +0 -0
  81. /package/dist/{sqlite-wasm-channel-H43KEZLM.js.map → sqlite-wasm-channel-GGHA2BXU.js.map} +0 -0
package/dist/index.js CHANGED
@@ -2,10 +2,14 @@ import {
2
2
  decodeAuditExport,
3
3
  readAuditExportManifest,
4
4
  verifyAuditExportChecksum
5
- } from "./chunk-EIVXWF4B.js";
5
+ } from "./chunk-CYO3YKFL.js";
6
6
  import {
7
7
  persistedAuditTraceFromEvent
8
8
  } from "./chunk-MYSJN3DF.js";
9
+ import {
10
+ BlobIntegrityError,
11
+ MemoryBlobStore
12
+ } from "./chunk-W4MTTNEZ.js";
9
13
  import {
10
14
  InvalidStateTransitionError,
11
15
  allocateNextSequenceInTransaction,
@@ -29,9 +33,10 @@ import {
29
33
  serializeRowVersion,
30
34
  validateStateTransition,
31
35
  validateUpdateStateMachine
32
- } from "./chunk-OUQDC5Z6.js";
36
+ } from "./chunk-YT7MRFCP.js";
33
37
  import {
34
38
  AdapterError,
39
+ NoLeaderError,
35
40
  OptimisticLockError,
36
41
  PersistenceError,
37
42
  QueryError,
@@ -39,13 +44,13 @@ import {
39
44
  StoreNotOpenError,
40
45
  WorkerInitError,
41
46
  WorkerTimeoutError
42
- } from "./chunk-QL2XVZRN.js";
47
+ } from "./chunk-TUR7RGEP.js";
43
48
  import {
44
49
  exportBackup,
45
50
  readBackupManifest,
46
51
  restoreBackup,
47
52
  verifyBackupChecksum
48
- } from "./chunk-5R4QMY3Z.js";
53
+ } from "./chunk-HS6DBMST.js";
49
54
  import {
50
55
  decodeRichtext,
51
56
  decodeRichtextFieldsFromOpData,
@@ -57,13 +62,13 @@ import {
57
62
  richtextToPlainText,
58
63
  serializeOperation,
59
64
  serializeRecord
60
- } from "./chunk-ETDJEH2I.js";
65
+ } from "./chunk-GSJ7EEUC.js";
61
66
 
62
67
  // src/compaction/types.ts
63
68
  var COMPACTION_BASELINE_META_KEY = "compaction_baseline_at";
64
69
 
65
70
  // src/compaction/compact-operation-log.ts
66
- import { HybridLogicalClock, createVersionVector } from "@korajs/core";
71
+ import { HybridLogicalClock, createVersionVector, quoteIdent } from "@korajs/core";
67
72
  function computeAckCompactionWatermark(serverVector) {
68
73
  const watermark = createVersionVector();
69
74
  for (const [nodeId, seq] of serverVector) {
@@ -91,7 +96,7 @@ async function compactOperationLog(adapter, schema, strategy, serverVector) {
91
96
  const collectionNames = Object.keys(schema.collections);
92
97
  await adapter.transaction(async (tx) => {
93
98
  for (const collectionName of collectionNames) {
94
- const table = `_kora_ops_${collectionName}`;
99
+ const table = quoteIdent(`_kora_ops_${collectionName}`);
95
100
  for (const [nodeId, maxSeq] of watermark) {
96
101
  if (maxSeq <= 0) continue;
97
102
  const countSql = buildCountSql(table, ageCutoffSerialized !== null);
@@ -126,6 +131,7 @@ function buildDeleteSql(table, withAge) {
126
131
  import { createVersionVector as createVersionVector2 } from "@korajs/core";
127
132
  var LAST_ACKED_SERVER_VECTOR_META_KEY = "last_acked_server_vector";
128
133
  var DELTA_CURSOR_META_KEY = "delta_cursor";
134
+ var DELIVERY_WATERMARK_META_KEY = "delivery_watermark";
129
135
  function serializeVersionVectorToMeta(vector) {
130
136
  const record = {};
131
137
  for (const [nodeId, seq] of vector) {
@@ -198,6 +204,45 @@ async function saveDeltaCursor(adapter, cursor) {
198
204
  cursor
199
205
  ]);
200
206
  }
207
+ function deliveryWatermarkKey(signature) {
208
+ return signature === "" ? DELIVERY_WATERMARK_META_KEY : `${DELIVERY_WATERMARK_META_KEY}:${signature}`;
209
+ }
210
+ async function loadDeliveryWatermark(adapter, signature) {
211
+ const rows = await adapter.query("SELECT value FROM _kora_meta WHERE key = ?", [
212
+ deliveryWatermarkKey(signature)
213
+ ]);
214
+ const value = rows[0]?.value;
215
+ if (value === void 0 || value === null) {
216
+ return 0;
217
+ }
218
+ const parsed = Number(value);
219
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
220
+ }
221
+ async function saveDeliveryWatermark(adapter, signature, watermark) {
222
+ await adapter.execute("INSERT OR REPLACE INTO _kora_meta (key, value) VALUES (?, ?)", [
223
+ deliveryWatermarkKey(signature),
224
+ String(watermark)
225
+ ]);
226
+ }
227
+ async function deleteDeliveryWatermark(adapter, signature) {
228
+ await adapter.execute("DELETE FROM _kora_meta WHERE key = ?", [deliveryWatermarkKey(signature)]);
229
+ }
230
+ async function loadAllDeliveryWatermarks(adapter) {
231
+ const rows = await adapter.query(
232
+ "SELECT key, value FROM _kora_meta WHERE key = ? OR key LIKE ?",
233
+ [DELIVERY_WATERMARK_META_KEY, `${DELIVERY_WATERMARK_META_KEY}:%`]
234
+ );
235
+ const result = {};
236
+ const prefix = `${DELIVERY_WATERMARK_META_KEY}:`;
237
+ for (const row of rows) {
238
+ const signature = row.key === DELIVERY_WATERMARK_META_KEY ? "" : row.key.slice(prefix.length);
239
+ const parsed = Number(row.value);
240
+ if (Number.isFinite(parsed) && parsed >= 0) {
241
+ result[signature] = parsed;
242
+ }
243
+ }
244
+ return result;
245
+ }
201
246
 
202
247
  // src/store/store.ts
203
248
  import {
@@ -205,12 +250,15 @@ import {
205
250
  HybridLogicalClock as HybridLogicalClock3,
206
251
  createVersionVector as createVersionVector3,
207
252
  generateUUIDv7 as generateUUIDv73,
208
- migrationStepsToSQL
253
+ migrationStepsToSQL,
254
+ quoteIdent as quoteIdent7,
255
+ replayOperationsForRecord
209
256
  } from "@korajs/core";
210
257
 
211
258
  // src/collection/collection.ts
259
+ import { quoteIdent as quoteIdent2 } from "@korajs/core";
212
260
  var Collection = class {
213
- constructor(name, definition, schema, adapter, clock, nodeId, allocateSequenceNumber, onMutation, relationEnforcer, mutationHandler, causalTracker) {
261
+ constructor(name, definition, schema, adapter, clock, nodeId, allocateSequenceNumber, onMutation, relationEnforcer, mutationHandler, causalTracker, secretKeyProvider) {
214
262
  this.name = name;
215
263
  this.definition = definition;
216
264
  this.schema = schema;
@@ -222,6 +270,7 @@ var Collection = class {
222
270
  this.relationEnforcer = relationEnforcer;
223
271
  this.mutationHandler = mutationHandler;
224
272
  this.causalTracker = causalTracker;
273
+ this.secretKeyProvider = secretKeyProvider;
225
274
  }
226
275
  name;
227
276
  definition;
@@ -234,6 +283,7 @@ var Collection = class {
234
283
  relationEnforcer;
235
284
  mutationHandler;
236
285
  causalTracker;
286
+ secretKeyProvider;
237
287
  mutationContext() {
238
288
  return {
239
289
  collection: this.name,
@@ -246,7 +296,8 @@ var Collection = class {
246
296
  onMutation: this.onMutation,
247
297
  relationEnforcer: this.relationEnforcer,
248
298
  causalTracker: this.causalTracker,
249
- inTransaction: false
299
+ inTransaction: false,
300
+ secretKeyProvider: this.secretKeyProvider
250
301
  };
251
302
  }
252
303
  async insert(data) {
@@ -257,7 +308,7 @@ var Collection = class {
257
308
  }
258
309
  async findById(id) {
259
310
  const rows = await this.adapter.query(
260
- `SELECT * FROM ${this.name} WHERE id = ? AND _deleted = 0`,
311
+ `SELECT * FROM ${quoteIdent2(this.name)} WHERE id = ? AND _deleted = 0`,
261
312
  [id]
262
313
  );
263
314
  const row = rows[0];
@@ -289,6 +340,9 @@ var Collection = class {
289
340
  }
290
341
  };
291
342
 
343
+ // src/query/query-builder.ts
344
+ import { quoteIdent as quoteIdent3 } from "@korajs/core";
345
+
292
346
  // src/query/pluralize.ts
293
347
  function isVowel(char) {
294
348
  if (!char) return false;
@@ -518,7 +572,7 @@ var QueryBuilder = class _QueryBuilder {
518
572
  const relatedDef = this.schema?.collections[relatedCollection];
519
573
  if (!relatedDef) return;
520
574
  const placeholders = uniqueFks.map(() => "?").join(", ");
521
- const sql = `SELECT * FROM ${relatedCollection} WHERE id IN (${placeholders}) AND _deleted = 0`;
575
+ const sql = `SELECT * FROM ${quoteIdent3(relatedCollection)} WHERE id IN (${placeholders}) AND _deleted = 0`;
522
576
  const rows = await this.adapter.query(sql, uniqueFks);
523
577
  const relatedRecords = rows.map((row) => deserializeRecord(row, relatedDef.fields));
524
578
  const lookup = /* @__PURE__ */ new Map();
@@ -541,7 +595,7 @@ var QueryBuilder = class _QueryBuilder {
541
595
  if (!relatedDef) return;
542
596
  const fkField = relation.field;
543
597
  const placeholders = primaryIds.map(() => "?").join(", ");
544
- const sql = `SELECT * FROM ${relatedCollection} WHERE ${fkField} IN (${placeholders}) AND _deleted = 0`;
598
+ const sql = `SELECT * FROM ${quoteIdent3(relatedCollection)} WHERE ${quoteIdent3(fkField)} IN (${placeholders}) AND _deleted = 0`;
545
599
  const rows = await this.adapter.query(sql, primaryIds);
546
600
  const relatedRecords = rows.map((row) => deserializeRecord(row, relatedDef.fields));
547
601
  const grouped = /* @__PURE__ */ new Map();
@@ -583,7 +637,7 @@ var QueryError2 = class extends Error {
583
637
  };
584
638
 
585
639
  // src/relations/relation-enforcer.ts
586
- import { KoraError, createOperation } from "@korajs/core";
640
+ import { KoraError, createOperation, quoteIdent as quoteIdent4 } from "@korajs/core";
587
641
 
588
642
  // src/relations/relation-lookup.ts
589
643
  function buildRelationLookup(schema) {
@@ -686,7 +740,7 @@ var RelationEnforcer = class {
686
740
  async enforceCascade(incoming, deletedRecordId, tx, causalDeps) {
687
741
  const { sourceCollection, foreignKeyField } = incoming;
688
742
  const referencingRows = await tx.query(
689
- `SELECT id FROM ${sourceCollection} WHERE ${foreignKeyField} = ? AND _deleted = 0`,
743
+ `SELECT id FROM ${quoteIdent4(sourceCollection)} WHERE ${quoteIdent4(foreignKeyField)} = ? AND _deleted = 0`,
690
744
  [deletedRecordId]
691
745
  );
692
746
  if (referencingRows.length === 0) {
@@ -733,7 +787,7 @@ var RelationEnforcer = class {
733
787
  return [];
734
788
  }
735
789
  const referencingRows = await tx.query(
736
- `SELECT id FROM ${sourceCollection} WHERE ${foreignKeyField} = ? AND _deleted = 0`,
790
+ `SELECT id FROM ${quoteIdent4(sourceCollection)} WHERE ${quoteIdent4(foreignKeyField)} = ? AND _deleted = 0`,
737
791
  [deletedRecordId]
738
792
  );
739
793
  if (referencingRows.length === 0) {
@@ -781,7 +835,7 @@ var RelationEnforcer = class {
781
835
  async enforceRestrict(incoming, deletedRecordId, tx) {
782
836
  const { sourceCollection, foreignKeyField, relationName } = incoming;
783
837
  const countRows = await tx.query(
784
- `SELECT COUNT(*) as cnt FROM ${sourceCollection} WHERE ${foreignKeyField} = ? AND _deleted = 0`,
838
+ `SELECT COUNT(*) as cnt FROM ${quoteIdent4(sourceCollection)} WHERE ${quoteIdent4(foreignKeyField)} = ? AND _deleted = 0`,
785
839
  [deletedRecordId]
786
840
  );
787
841
  const count = countRows[0]?.cnt ?? 0;
@@ -1469,7 +1523,7 @@ var SubscriptionManager = class {
1469
1523
  };
1470
1524
 
1471
1525
  // src/sync/rebase-unsynced-operations.ts
1472
- import { HybridLogicalClock as HybridLogicalClock2, MAX_LOGICAL } from "@korajs/core";
1526
+ import { HybridLogicalClock as HybridLogicalClock2, MAX_LOGICAL, quoteIdent as quoteIdent5 } from "@korajs/core";
1473
1527
  import { computeOperationId } from "@korajs/core/internal";
1474
1528
  var LOGICAL_COUNTER_LIMIT = MAX_LOGICAL + 1;
1475
1529
  function emptyResult() {
@@ -1483,7 +1537,9 @@ async function rebaseUnsyncedOperationsInLog(adapter, schema, unsyncedOpIds, cor
1483
1537
  const rebased = [];
1484
1538
  let maxOtherSerializedTs = null;
1485
1539
  for (const collectionName of Object.keys(schema.collections)) {
1486
- const rows = await adapter.query(`SELECT * FROM _kora_ops_${collectionName}`);
1540
+ const rows = await adapter.query(
1541
+ `SELECT * FROM ${quoteIdent5(`_kora_ops_${collectionName}`)}`
1542
+ );
1487
1543
  for (const row of rows) {
1488
1544
  if (rebaseIds.has(row.id)) {
1489
1545
  rebased.push(deserializeOperationWithCollection(row, collectionName));
@@ -1552,7 +1608,7 @@ async function rebaseUnsyncedOperationsInLog(adapter, schema, unsyncedOpIds, cor
1552
1608
  continue;
1553
1609
  }
1554
1610
  const opsTable = `_kora_ops_${oldOp.collection}`;
1555
- await tx.execute(`DELETE FROM ${opsTable} WHERE id = ?`, [oldOp.id]);
1611
+ await tx.execute(`DELETE FROM ${quoteIdent5(opsTable)} WHERE id = ?`, [oldOp.id]);
1556
1612
  const opInsert = buildInsertQuery(
1557
1613
  opsTable,
1558
1614
  serializeOperation(newOp)
@@ -1561,11 +1617,11 @@ async function rebaseUnsyncedOperationsInLog(adapter, schema, unsyncedOpIds, cor
1561
1617
  const oldVersion = serializeRowVersion(oldOp.timestamp);
1562
1618
  const newVersion = serializeRowVersion(newOp.timestamp);
1563
1619
  await tx.execute(
1564
- `UPDATE ${oldOp.collection} SET _version = ?, _updated_at = ? WHERE id = ? AND _version = ?`,
1620
+ `UPDATE ${quoteIdent5(oldOp.collection)} SET _version = ?, _updated_at = ? WHERE id = ? AND _version = ?`,
1565
1621
  [newVersion, newOp.timestamp.wallTime, oldOp.recordId, oldVersion]
1566
1622
  );
1567
1623
  const rows = await tx.query(
1568
- `SELECT _field_versions FROM ${oldOp.collection} WHERE id = ?`,
1624
+ `SELECT _field_versions FROM ${quoteIdent5(oldOp.collection)} WHERE id = ?`,
1569
1625
  [oldOp.recordId]
1570
1626
  );
1571
1627
  const fieldVersions = parseFieldVersions(rows[0]?._field_versions);
@@ -1577,14 +1633,14 @@ async function rebaseUnsyncedOperationsInLog(adapter, schema, unsyncedOpIds, cor
1577
1633
  }
1578
1634
  }
1579
1635
  if (changed) {
1580
- await tx.execute(`UPDATE ${oldOp.collection} SET _field_versions = ? WHERE id = ?`, [
1581
- serializeFieldVersions(fieldVersions),
1582
- oldOp.recordId
1583
- ]);
1636
+ await tx.execute(
1637
+ `UPDATE ${quoteIdent5(oldOp.collection)} SET _field_versions = ? WHERE id = ?`,
1638
+ [serializeFieldVersions(fieldVersions), oldOp.recordId]
1639
+ );
1584
1640
  }
1585
1641
  if (oldOp.type === "insert") {
1586
1642
  await tx.execute(
1587
- `UPDATE ${oldOp.collection} SET _created_at = ? WHERE id = ? AND _created_at = ?`,
1643
+ `UPDATE ${quoteIdent5(oldOp.collection)} SET _created_at = ? WHERE id = ? AND _created_at = ?`,
1588
1644
  [newOp.timestamp.wallTime, oldOp.recordId, oldOp.timestamp.wallTime]
1589
1645
  );
1590
1646
  }
@@ -1604,6 +1660,7 @@ import {
1604
1660
  createOperation as createOperation2,
1605
1661
  generateUUIDv7,
1606
1662
  isAtomicOp,
1663
+ quoteIdent as quoteIdent6,
1607
1664
  resolveAtomicOp,
1608
1665
  toAtomicOp,
1609
1666
  validateRecord
@@ -1920,7 +1977,7 @@ var TransactionContext = class {
1920
1977
  }
1921
1978
  }
1922
1979
  const rows = await this.config.adapter.query(
1923
- `SELECT * FROM ${collectionName} WHERE id = ? AND _deleted = 0`,
1980
+ `SELECT * FROM ${quoteIdent6(collectionName)} WHERE id = ? AND _deleted = 0`,
1924
1981
  [id]
1925
1982
  );
1926
1983
  let record = rows[0] ? deserializeRecord(rows[0], definition.fields) : null;
@@ -2020,6 +2077,7 @@ var Store = class {
2020
2077
  localMutationHandler;
2021
2078
  relationEnforcer = null;
2022
2079
  causalTracker = null;
2080
+ secretKeyProvider;
2023
2081
  constructor(config) {
2024
2082
  this.schema = config.schema;
2025
2083
  this.adapter = config.adapter;
@@ -2028,6 +2086,7 @@ var Store = class {
2028
2086
  this.isolation = config.isolation ?? "shared";
2029
2087
  this.emitter = config.emitter ?? null;
2030
2088
  this.localMutationHandler = config.localMutationHandler ?? null;
2089
+ this.secretKeyProvider = config.secretKeyProvider;
2031
2090
  this.subscriptionManager = new SubscriptionManager({
2032
2091
  onQuerySubscribed: config.onQuerySubscribed
2033
2092
  });
@@ -2070,7 +2129,8 @@ var Store = class {
2070
2129
  },
2071
2130
  this.relationEnforcer,
2072
2131
  this.localMutationHandler,
2073
- this.causalTracker
2132
+ this.causalTracker,
2133
+ this.secretKeyProvider
2074
2134
  );
2075
2135
  this.collections.set(name, col);
2076
2136
  }
@@ -2152,7 +2212,7 @@ var Store = class {
2152
2212
  return "skipped";
2153
2213
  }
2154
2214
  const existing = await this.adapter.query(
2155
- `SELECT id FROM _kora_ops_${collection} WHERE id = ?`,
2215
+ `SELECT id FROM ${quoteIdent7(`_kora_ops_${collection}`)} WHERE id = ?`,
2156
2216
  [op.id]
2157
2217
  );
2158
2218
  if (existing.length > 0) {
@@ -2177,7 +2237,8 @@ var Store = class {
2177
2237
  throw new OptimisticLockError(collection, op.recordId);
2178
2238
  }
2179
2239
  };
2180
- if (op.type === "insert" && materializeSource) {
2240
+ if (options?.logOnly) {
2241
+ } else if (op.type === "insert" && materializeSource) {
2181
2242
  const serializedData = serializeRecord(materializeSource, definition.fields);
2182
2243
  const existing2 = await tx.query(
2183
2244
  `SELECT _updated_at, _version, _field_versions, _deleted FROM ${collection} WHERE id = ?`,
@@ -2199,7 +2260,7 @@ var Store = class {
2199
2260
  };
2200
2261
  const insertQuery = buildInsertQuery(collection, record);
2201
2262
  await tx.execute(insertQuery.sql, insertQuery.params);
2202
- await this.foldOrphanedOperations(tx, collection, definition, op.recordId);
2263
+ await this.foldOrphanedOperations(tx, collection, definition, op.recordId, op);
2203
2264
  } else if (row._deleted === 1) {
2204
2265
  if (isIncomingNewerThanRow(materializeTimestamp, row)) {
2205
2266
  const fieldChanges = {
@@ -2257,7 +2318,7 @@ var Store = class {
2257
2318
  const serializedChanges = serializeRecord(materializeSource, definition.fields);
2258
2319
  const changedFields = Object.keys(serializedChanges);
2259
2320
  const currentRows = await tx.query(
2260
- `SELECT _version, _field_versions FROM ${collection} WHERE id = ?`,
2321
+ `SELECT _version, _field_versions FROM ${quoteIdent7(collection)} WHERE id = ?`,
2261
2322
  [op.recordId]
2262
2323
  );
2263
2324
  const currentRow = currentRows[0];
@@ -2351,9 +2412,9 @@ var Store = class {
2351
2412
  * necessarily an orphan (rows are soft-deleted, never removed, so a row
2352
2413
  * that is absent now was never materialized).
2353
2414
  */
2354
- async foldOrphanedOperations(tx, collection, definition, recordId) {
2415
+ async foldOrphanedOperations(tx, collection, definition, recordId, insertOp) {
2355
2416
  const orphanRows = await tx.query(
2356
- `SELECT * FROM _kora_ops_${collection} WHERE record_id = ? AND type IN ('update', 'delete')`,
2417
+ `SELECT * FROM ${quoteIdent7(`_kora_ops_${collection}`)} WHERE record_id = ? AND type IN ('update', 'delete')`,
2357
2418
  [recordId]
2358
2419
  );
2359
2420
  if (orphanRows.length === 0) {
@@ -2366,7 +2427,7 @@ var Store = class {
2366
2427
  if (orphan.type === "update" && orphan.data) {
2367
2428
  const orphanChanges = serializeRecord(orphan.data, definition.fields);
2368
2429
  const rows = await tx.query(
2369
- `SELECT _version, _field_versions FROM ${collection} WHERE id = ?`,
2430
+ `SELECT _version, _field_versions FROM ${quoteIdent7(collection)} WHERE id = ?`,
2370
2431
  [recordId]
2371
2432
  );
2372
2433
  const current = rows[0];
@@ -2400,6 +2461,37 @@ var Store = class {
2400
2461
  await tx.execute(query.sql, query.params);
2401
2462
  }
2402
2463
  }
2464
+ const atomicFields = /* @__PURE__ */ new Set();
2465
+ for (const orphan of orphans) {
2466
+ if (orphan.type === "update" && orphan.atomicOps) {
2467
+ for (const field of Object.keys(orphan.atomicOps)) {
2468
+ atomicFields.add(field);
2469
+ }
2470
+ }
2471
+ }
2472
+ if (atomicFields.size > 0) {
2473
+ const ordered = [insertOp, ...orphans].sort(
2474
+ (a, b) => HybridLogicalClock3.compare(a.timestamp, b.timestamp)
2475
+ );
2476
+ const folded = replayOperationsForRecord(ordered);
2477
+ if (folded) {
2478
+ const composed = {};
2479
+ for (const field of atomicFields) {
2480
+ if (field in folded) {
2481
+ composed[field] = folded[field];
2482
+ }
2483
+ }
2484
+ const serialized = serializeRecord(composed, definition.fields);
2485
+ const cols = Object.keys(serialized);
2486
+ if (cols.length > 0) {
2487
+ const setClause = cols.map((c) => `${quoteIdent7(c)} = ?`).join(", ");
2488
+ await tx.execute(`UPDATE ${quoteIdent7(collection)} SET ${setClause} WHERE id = ?`, [
2489
+ ...cols.map((c) => serialized[c]),
2490
+ recordId
2491
+ ]);
2492
+ }
2493
+ }
2494
+ }
2403
2495
  }
2404
2496
  /**
2405
2497
  * Get operations from a node within a sequence number range.
@@ -2416,7 +2508,7 @@ var Store = class {
2416
2508
  const allOps = [];
2417
2509
  for (const collectionName of Object.keys(this.schema.collections)) {
2418
2510
  const rows = await this.adapter.query(
2419
- `SELECT * FROM _kora_ops_${collectionName} WHERE node_id = ? AND sequence_number >= ? AND sequence_number <= ? ORDER BY sequence_number ASC`,
2511
+ `SELECT * FROM ${quoteIdent7(`_kora_ops_${collectionName}`)} WHERE node_id = ? AND sequence_number >= ? AND sequence_number <= ? ORDER BY sequence_number ASC`,
2420
2512
  [nodeId, fromSeq, toSeq]
2421
2513
  );
2422
2514
  for (const row of rows) {
@@ -2435,7 +2527,7 @@ var Store = class {
2435
2527
  const allOps = [];
2436
2528
  for (const collectionName of Object.keys(this.schema.collections)) {
2437
2529
  const rows = await this.adapter.query(
2438
- `SELECT * FROM _kora_ops_${collectionName} ORDER BY sequence_number ASC`
2530
+ `SELECT * FROM ${quoteIdent7(`_kora_ops_${collectionName}`)} ORDER BY sequence_number ASC`
2439
2531
  );
2440
2532
  for (const row of rows) {
2441
2533
  allOps.push(deserializeOperationWithCollection(row, collectionName));
@@ -2486,7 +2578,7 @@ var Store = class {
2486
2578
  */
2487
2579
  async exportAudit(options) {
2488
2580
  this.ensureOpen();
2489
- const { exportAudit: doExport } = await import("./export-audit-VPGQWNR6.js");
2581
+ const { exportAudit: doExport } = await import("./export-audit-ODGQTQIK.js");
2490
2582
  return doExport(this.adapter, this.schema, this.nodeId, this.schema.version, options);
2491
2583
  }
2492
2584
  /**
@@ -2504,6 +2596,22 @@ var Store = class {
2504
2596
  col.setMutationHandler(handler);
2505
2597
  }
2506
2598
  }
2599
+ /**
2600
+ * Notify this store that another same-origin runtime committed an operation to
2601
+ * the shared local database. The operation has already been persisted and
2602
+ * materialized by the database owner; this method only advances in-memory causal
2603
+ * watermarks and invalidates local reactive queries so already-open tabs stay
2604
+ * live without reapplying or rewriting the operation.
2605
+ */
2606
+ notifyExternalOperation(operation) {
2607
+ this.ensureOpen();
2608
+ const definition = this.schema.collections[operation.collection];
2609
+ if (!definition) {
2610
+ return;
2611
+ }
2612
+ this.recordOperationSequence(operation);
2613
+ this.subscriptionManager.notify(operation.collection, operation);
2614
+ }
2507
2615
  /**
2508
2616
  * Build mutation context for a collection (used by ApplyPipeline side effects).
2509
2617
  */
@@ -2531,7 +2639,8 @@ var Store = class {
2531
2639
  relationEnforcer: this.relationEnforcer,
2532
2640
  causalTracker: this.causalTracker,
2533
2641
  inTransaction: options?.inTransaction ?? false,
2534
- extraCausalDeps: options?.extraCausalDeps
2642
+ extraCausalDeps: options?.extraCausalDeps,
2643
+ secretKeyProvider: this.secretKeyProvider
2535
2644
  };
2536
2645
  }
2537
2646
  /**
@@ -2544,7 +2653,7 @@ var Store = class {
2544
2653
  return null;
2545
2654
  }
2546
2655
  const rows = await this.adapter.query(
2547
- `SELECT * FROM ${collection} WHERE id = ?`,
2656
+ `SELECT * FROM ${quoteIdent7(collection)} WHERE id = ?`,
2548
2657
  [recordId]
2549
2658
  );
2550
2659
  const row = rows[0];
@@ -2569,7 +2678,7 @@ var Store = class {
2569
2678
  return null;
2570
2679
  }
2571
2680
  const rows = await this.adapter.query(
2572
- `SELECT _version, _field_versions FROM ${collection} WHERE id = ?`,
2681
+ `SELECT _version, _field_versions FROM ${quoteIdent7(collection)} WHERE id = ?`,
2573
2682
  [recordId]
2574
2683
  );
2575
2684
  const row = rows[0];
@@ -2612,6 +2721,35 @@ var Store = class {
2612
2721
  this.ensureOpen();
2613
2722
  await saveDeltaCursor(this.adapter, cursor);
2614
2723
  }
2724
+ /**
2725
+ * Load the persisted delivery watermark for a view signature (0 when none recorded).
2726
+ * Defaults to the unfiltered view so existing single-watermark reads keep working.
2727
+ */
2728
+ async loadDeliveryWatermark(signature = "") {
2729
+ this.ensureOpen();
2730
+ return loadDeliveryWatermark(this.adapter, signature);
2731
+ }
2732
+ /**
2733
+ * Persist the delivery watermark for a view signature so it survives a client restart.
2734
+ */
2735
+ async saveDeliveryWatermark(signature, watermark) {
2736
+ this.ensureOpen();
2737
+ await saveDeliveryWatermark(this.adapter, signature, watermark);
2738
+ }
2739
+ /**
2740
+ * Load every persisted view watermark, keyed by signature.
2741
+ */
2742
+ async loadAllDeliveryWatermarks() {
2743
+ this.ensureOpen();
2744
+ return loadAllDeliveryWatermarks(this.adapter);
2745
+ }
2746
+ /**
2747
+ * Delete a persisted view watermark (safe: the view back-fills from 0 when next visited).
2748
+ */
2749
+ async deleteDeliveryWatermark(signature) {
2750
+ this.ensureOpen();
2751
+ await deleteDeliveryWatermark(this.adapter, signature);
2752
+ }
2615
2753
  /**
2616
2754
  * Local operations not yet reflected on the server version vector.
2617
2755
  */
@@ -2675,7 +2813,7 @@ var Store = class {
2675
2813
  async getLatestLocalOperationForRecord(collection, recordId) {
2676
2814
  this.ensureOpen();
2677
2815
  const rows = await this.adapter.query(
2678
- `SELECT * FROM _kora_ops_${collection} WHERE node_id = ? AND record_id = ? ORDER BY sequence_number DESC LIMIT 1`,
2816
+ `SELECT * FROM ${quoteIdent7(`_kora_ops_${collection}`)} WHERE node_id = ? AND record_id = ? ORDER BY sequence_number DESC LIMIT 1`,
2679
2817
  [this.nodeId, recordId]
2680
2818
  );
2681
2819
  const row = rows[0];
@@ -2690,7 +2828,7 @@ var Store = class {
2690
2828
  async getLatestOperationForRecord(collection, recordId) {
2691
2829
  this.ensureOpen();
2692
2830
  const rows = await this.adapter.query(
2693
- `SELECT * FROM _kora_ops_${collection} WHERE record_id = ?`,
2831
+ `SELECT * FROM ${quoteIdent7(`_kora_ops_${collection}`)} WHERE record_id = ?`,
2694
2832
  [recordId]
2695
2833
  );
2696
2834
  let latest = null;
@@ -2702,6 +2840,21 @@ var Store = class {
2702
2840
  }
2703
2841
  return latest;
2704
2842
  }
2843
+ /**
2844
+ * All operations for a record, sorted in HLC total order (wallTime, logical,
2845
+ * nodeId). Used by the apply pipeline to materialize atomic-op fields by folding
2846
+ * the record's log, matching the server's materialization exactly.
2847
+ */
2848
+ async getOperationsForRecord(collection, recordId) {
2849
+ this.ensureOpen();
2850
+ const rows = await this.adapter.query(
2851
+ `SELECT * FROM ${quoteIdent7(`_kora_ops_${collection}`)} WHERE record_id = ?`,
2852
+ [recordId]
2853
+ );
2854
+ const ops = rows.map((row) => deserializeOperationWithCollection(row, collection));
2855
+ ops.sort((a, b) => HybridLogicalClock3.compare(a.timestamp, b.timestamp));
2856
+ return ops;
2857
+ }
2705
2858
  /** Expose the subscription manager for direct access (e.g., by QueryBuilder) */
2706
2859
  getSubscriptionManager() {
2707
2860
  return this.subscriptionManager;
@@ -2780,7 +2933,7 @@ var Store = class {
2780
2933
  */
2781
2934
  async exportBackup(options) {
2782
2935
  this.ensureOpen();
2783
- const { exportBackup: doExport } = await import("./backup-5XZ7THNJ.js");
2936
+ const { exportBackup: doExport } = await import("./backup-5S7K2Z35.js");
2784
2937
  return doExport(this.adapter, this.schema, this.nodeId, this.schema.version, options);
2785
2938
  }
2786
2939
  /**
@@ -2792,7 +2945,7 @@ var Store = class {
2792
2945
  */
2793
2946
  async importBackup(data, options) {
2794
2947
  this.ensureOpen();
2795
- const { restoreBackup: doRestore } = await import("./backup-5XZ7THNJ.js");
2948
+ const { restoreBackup: doRestore } = await import("./backup-5S7K2Z35.js");
2796
2949
  return doRestore(this.adapter, this.schema, data, options);
2797
2950
  }
2798
2951
  /**
@@ -2877,14 +3030,14 @@ var Store = class {
2877
3030
  */
2878
3031
  async runBackfill(collection, transform) {
2879
3032
  const rows = await this.adapter.query(
2880
- `SELECT * FROM ${collection} WHERE _deleted = 0`
3033
+ `SELECT * FROM ${quoteIdent7(collection)} WHERE _deleted = 0`
2881
3034
  );
2882
3035
  await this.adapter.transaction(async (tx) => {
2883
3036
  for (const row of rows) {
2884
3037
  const updates = transform(row);
2885
3038
  const fields = Object.keys(updates);
2886
3039
  if (fields.length === 0) continue;
2887
- const setClauses = fields.map((f) => `${f} = ?`).join(", ");
3040
+ const setClauses = fields.map((f) => `${quoteIdent7(f)} = ?`).join(", ");
2888
3041
  const values = fields.map((f) => {
2889
3042
  const val = updates[f];
2890
3043
  if (typeof val === "boolean") return val ? 1 : 0;
@@ -2894,7 +3047,7 @@ var Store = class {
2894
3047
  return val;
2895
3048
  });
2896
3049
  values.push(row.id);
2897
- await tx.execute(`UPDATE ${collection} SET ${setClauses} WHERE id = ?`, values);
3050
+ await tx.execute(`UPDATE ${quoteIdent7(collection)} SET ${setClauses} WHERE id = ?`, values);
2898
3051
  }
2899
3052
  });
2900
3053
  }
@@ -2954,6 +3107,7 @@ var QueryStore = class {
2954
3107
  listeners = /* @__PURE__ */ new Set();
2955
3108
  unsubscribeQuery = null;
2956
3109
  active = false;
3110
+ hasEmittedSnapshot = false;
2957
3111
  queryBuilder;
2958
3112
  constructor(queryBuilder) {
2959
3113
  this.queryBuilder = queryBuilder;
@@ -2979,16 +3133,25 @@ var QueryStore = class {
2979
3133
  getSnapshot = () => {
2980
3134
  return this.snapshot;
2981
3135
  };
3136
+ /**
3137
+ * Whether the underlying query subscription has delivered at least one
3138
+ * authoritative result for the current subscription lifecycle.
3139
+ */
3140
+ hasSnapshot = () => {
3141
+ return this.hasEmittedSnapshot;
3142
+ };
2982
3143
  /** Tear down listeners and the underlying query subscription. */
2983
3144
  destroy() {
2984
3145
  this.stopSubscription();
2985
3146
  this.listeners.clear();
2986
3147
  this.snapshot = EMPTY_ARRAY;
3148
+ this.hasEmittedSnapshot = false;
2987
3149
  }
2988
3150
  startSubscription() {
2989
3151
  this.active = true;
2990
3152
  this.unsubscribeQuery = this.queryBuilder.subscribe((results) => {
2991
3153
  if (!this.active) return;
3154
+ this.hasEmittedSnapshot = true;
2992
3155
  this.snapshot = Object.freeze([...results]);
2993
3156
  this.notifyListeners();
2994
3157
  });
@@ -3422,12 +3585,519 @@ function asRichTextSyncEngine(engine) {
3422
3585
  }
3423
3586
  return engine;
3424
3587
  }
3588
+
3589
+ // src/blob/opfs-blob-store.ts
3590
+ import { createBlobRef, hashBlob } from "@korajs/core";
3591
+ var OpfsBlobStore = class {
3592
+ constructor(dir) {
3593
+ this.dir = dir;
3594
+ }
3595
+ dir;
3596
+ shardFor(hash) {
3597
+ return hash.slice(0, 2);
3598
+ }
3599
+ async put(bytes, metadata = {}) {
3600
+ const ref = await createBlobRef(bytes, metadata);
3601
+ const shard = this.shardFor(ref.hash);
3602
+ if (await this.dir.has(shard, ref.hash)) {
3603
+ return ref;
3604
+ }
3605
+ const out = new Uint8Array(bytes.byteLength);
3606
+ out.set(bytes);
3607
+ await this.dir.write(shard, ref.hash, out);
3608
+ return ref;
3609
+ }
3610
+ async get(hash) {
3611
+ const bytes = await this.dir.read(this.shardFor(hash), hash);
3612
+ if (bytes === null) {
3613
+ return null;
3614
+ }
3615
+ const actual = await hashBlob(bytes);
3616
+ if (actual !== hash) {
3617
+ throw new BlobIntegrityError(hash, actual);
3618
+ }
3619
+ return bytes;
3620
+ }
3621
+ async has(hash) {
3622
+ return this.dir.has(this.shardFor(hash), hash);
3623
+ }
3624
+ async delete(hash) {
3625
+ return this.dir.remove(this.shardFor(hash), hash);
3626
+ }
3627
+ async size() {
3628
+ return this.dir.count();
3629
+ }
3630
+ async list() {
3631
+ return this.dir.keys();
3632
+ }
3633
+ };
3634
+ function getStorageManager() {
3635
+ const nav = globalThis.navigator;
3636
+ if (!nav?.storage || typeof nav.storage.getDirectory !== "function") {
3637
+ throw new Error(
3638
+ "OPFS is unavailable in this environment. The OPFS blob store requires a browser with Origin Private File System support; use MemoryBlobStore or a server-side store elsewhere."
3639
+ );
3640
+ }
3641
+ return nav.storage;
3642
+ }
3643
+ function isNotFoundError(error) {
3644
+ return typeof error === "object" && error !== null && error.name === "NotFoundError";
3645
+ }
3646
+ async function createOpfsBlobDirectory(rootDirName = "kora-blobs") {
3647
+ const storage = getStorageManager();
3648
+ if (typeof storage.persist === "function") {
3649
+ try {
3650
+ await storage.persist();
3651
+ } catch {
3652
+ }
3653
+ }
3654
+ const opfsRoot = await storage.getDirectory();
3655
+ const root = await opfsRoot.getDirectoryHandle(rootDirName, { create: true });
3656
+ async function shardHandle(shard, create) {
3657
+ try {
3658
+ return await root.getDirectoryHandle(shard, { create });
3659
+ } catch (error) {
3660
+ if (isNotFoundError(error)) {
3661
+ return null;
3662
+ }
3663
+ throw error;
3664
+ }
3665
+ }
3666
+ return {
3667
+ async read(shard, name) {
3668
+ const dir = await shardHandle(shard, false);
3669
+ if (!dir) {
3670
+ return null;
3671
+ }
3672
+ let handle;
3673
+ try {
3674
+ handle = await dir.getFileHandle(name, { create: false });
3675
+ } catch (error) {
3676
+ if (isNotFoundError(error)) {
3677
+ return null;
3678
+ }
3679
+ throw error;
3680
+ }
3681
+ const file = await handle.getFile();
3682
+ return new Uint8Array(await file.arrayBuffer());
3683
+ },
3684
+ async write(shard, name, bytes) {
3685
+ const dir = await root.getDirectoryHandle(shard, { create: true });
3686
+ const handle = await dir.getFileHandle(name, { create: true });
3687
+ const writable = await handle.createWritable({ keepExistingData: false });
3688
+ try {
3689
+ await writable.write(bytes);
3690
+ } finally {
3691
+ await writable.close();
3692
+ }
3693
+ },
3694
+ async remove(shard, name) {
3695
+ const dir = await shardHandle(shard, false);
3696
+ if (!dir) {
3697
+ return false;
3698
+ }
3699
+ try {
3700
+ await dir.removeEntry(name);
3701
+ return true;
3702
+ } catch (error) {
3703
+ if (isNotFoundError(error)) {
3704
+ return false;
3705
+ }
3706
+ throw error;
3707
+ }
3708
+ },
3709
+ async has(shard, name) {
3710
+ const dir = await shardHandle(shard, false);
3711
+ if (!dir) {
3712
+ return false;
3713
+ }
3714
+ try {
3715
+ await dir.getFileHandle(name, { create: false });
3716
+ return true;
3717
+ } catch (error) {
3718
+ if (isNotFoundError(error)) {
3719
+ return false;
3720
+ }
3721
+ throw error;
3722
+ }
3723
+ },
3724
+ async count() {
3725
+ let total = 0;
3726
+ for await (const shard of root.keys()) {
3727
+ const dir = await shardHandle(shard, false);
3728
+ if (!dir) {
3729
+ continue;
3730
+ }
3731
+ for await (const name of dir.keys()) {
3732
+ if (!name.endsWith(".tmp")) {
3733
+ total++;
3734
+ }
3735
+ }
3736
+ }
3737
+ return total;
3738
+ },
3739
+ async keys() {
3740
+ const names = [];
3741
+ for await (const shard of root.keys()) {
3742
+ const dir = await shardHandle(shard, false);
3743
+ if (!dir) {
3744
+ continue;
3745
+ }
3746
+ for await (const name of dir.keys()) {
3747
+ if (!name.endsWith(".tmp")) {
3748
+ names.push(name);
3749
+ }
3750
+ }
3751
+ }
3752
+ return names;
3753
+ }
3754
+ };
3755
+ }
3756
+ async function createOpfsBlobStore(rootDirName = "kora-blobs") {
3757
+ return new OpfsBlobStore(await createOpfsBlobDirectory(rootDirName));
3758
+ }
3759
+
3760
+ // src/blob/blob-chunking.ts
3761
+ import { hashBlob as hashBlob2 } from "@korajs/core";
3762
+ var DEFAULT_CHUNK_SIZE = 256 * 1024;
3763
+ async function chunkBlob(bytes, chunkSize = DEFAULT_CHUNK_SIZE, metadata = {}) {
3764
+ if (chunkSize <= 0) {
3765
+ throw new Error(`chunkBlob requires a positive chunkSize, got ${chunkSize}`);
3766
+ }
3767
+ const chunkHashes = [];
3768
+ const chunks = /* @__PURE__ */ new Map();
3769
+ for (let offset = 0; offset < bytes.byteLength; offset += chunkSize) {
3770
+ const end = Math.min(offset + chunkSize, bytes.byteLength);
3771
+ const chunk = new Uint8Array(end - offset);
3772
+ chunk.set(bytes.subarray(offset, end));
3773
+ const hash = await hashBlob2(chunk);
3774
+ chunkHashes.push(hash);
3775
+ if (!chunks.has(hash)) {
3776
+ chunks.set(hash, chunk);
3777
+ }
3778
+ }
3779
+ const blobHash = await hashBlob2(bytes);
3780
+ const manifest = {
3781
+ blobHash,
3782
+ size: bytes.byteLength,
3783
+ chunkSize,
3784
+ chunkHashes
3785
+ };
3786
+ if (metadata.mimeType !== void 0) {
3787
+ manifest.mimeType = metadata.mimeType;
3788
+ }
3789
+ if (metadata.filename !== void 0) {
3790
+ manifest.filename = metadata.filename;
3791
+ }
3792
+ return { manifest, chunks };
3793
+ }
3794
+ async function reassembleBlob(manifest, chunkStore) {
3795
+ const parts = [];
3796
+ let total = 0;
3797
+ for (const hash of manifest.chunkHashes) {
3798
+ const chunk = await chunkStore.get(hash);
3799
+ if (chunk === null) {
3800
+ throw new Error(`Cannot reassemble blob ${manifest.blobHash}: missing chunk ${hash}`);
3801
+ }
3802
+ parts.push(chunk);
3803
+ total += chunk.byteLength;
3804
+ }
3805
+ const out = new Uint8Array(total);
3806
+ let offset = 0;
3807
+ for (const part of parts) {
3808
+ out.set(part, offset);
3809
+ offset += part.byteLength;
3810
+ }
3811
+ const actual = await hashBlob2(out);
3812
+ if (actual !== manifest.blobHash) {
3813
+ throw new BlobIntegrityError(manifest.blobHash, actual);
3814
+ }
3815
+ return out;
3816
+ }
3817
+
3818
+ // src/blob/blob-transfer.ts
3819
+ import { hashBlob as hashBlob3 } from "@korajs/core";
3820
+ async function receiveBlob(manifest, provider, stores) {
3821
+ let chunksFetched = 0;
3822
+ let chunksSkipped = 0;
3823
+ const seen = /* @__PURE__ */ new Set();
3824
+ for (const hash of manifest.chunkHashes) {
3825
+ if (seen.has(hash)) {
3826
+ continue;
3827
+ }
3828
+ seen.add(hash);
3829
+ if (await stores.chunkStore.has(hash)) {
3830
+ chunksSkipped++;
3831
+ continue;
3832
+ }
3833
+ const bytes = await provider.getChunk(hash);
3834
+ if (bytes === null) {
3835
+ throw new Error(`Blob transfer failed: provider could not supply chunk ${hash}`);
3836
+ }
3837
+ const actual = await hashBlob3(bytes);
3838
+ if (actual !== hash) {
3839
+ throw new BlobIntegrityError(hash, actual);
3840
+ }
3841
+ await stores.chunkStore.put(bytes);
3842
+ chunksFetched++;
3843
+ }
3844
+ const full = await reassembleBlob(manifest, stores.chunkStore);
3845
+ const ref = await stores.blobStore.put(full, {
3846
+ mimeType: manifest.mimeType,
3847
+ filename: manifest.filename
3848
+ });
3849
+ return { ref, chunksFetched, chunksSkipped };
3850
+ }
3851
+ async function prepareBlobForSend(bytes, chunkStore, options = {}) {
3852
+ const { manifest, chunks } = await chunkBlob(bytes, options.chunkSize, {
3853
+ mimeType: options.mimeType,
3854
+ filename: options.filename
3855
+ });
3856
+ for (const chunk of chunks.values()) {
3857
+ await chunkStore.put(chunk);
3858
+ }
3859
+ const provider = {
3860
+ getChunk: (hash) => chunkStore.get(hash)
3861
+ };
3862
+ return { manifest, provider };
3863
+ }
3864
+
3865
+ // src/blob/blob-manifest-transfer.ts
3866
+ import { hashBlob as hashBlob4 } from "@korajs/core";
3867
+ function serializeBlobManifest(manifest) {
3868
+ const canonical = {
3869
+ blobHash: manifest.blobHash,
3870
+ size: manifest.size,
3871
+ chunkSize: manifest.chunkSize,
3872
+ chunkHashes: manifest.chunkHashes
3873
+ };
3874
+ if (manifest.mimeType !== void 0) {
3875
+ canonical.mimeType = manifest.mimeType;
3876
+ }
3877
+ if (manifest.filename !== void 0) {
3878
+ canonical.filename = manifest.filename;
3879
+ }
3880
+ return new TextEncoder().encode(JSON.stringify(canonical));
3881
+ }
3882
+ function parseBlobManifest(bytes) {
3883
+ const parsed = JSON.parse(new TextDecoder().decode(bytes));
3884
+ if (typeof parsed !== "object" || parsed === null) {
3885
+ throw new Error("Invalid blob manifest: not an object");
3886
+ }
3887
+ const obj = parsed;
3888
+ if (typeof obj.blobHash !== "string") {
3889
+ throw new Error("Invalid blob manifest: missing blobHash");
3890
+ }
3891
+ if (typeof obj.size !== "number" || typeof obj.chunkSize !== "number") {
3892
+ throw new Error("Invalid blob manifest: missing size or chunkSize");
3893
+ }
3894
+ if (!Array.isArray(obj.chunkHashes) || obj.chunkHashes.some((h) => typeof h !== "string")) {
3895
+ throw new Error("Invalid blob manifest: chunkHashes must be an array of strings");
3896
+ }
3897
+ const manifest = {
3898
+ blobHash: obj.blobHash,
3899
+ size: obj.size,
3900
+ chunkSize: obj.chunkSize,
3901
+ chunkHashes: obj.chunkHashes
3902
+ };
3903
+ if (typeof obj.mimeType === "string") {
3904
+ manifest.mimeType = obj.mimeType;
3905
+ }
3906
+ if (typeof obj.filename === "string") {
3907
+ manifest.filename = obj.filename;
3908
+ }
3909
+ return manifest;
3910
+ }
3911
+ async function putBlobForTransfer(store, bytes, options = {}) {
3912
+ const { manifest, chunks } = await chunkBlob(bytes, options.chunkSize, {
3913
+ ...options.mimeType !== void 0 ? { mimeType: options.mimeType } : {},
3914
+ ...options.filename !== void 0 ? { filename: options.filename } : {}
3915
+ });
3916
+ for (const chunk of chunks.values()) {
3917
+ await store.put(chunk);
3918
+ }
3919
+ const baseRef = await store.put(bytes, {
3920
+ ...options.mimeType !== void 0 ? { mimeType: options.mimeType } : {},
3921
+ ...options.filename !== void 0 ? { filename: options.filename } : {}
3922
+ });
3923
+ const manifestRef = await store.put(serializeBlobManifest(manifest));
3924
+ return { ref: { ...baseRef, manifestHash: manifestRef.hash }, manifest };
3925
+ }
3926
+ async function fetchBlobManifest(provider, manifestHash) {
3927
+ const bytes = await provider.getChunk(manifestHash);
3928
+ if (bytes === null) {
3929
+ throw new Error(`Blob manifest ${manifestHash} could not be fetched from the provider`);
3930
+ }
3931
+ const actual = await hashBlob4(bytes);
3932
+ if (actual !== manifestHash) {
3933
+ throw new BlobIntegrityError(manifestHash, actual);
3934
+ }
3935
+ return parseBlobManifest(bytes);
3936
+ }
3937
+ async function resolveBlobManifest(provider, ref) {
3938
+ if (ref.manifestHash === void 0) {
3939
+ throw new Error(
3940
+ `Blob ${ref.hash} has no manifest to resolve. Store it with putBlobForTransfer (or app.blobs.put) so its reference carries a manifestHash, or pull with an explicit manifest.`
3941
+ );
3942
+ }
3943
+ return fetchBlobManifest(provider, ref.manifestHash);
3944
+ }
3945
+
3946
+ // src/blob/server-blob-callbacks.ts
3947
+ import { hashBlob as hashBlob5 } from "@korajs/core";
3948
+ function toServerBlobCallbacks(store) {
3949
+ return {
3950
+ resolveBlobChunk: (hash) => store.get(hash),
3951
+ async persistBlobChunk(hash, bytes) {
3952
+ const actual = await hashBlob5(bytes);
3953
+ if (actual !== hash) {
3954
+ return;
3955
+ }
3956
+ await store.put(bytes);
3957
+ }
3958
+ };
3959
+ }
3960
+ function createMemoryServerBlobStore() {
3961
+ const store = new MemoryBlobStore();
3962
+ return { store, callbacks: toServerBlobCallbacks(store) };
3963
+ }
3964
+
3965
+ // src/blob/blob-gc.ts
3966
+ import { isBlobRef } from "@korajs/core";
3967
+ function extractBlobRefs(record) {
3968
+ const refs = [];
3969
+ for (const value of Object.values(record)) {
3970
+ if (isBlobRef(value)) {
3971
+ refs.push(value);
3972
+ }
3973
+ }
3974
+ return refs;
3975
+ }
3976
+ async function collectBlobGarbage(store, liveRefs, options = {}) {
3977
+ const live = /* @__PURE__ */ new Set();
3978
+ for (const ref of liveRefs) {
3979
+ live.add(ref.hash);
3980
+ if (ref.manifestHash === void 0) {
3981
+ continue;
3982
+ }
3983
+ live.add(ref.manifestHash);
3984
+ try {
3985
+ const manifestBytes = await store.get(ref.manifestHash);
3986
+ if (manifestBytes !== null) {
3987
+ const manifest = parseBlobManifest(manifestBytes);
3988
+ for (const chunkHash of manifest.chunkHashes) {
3989
+ live.add(chunkHash);
3990
+ }
3991
+ }
3992
+ } catch {
3993
+ }
3994
+ }
3995
+ const all = await store.list();
3996
+ const collectedHashes = [];
3997
+ for (const hash of all) {
3998
+ if (live.has(hash)) {
3999
+ continue;
4000
+ }
4001
+ collectedHashes.push(hash);
4002
+ if (!options.dryRun) {
4003
+ await store.delete(hash);
4004
+ }
4005
+ }
4006
+ return {
4007
+ scanned: all.length,
4008
+ live: live.size,
4009
+ collected: collectedHashes.length,
4010
+ collectedHashes
4011
+ };
4012
+ }
4013
+
4014
+ // src/blob/blob-chunk-transport.ts
4015
+ import { generateUUIDv7 as generateUUIDv74 } from "@korajs/core";
4016
+ function createRemoteChunkProvider(port, options = {}) {
4017
+ const timeoutMs = options.timeoutMs ?? 3e4;
4018
+ const pending = /* @__PURE__ */ new Map();
4019
+ port.onMessage((message) => {
4020
+ if (message.type !== "blob-chunk-response") {
4021
+ return;
4022
+ }
4023
+ const entry = pending.get(message.requestId);
4024
+ if (!entry) {
4025
+ return;
4026
+ }
4027
+ pending.delete(message.requestId);
4028
+ clearTimeout(entry.timer);
4029
+ entry.resolve(message.bytes);
4030
+ });
4031
+ return {
4032
+ pendingCount: () => pending.size,
4033
+ getChunk(hash) {
4034
+ const requestId = generateUUIDv74();
4035
+ return new Promise((resolve, reject) => {
4036
+ const timer = setTimeout(() => {
4037
+ pending.delete(requestId);
4038
+ reject(new Error(`Blob chunk request for ${hash} timed out after ${timeoutMs}ms`));
4039
+ }, timeoutMs);
4040
+ pending.set(requestId, { resolve, reject, timer });
4041
+ port.send({ type: "blob-chunk-request", requestId, hash });
4042
+ });
4043
+ }
4044
+ };
4045
+ }
4046
+ function serveBlobChunks(port, blobStore) {
4047
+ port.onMessage((message) => {
4048
+ if (message.type !== "blob-chunk-request") {
4049
+ return;
4050
+ }
4051
+ void blobStore.get(message.hash).then(
4052
+ (bytes) => {
4053
+ port.send({ type: "blob-chunk-response", requestId: message.requestId, bytes });
4054
+ },
4055
+ () => {
4056
+ port.send({ type: "blob-chunk-response", requestId: message.requestId, bytes: null });
4057
+ }
4058
+ );
4059
+ });
4060
+ }
4061
+ function createChunkPortPair() {
4062
+ const handlersA = [];
4063
+ const handlersB = [];
4064
+ const a = {
4065
+ send(message) {
4066
+ queueMicrotask(() => {
4067
+ for (const handler of handlersB) {
4068
+ handler(message);
4069
+ }
4070
+ });
4071
+ },
4072
+ onMessage(handler) {
4073
+ handlersA.push(handler);
4074
+ }
4075
+ };
4076
+ const b = {
4077
+ send(message) {
4078
+ queueMicrotask(() => {
4079
+ for (const handler of handlersA) {
4080
+ handler(message);
4081
+ }
4082
+ });
4083
+ },
4084
+ onMessage(handler) {
4085
+ handlersB.push(handler);
4086
+ }
4087
+ };
4088
+ return { a, b };
4089
+ }
3425
4090
  export {
3426
4091
  AdapterError,
4092
+ BlobIntegrityError,
3427
4093
  COMPACTION_BASELINE_META_KEY,
3428
4094
  Collection,
4095
+ DEFAULT_CHUNK_SIZE,
3429
4096
  InvalidStateTransitionError,
3430
4097
  LAST_ACKED_SERVER_VECTOR_META_KEY,
4098
+ MemoryBlobStore,
4099
+ NoLeaderError,
4100
+ OpfsBlobStore,
3431
4101
  OptimisticLockError,
3432
4102
  PersistenceError,
3433
4103
  QueryBuilder,
@@ -3445,26 +4115,44 @@ export {
3445
4115
  WorkerTimeoutError,
3446
4116
  asRichTextSyncEngine,
3447
4117
  assertQueryReady,
4118
+ chunkBlob,
4119
+ collectBlobGarbage,
3448
4120
  collectOperationsAheadOfServer,
3449
4121
  compactOperationLog,
3450
4122
  computeAckCompactionWatermark,
4123
+ createChunkPortPair,
4124
+ createMemoryServerBlobStore,
4125
+ createOpfsBlobDirectory,
4126
+ createOpfsBlobStore,
4127
+ createRemoteChunkProvider,
3451
4128
  createRichTextController,
3452
4129
  decodeAuditExport,
3453
4130
  decodeRichtext,
3454
4131
  deserializeVersionVectorFromMeta,
3455
4132
  encodeRichtext,
3456
4133
  exportBackup,
4134
+ extractBlobRefs,
4135
+ fetchBlobManifest,
3457
4136
  getSharedQueryStoreCache,
3458
4137
  mergeVersionVectors,
4138
+ parseBlobManifest,
3459
4139
  persistedAuditTraceFromEvent,
3460
4140
  pluralize,
4141
+ prepareBlobForSend,
4142
+ putBlobForTransfer,
3461
4143
  readAuditExportManifest,
3462
4144
  readBackupManifest,
4145
+ reassembleBlob,
4146
+ receiveBlob,
4147
+ resolveBlobManifest,
3463
4148
  restoreBackup,
3464
4149
  richtextStatesEqual,
3465
4150
  richtextToPlainText,
4151
+ serializeBlobManifest,
3466
4152
  serializeVersionVectorToMeta,
4153
+ serveBlobChunks,
3467
4154
  singularize,
4155
+ toServerBlobCallbacks,
3468
4156
  validateStateTransition,
3469
4157
  validateUpdateStateMachine,
3470
4158
  verifyAuditExportChecksum,