@korajs/store 0.6.0 → 1.0.0-beta.1

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 (73) hide show
  1. package/LICENSE +21 -0
  2. package/dist/adapters/better-sqlite3.cjs +94 -40
  3. package/dist/adapters/better-sqlite3.cjs.map +1 -1
  4. package/dist/adapters/better-sqlite3.d.cts +9 -1
  5. package/dist/adapters/better-sqlite3.d.ts +9 -1
  6. package/dist/adapters/better-sqlite3.js +62 -41
  7. package/dist/adapters/better-sqlite3.js.map +1 -1
  8. package/dist/adapters/indexeddb.cjs +24 -13
  9. package/dist/adapters/indexeddb.cjs.map +1 -1
  10. package/dist/adapters/indexeddb.d.cts +2 -2
  11. package/dist/adapters/indexeddb.d.ts +2 -2
  12. package/dist/adapters/indexeddb.js +4 -3
  13. package/dist/adapters/indexeddb.js.map +1 -1
  14. package/dist/adapters/sqlite-wasm.cjs +24 -13
  15. package/dist/adapters/sqlite-wasm.cjs.map +1 -1
  16. package/dist/adapters/sqlite-wasm.d.cts +27 -4
  17. package/dist/adapters/sqlite-wasm.d.ts +27 -4
  18. package/dist/adapters/sqlite-wasm.js +6 -4
  19. package/dist/{backup-JN3LB4YT.js → backup-ZV4Y424B.js} +3 -3
  20. package/dist/blob-fs.cjs +166 -0
  21. package/dist/blob-fs.cjs.map +1 -0
  22. package/dist/blob-fs.d.cts +29 -0
  23. package/dist/blob-fs.d.ts +29 -0
  24. package/dist/blob-fs.js +128 -0
  25. package/dist/blob-fs.js.map +1 -0
  26. package/dist/{chunk-56LL4XP6.js → chunk-6NKLDBTY.js} +52 -1
  27. package/dist/chunk-6NKLDBTY.js.map +1 -0
  28. package/dist/{chunk-AGN3CIPB.js → chunk-FFYA7J5H.js} +2 -2
  29. package/dist/{chunk-XO4LMPAH.js → chunk-IOEFNHHP.js} +2 -35
  30. package/dist/chunk-IOEFNHHP.js.map +1 -0
  31. package/dist/{chunk-LAWV6CFH.js → chunk-QL2XVZRN.js} +12 -1
  32. package/dist/chunk-QL2XVZRN.js.map +1 -0
  33. package/dist/{chunk-6AHYGQQT.js → chunk-RBPVEAVM.js} +4 -4
  34. package/dist/chunk-TVJEC5S2.js +40 -0
  35. package/dist/chunk-TVJEC5S2.js.map +1 -0
  36. package/dist/chunk-W4MTTNEZ.js +55 -0
  37. package/dist/chunk-W4MTTNEZ.js.map +1 -0
  38. package/dist/{chunk-EZSTD2XC.js → chunk-X57MWW35.js} +2 -2
  39. package/dist/{chunk-7BPETY5Y.js → chunk-ZLWAYQEQ.js} +128 -15
  40. package/dist/chunk-ZLWAYQEQ.js.map +1 -0
  41. package/dist/content-addressed-blob-store-Dcj3SB9r.d.cts +60 -0
  42. package/dist/content-addressed-blob-store-Dcj3SB9r.d.ts +60 -0
  43. package/dist/{export-audit-5GV6VRDZ.js → export-audit-AJ7RORNK.js} +3 -3
  44. package/dist/index.cjs +1245 -113
  45. package/dist/index.cjs.map +1 -1
  46. package/dist/index.d.cts +468 -7
  47. package/dist/index.d.ts +468 -7
  48. package/dist/index.js +975 -61
  49. package/dist/index.js.map +1 -1
  50. package/dist/internal.cjs +123 -33
  51. package/dist/internal.cjs.map +1 -1
  52. package/dist/internal.d.cts +3 -3
  53. package/dist/internal.d.ts +3 -3
  54. package/dist/internal.js +3 -3
  55. package/dist/{sqlite-wasm-channel-I9y_jObg.d.cts → sqlite-wasm-channel-DUDPclTE.d.cts} +2 -15
  56. package/dist/{sqlite-wasm-channel-I9y_jObg.d.ts → sqlite-wasm-channel-DUDPclTE.d.ts} +2 -15
  57. package/dist/sqlite-wasm-channel-H43KEZLM.js +12 -0
  58. package/dist/{types-CTr00g_d.d.cts → types-5h4Hqcwc.d.ts} +10 -3
  59. package/dist/{types-BMyHmwvn.d.ts → types-DfPiPwKr.d.cts} +10 -3
  60. package/dist/{types-DepnRDk_.d.cts → types-k6AZgiH4.d.cts} +48 -2
  61. package/dist/{types-DepnRDk_.d.ts → types-k6AZgiH4.d.ts} +48 -2
  62. package/package.json +15 -2
  63. package/dist/chunk-56LL4XP6.js.map +0 -1
  64. package/dist/chunk-7BPETY5Y.js.map +0 -1
  65. package/dist/chunk-LAWV6CFH.js.map +0 -1
  66. package/dist/chunk-XO4LMPAH.js.map +0 -1
  67. package/dist/sqlite-wasm-channel-HDDW6UBH.js +0 -10
  68. /package/dist/{backup-JN3LB4YT.js.map → backup-ZV4Y424B.js.map} +0 -0
  69. /package/dist/{chunk-AGN3CIPB.js.map → chunk-FFYA7J5H.js.map} +0 -0
  70. /package/dist/{chunk-6AHYGQQT.js.map → chunk-RBPVEAVM.js.map} +0 -0
  71. /package/dist/{chunk-EZSTD2XC.js.map → chunk-X57MWW35.js.map} +0 -0
  72. /package/dist/{export-audit-5GV6VRDZ.js.map → export-audit-AJ7RORNK.js.map} +0 -0
  73. /package/dist/{sqlite-wasm-channel-HDDW6UBH.js.map → sqlite-wasm-channel-H43KEZLM.js.map} +0 -0
package/dist/index.js CHANGED
@@ -1,56 +1,67 @@
1
+ import {
2
+ exportBackup,
3
+ readBackupManifest,
4
+ restoreBackup,
5
+ verifyBackupChecksum
6
+ } from "./chunk-X57MWW35.js";
1
7
  import {
2
8
  decodeAuditExport,
3
9
  readAuditExportManifest,
4
10
  verifyAuditExportChecksum
5
- } from "./chunk-AGN3CIPB.js";
11
+ } from "./chunk-FFYA7J5H.js";
6
12
  import {
7
13
  persistedAuditTraceFromEvent
8
14
  } from "./chunk-MYSJN3DF.js";
15
+ import {
16
+ BlobIntegrityError,
17
+ MemoryBlobStore
18
+ } from "./chunk-W4MTTNEZ.js";
9
19
  import {
10
20
  InvalidStateTransitionError,
11
21
  allocateNextSequenceInTransaction,
12
22
  allocateNextSequenceNumber,
13
23
  buildCountQuery,
24
+ buildFieldFastForwardUpdateQuery,
14
25
  buildInsertQuery,
15
26
  buildLwwSoftDeleteQuery,
16
- buildLwwUpdateQuery,
17
27
  buildSelectQuery,
18
28
  buildSoftDeleteQuery,
19
29
  buildUpdateQuery,
20
30
  executeDelete,
21
31
  executeInsert,
22
32
  executeUpdate,
33
+ fieldVersionsForFields,
23
34
  isIncomingNewerThanRow,
35
+ parseFieldVersions,
24
36
  readSequenceNumber,
37
+ resolvePerFieldLww,
38
+ serializeFieldVersions,
25
39
  serializeRowVersion,
26
40
  validateStateTransition,
27
41
  validateUpdateStateMachine
28
- } from "./chunk-7BPETY5Y.js";
29
- import {
30
- AdapterError,
31
- PersistenceError,
32
- QueryError,
33
- RecordNotFoundError,
34
- StoreNotOpenError,
35
- WorkerInitError,
36
- WorkerTimeoutError
37
- } from "./chunk-LAWV6CFH.js";
38
- import {
39
- exportBackup,
40
- readBackupManifest,
41
- restoreBackup,
42
- verifyBackupChecksum
43
- } from "./chunk-EZSTD2XC.js";
42
+ } from "./chunk-ZLWAYQEQ.js";
44
43
  import {
45
44
  decodeRichtext,
45
+ decodeRichtextFieldsFromOpData,
46
46
  deserializeOperationWithCollection,
47
47
  deserializeRecord,
48
48
  encodeRichtext,
49
+ encodeRichtextFieldsForOpData,
49
50
  richtextStatesEqual,
50
51
  richtextToPlainText,
51
52
  serializeOperation,
52
53
  serializeRecord
53
- } from "./chunk-56LL4XP6.js";
54
+ } from "./chunk-6NKLDBTY.js";
55
+ import {
56
+ AdapterError,
57
+ OptimisticLockError,
58
+ PersistenceError,
59
+ QueryError,
60
+ RecordNotFoundError,
61
+ StoreNotOpenError,
62
+ WorkerInitError,
63
+ WorkerTimeoutError
64
+ } from "./chunk-QL2XVZRN.js";
54
65
 
55
66
  // src/compaction/types.ts
56
67
  var COMPACTION_BASELINE_META_KEY = "compaction_baseline_at";
@@ -195,7 +206,7 @@ async function saveDeltaCursor(adapter, cursor) {
195
206
  // src/store/store.ts
196
207
  import {
197
208
  CausalTracker,
198
- HybridLogicalClock as HybridLogicalClock2,
209
+ HybridLogicalClock as HybridLogicalClock3,
199
210
  createVersionVector as createVersionVector3,
200
211
  generateUUIDv7 as generateUUIDv73,
201
212
  migrationStepsToSQL
@@ -203,7 +214,7 @@ import {
203
214
 
204
215
  // src/collection/collection.ts
205
216
  var Collection = class {
206
- constructor(name, definition, schema, adapter, clock, nodeId, allocateSequenceNumber, onMutation, relationEnforcer, mutationHandler, causalTracker) {
217
+ constructor(name, definition, schema, adapter, clock, nodeId, allocateSequenceNumber, onMutation, relationEnforcer, mutationHandler, causalTracker, secretKeyProvider) {
207
218
  this.name = name;
208
219
  this.definition = definition;
209
220
  this.schema = schema;
@@ -215,6 +226,7 @@ var Collection = class {
215
226
  this.relationEnforcer = relationEnforcer;
216
227
  this.mutationHandler = mutationHandler;
217
228
  this.causalTracker = causalTracker;
229
+ this.secretKeyProvider = secretKeyProvider;
218
230
  }
219
231
  name;
220
232
  definition;
@@ -227,6 +239,7 @@ var Collection = class {
227
239
  relationEnforcer;
228
240
  mutationHandler;
229
241
  causalTracker;
242
+ secretKeyProvider;
230
243
  mutationContext() {
231
244
  return {
232
245
  collection: this.name,
@@ -239,7 +252,8 @@ var Collection = class {
239
252
  onMutation: this.onMutation,
240
253
  relationEnforcer: this.relationEnforcer,
241
254
  causalTracker: this.causalTracker,
242
- inTransaction: false
255
+ inTransaction: false,
256
+ secretKeyProvider: this.secretKeyProvider
243
257
  };
244
258
  }
245
259
  async insert(data) {
@@ -885,7 +899,9 @@ function applyOperationToMemory(state, op, schema) {
885
899
  }
886
900
  colMap.set(op.recordId, {
887
901
  id: op.recordId,
888
- fields: { ...op.data },
902
+ // op.data stores binary richtext as tagged JSON; snapshots must
903
+ // expose record-shaped values (Uint8Array/string).
904
+ fields: decodeRichtextFieldsFromOpData(op.data, definition.fields),
889
905
  deleted: false,
890
906
  createdAt: wallTime,
891
907
  updatedAt: wallTime
@@ -896,9 +912,10 @@ function applyOperationToMemory(state, op, schema) {
896
912
  if (!op.data) {
897
913
  return;
898
914
  }
915
+ const decodedData = decodeRichtextFieldsFromOpData(op.data, definition.fields);
899
916
  const existing = colMap.get(op.recordId);
900
917
  if (existing && !existing.deleted) {
901
- existing.fields = { ...existing.fields, ...op.data };
918
+ existing.fields = { ...existing.fields, ...decodedData };
902
919
  existing.updatedAt = wallTime;
903
920
  break;
904
921
  }
@@ -907,7 +924,7 @@ function applyOperationToMemory(state, op, schema) {
907
924
  }
908
925
  colMap.set(op.recordId, {
909
926
  id: op.recordId,
910
- fields: { ...op.data },
927
+ fields: decodedData,
911
928
  deleted: false,
912
929
  createdAt: wallTime,
913
930
  updatedAt: wallTime
@@ -1458,6 +1475,137 @@ var SubscriptionManager = class {
1458
1475
  }
1459
1476
  };
1460
1477
 
1478
+ // src/sync/rebase-unsynced-operations.ts
1479
+ import { HybridLogicalClock as HybridLogicalClock2, MAX_LOGICAL } from "@korajs/core";
1480
+ import { computeOperationId } from "@korajs/core/internal";
1481
+ var LOGICAL_COUNTER_LIMIT = MAX_LOGICAL + 1;
1482
+ function emptyResult() {
1483
+ return { operations: [], idMapping: {}, rebasedCount: 0, newMaxTimestamp: null };
1484
+ }
1485
+ async function rebaseUnsyncedOperationsInLog(adapter, schema, unsyncedOpIds, correctedNowMs) {
1486
+ if (unsyncedOpIds.length === 0) {
1487
+ return emptyResult();
1488
+ }
1489
+ const rebaseIds = new Set(unsyncedOpIds);
1490
+ const rebased = [];
1491
+ let maxOtherSerializedTs = null;
1492
+ for (const collectionName of Object.keys(schema.collections)) {
1493
+ const rows = await adapter.query(`SELECT * FROM _kora_ops_${collectionName}`);
1494
+ for (const row of rows) {
1495
+ if (rebaseIds.has(row.id)) {
1496
+ rebased.push(deserializeOperationWithCollection(row, collectionName));
1497
+ } else if (maxOtherSerializedTs === null || row.timestamp > maxOtherSerializedTs) {
1498
+ maxOtherSerializedTs = row.timestamp;
1499
+ }
1500
+ }
1501
+ }
1502
+ if (rebased.length === 0) {
1503
+ return emptyResult();
1504
+ }
1505
+ const maxOtherWallTime = maxOtherSerializedTs === null ? Number.NEGATIVE_INFINITY : HybridLogicalClock2.deserialize(maxOtherSerializedTs).wallTime;
1506
+ const baseWallTime = Math.max(correctedNowMs, maxOtherWallTime + 1);
1507
+ rebased.sort((a, b) => HybridLogicalClock2.compare(a.timestamp, b.timestamp));
1508
+ const newTimestamps = rebased.map((op, i) => ({
1509
+ // Spill into wallTime past the 5-digit logical serialization limit so the
1510
+ // serialized order stays correct even for enormous offline queues.
1511
+ wallTime: baseWallTime + Math.floor(i / LOGICAL_COUNTER_LIMIT),
1512
+ logical: i % LOGICAL_COUNTER_LIMIT,
1513
+ nodeId: op.nodeId
1514
+ }));
1515
+ const idMapping = {};
1516
+ const newIds = [];
1517
+ for (let i = 0; i < rebased.length; i++) {
1518
+ const op = rebased[i];
1519
+ const ts = newTimestamps[i];
1520
+ if (!op || !ts) {
1521
+ continue;
1522
+ }
1523
+ const input = {
1524
+ nodeId: op.nodeId,
1525
+ type: op.type,
1526
+ collection: op.collection,
1527
+ recordId: op.recordId,
1528
+ data: op.data,
1529
+ previousData: op.previousData,
1530
+ sequenceNumber: op.sequenceNumber,
1531
+ causalDeps: op.causalDeps,
1532
+ schemaVersion: op.schemaVersion,
1533
+ ...op.atomicOps !== void 0 ? { atomicOps: op.atomicOps } : {}
1534
+ };
1535
+ const newId = await computeOperationId(input, HybridLogicalClock2.serialize(ts));
1536
+ idMapping[op.id] = newId;
1537
+ newIds.push(newId);
1538
+ }
1539
+ const newOperations = [];
1540
+ for (let i = 0; i < rebased.length; i++) {
1541
+ const op = rebased[i];
1542
+ const ts = newTimestamps[i];
1543
+ const newId = newIds[i];
1544
+ if (!op || !ts || newId === void 0) {
1545
+ continue;
1546
+ }
1547
+ newOperations.push({
1548
+ ...op,
1549
+ id: newId,
1550
+ timestamp: ts,
1551
+ causalDeps: op.causalDeps.map((dep) => idMapping[dep] ?? dep)
1552
+ });
1553
+ }
1554
+ await adapter.transaction(async (tx) => {
1555
+ for (let i = 0; i < rebased.length; i++) {
1556
+ const oldOp = rebased[i];
1557
+ const newOp = newOperations[i];
1558
+ if (!oldOp || !newOp) {
1559
+ continue;
1560
+ }
1561
+ const opsTable = `_kora_ops_${oldOp.collection}`;
1562
+ await tx.execute(`DELETE FROM ${opsTable} WHERE id = ?`, [oldOp.id]);
1563
+ const opInsert = buildInsertQuery(
1564
+ opsTable,
1565
+ serializeOperation(newOp)
1566
+ );
1567
+ await tx.execute(opInsert.sql, opInsert.params);
1568
+ const oldVersion = serializeRowVersion(oldOp.timestamp);
1569
+ const newVersion = serializeRowVersion(newOp.timestamp);
1570
+ await tx.execute(
1571
+ `UPDATE ${oldOp.collection} SET _version = ?, _updated_at = ? WHERE id = ? AND _version = ?`,
1572
+ [newVersion, newOp.timestamp.wallTime, oldOp.recordId, oldVersion]
1573
+ );
1574
+ const rows = await tx.query(
1575
+ `SELECT _field_versions FROM ${oldOp.collection} WHERE id = ?`,
1576
+ [oldOp.recordId]
1577
+ );
1578
+ const fieldVersions = parseFieldVersions(rows[0]?._field_versions);
1579
+ let changed = false;
1580
+ for (const [field, version] of Object.entries(fieldVersions)) {
1581
+ if (version === oldVersion) {
1582
+ fieldVersions[field] = newVersion;
1583
+ changed = true;
1584
+ }
1585
+ }
1586
+ if (changed) {
1587
+ await tx.execute(`UPDATE ${oldOp.collection} SET _field_versions = ? WHERE id = ?`, [
1588
+ serializeFieldVersions(fieldVersions),
1589
+ oldOp.recordId
1590
+ ]);
1591
+ }
1592
+ if (oldOp.type === "insert") {
1593
+ await tx.execute(
1594
+ `UPDATE ${oldOp.collection} SET _created_at = ? WHERE id = ? AND _created_at = ?`,
1595
+ [newOp.timestamp.wallTime, oldOp.recordId, oldOp.timestamp.wallTime]
1596
+ );
1597
+ }
1598
+ }
1599
+ });
1600
+ const newMaxTimestamp = newTimestamps[newTimestamps.length - 1] ?? null;
1601
+ return {
1602
+ operations: newOperations,
1603
+ idMapping,
1604
+ rebasedCount: newOperations.length,
1605
+ newMaxTimestamp
1606
+ };
1607
+ }
1608
+
1461
1609
  // src/transaction/transaction-context.ts
1462
1610
  import {
1463
1611
  createOperation as createOperation2,
@@ -1607,7 +1755,9 @@ var TransactionContext = class {
1607
1755
  type: "insert",
1608
1756
  collection: collectionName,
1609
1757
  recordId,
1610
- data: { ...validated },
1758
+ // Binary richtext values are tagged as canonical JSON before the
1759
+ // operation is content-hashed (see encodeRichtextFieldsForOpData).
1760
+ data: encodeRichtextFieldsForOpData(validated, definition.fields),
1611
1761
  previousData: null,
1612
1762
  sequenceNumber,
1613
1763
  causalDeps,
@@ -1678,8 +1828,10 @@ var TransactionContext = class {
1678
1828
  type: "update",
1679
1829
  collection: collectionName,
1680
1830
  recordId: id,
1681
- data: { ...resolvedData },
1682
- previousData,
1831
+ // Binary richtext values (new and previous) are tagged as canonical
1832
+ // JSON before the operation is content-hashed.
1833
+ data: encodeRichtextFieldsForOpData(resolvedData, definition.fields),
1834
+ previousData: encodeRichtextFieldsForOpData(previousData, definition.fields),
1683
1835
  sequenceNumber,
1684
1836
  causalDeps,
1685
1837
  schemaVersion: this.config.schema.version,
@@ -1786,14 +1938,16 @@ var TransactionContext = class {
1786
1938
  if (entry.operation.type === "insert" && entry.operation.data) {
1787
1939
  record = {
1788
1940
  id,
1789
- ...entry.operation.data,
1941
+ // op.data stores binary richtext as tagged JSON; the effective
1942
+ // record must expose record-shaped values (Uint8Array/string).
1943
+ ...decodeRichtextFieldsFromOpData(entry.operation.data, definition.fields),
1790
1944
  createdAt: entry.operation.timestamp.wallTime,
1791
1945
  updatedAt: entry.operation.timestamp.wallTime
1792
1946
  };
1793
1947
  } else if (entry.operation.type === "update" && entry.operation.data && record) {
1794
1948
  record = {
1795
1949
  ...record,
1796
- ...entry.operation.data,
1950
+ ...decodeRichtextFieldsFromOpData(entry.operation.data, definition.fields),
1797
1951
  updatedAt: entry.operation.timestamp.wallTime
1798
1952
  };
1799
1953
  } else if (entry.operation.type === "delete") {
@@ -1873,6 +2027,7 @@ var Store = class {
1873
2027
  localMutationHandler;
1874
2028
  relationEnforcer = null;
1875
2029
  causalTracker = null;
2030
+ secretKeyProvider;
1876
2031
  constructor(config) {
1877
2032
  this.schema = config.schema;
1878
2033
  this.adapter = config.adapter;
@@ -1881,6 +2036,7 @@ var Store = class {
1881
2036
  this.isolation = config.isolation ?? "shared";
1882
2037
  this.emitter = config.emitter ?? null;
1883
2038
  this.localMutationHandler = config.localMutationHandler ?? null;
2039
+ this.secretKeyProvider = config.secretKeyProvider;
1884
2040
  this.subscriptionManager = new SubscriptionManager({
1885
2041
  onQuerySubscribed: config.onQuerySubscribed
1886
2042
  });
@@ -1893,7 +2049,7 @@ var Store = class {
1893
2049
  await this.adapter.open(this.schema);
1894
2050
  await this.runMigrationsIfNeeded();
1895
2051
  this.nodeId = await this.loadOrGenerateNodeId();
1896
- this.clock = new HybridLogicalClock2(this.nodeId);
2052
+ this.clock = new HybridLogicalClock3(this.nodeId);
1897
2053
  this.causalTracker = new CausalTracker();
1898
2054
  this.sequenceManager = new SequenceManager(this.adapter, this.nodeId);
1899
2055
  this.sequenceNumber = await this.loadSequenceNumber();
@@ -1923,7 +2079,8 @@ var Store = class {
1923
2079
  },
1924
2080
  this.relationEnforcer,
1925
2081
  this.localMutationHandler,
1926
- this.causalTracker
2082
+ this.causalTracker,
2083
+ this.secretKeyProvider
1927
2084
  );
1928
2085
  this.collections.set(name, col);
1929
2086
  }
@@ -1980,6 +2137,14 @@ var Store = class {
1980
2137
  /**
1981
2138
  * Get the node ID for this store instance.
1982
2139
  */
2140
+ /**
2141
+ * Records the offset between server time and this device's clock
2142
+ * (serverTime - localTime), learned at sync handshake. Lets the store's HLC
2143
+ * validate remote timestamps correctly even when the local clock is wrong.
2144
+ */
2145
+ setClockReferenceOffset(offsetMs) {
2146
+ this.clock?.setReferenceOffset(offsetMs);
2147
+ }
1983
2148
  getNodeId() {
1984
2149
  this.ensureOpen();
1985
2150
  return this.nodeId;
@@ -2006,45 +2171,158 @@ var Store = class {
2006
2171
  if (this.clock) {
2007
2172
  this.clock.receive(op.timestamp);
2008
2173
  }
2009
- const remoteVersion = serializeRowVersion(op.timestamp);
2010
- const wallTime = op.timestamp.wallTime;
2174
+ const materializeTimestamp = options?.materializeTimestamp ?? op.timestamp;
2175
+ const remoteVersion = serializeRowVersion(materializeTimestamp);
2176
+ const wallTime = materializeTimestamp.wallTime;
2177
+ const materializeSource = options?.materializeData ?? op.data;
2011
2178
  await this.adapter.transaction(async (tx) => {
2012
- if (op.type === "insert" && op.data) {
2179
+ const checkGuard = (row) => {
2180
+ const guard = options?.guardRowState;
2181
+ if (!guard) {
2182
+ return;
2183
+ }
2184
+ const version = typeof row?._version === "string" ? row._version : null;
2185
+ const fieldVersions = typeof row?._field_versions === "string" ? row._field_versions : null;
2186
+ if (version !== guard.version || fieldVersions !== guard.fieldVersions) {
2187
+ throw new OptimisticLockError(collection, op.recordId);
2188
+ }
2189
+ };
2190
+ if (op.type === "insert" && materializeSource) {
2191
+ const serializedData = serializeRecord(materializeSource, definition.fields);
2013
2192
  const existing2 = await tx.query(
2014
- `SELECT _updated_at, _version FROM ${collection} WHERE id = ?`,
2193
+ `SELECT _updated_at, _version, _field_versions, _deleted FROM ${collection} WHERE id = ?`,
2015
2194
  [op.recordId]
2016
2195
  );
2017
2196
  const row = existing2[0];
2018
- if (row && !isIncomingNewerThanRow(op.timestamp, row)) {
2019
- } else {
2020
- const serializedData = serializeRecord(op.data, definition.fields);
2197
+ checkGuard(row);
2198
+ if (!row) {
2021
2199
  const record = {
2022
2200
  id: op.recordId,
2023
2201
  ...serializedData,
2024
2202
  _created_at: wallTime,
2025
2203
  _updated_at: wallTime,
2026
- _version: remoteVersion
2204
+ _version: remoteVersion,
2205
+ // Stamp every inserted field so per-field LWW has a real baseline.
2206
+ _field_versions: serializeFieldVersions(
2207
+ fieldVersionsForFields(Object.keys(serializedData), remoteVersion)
2208
+ )
2027
2209
  };
2028
2210
  const insertQuery = buildInsertQuery(collection, record);
2029
2211
  await tx.execute(insertQuery.sql, insertQuery.params);
2212
+ await this.foldOrphanedOperations(tx, collection, definition, op.recordId);
2213
+ } else if (row._deleted === 1) {
2214
+ if (isIncomingNewerThanRow(materializeTimestamp, row)) {
2215
+ const fieldChanges = {
2216
+ ...serializedData,
2217
+ _deleted: 0,
2218
+ _field_versions: serializeFieldVersions(
2219
+ fieldVersionsForFields(Object.keys(serializedData), remoteVersion)
2220
+ )
2221
+ };
2222
+ const upsert = buildFieldFastForwardUpdateQuery(
2223
+ collection,
2224
+ op.recordId,
2225
+ fieldChanges,
2226
+ remoteVersion,
2227
+ wallTime,
2228
+ { maxCreatedAt: wallTime }
2229
+ );
2230
+ await tx.execute(upsert.sql, upsert.params);
2231
+ }
2232
+ } else {
2233
+ const currentVersions = parseFieldVersions(row._field_versions);
2234
+ const rowVersion = typeof row._version === "string" ? row._version : void 0;
2235
+ const fieldChanges = {};
2236
+ let fieldVersionsJson;
2237
+ if (options?.forceMaterialize) {
2238
+ const merged = { ...currentVersions };
2239
+ for (const field of Object.keys(serializedData)) {
2240
+ fieldChanges[field] = serializedData[field];
2241
+ merged[field] = remoteVersion;
2242
+ }
2243
+ fieldVersionsJson = serializeFieldVersions(merged);
2244
+ } else {
2245
+ const { winners, merged } = resolvePerFieldLww(
2246
+ currentVersions,
2247
+ Object.keys(serializedData),
2248
+ remoteVersion,
2249
+ rowVersion
2250
+ );
2251
+ for (const field of winners) {
2252
+ fieldChanges[field] = serializedData[field];
2253
+ }
2254
+ fieldVersionsJson = serializeFieldVersions(merged);
2255
+ }
2256
+ const upsert = buildFieldFastForwardUpdateQuery(
2257
+ collection,
2258
+ op.recordId,
2259
+ { ...fieldChanges, _field_versions: fieldVersionsJson },
2260
+ remoteVersion,
2261
+ wallTime,
2262
+ { maxCreatedAt: wallTime }
2263
+ );
2264
+ await tx.execute(upsert.sql, upsert.params);
2030
2265
  }
2031
- } else if (op.type === "update" && op.data) {
2032
- const serializedChanges = serializeRecord(op.data, definition.fields);
2033
- const updatePayload = {
2034
- ...serializedChanges,
2035
- _updated_at: wallTime,
2036
- _version: remoteVersion
2037
- };
2038
- if (options?.reactivateIfDeleted) {
2039
- updatePayload._deleted = 0;
2040
- }
2041
- const updateQuery = buildLwwUpdateQuery(
2042
- collection,
2043
- op.recordId,
2044
- updatePayload,
2045
- remoteVersion
2266
+ } else if (op.type === "update" && materializeSource) {
2267
+ const serializedChanges = serializeRecord(materializeSource, definition.fields);
2268
+ const changedFields = Object.keys(serializedChanges);
2269
+ const currentRows = await tx.query(
2270
+ `SELECT _version, _field_versions FROM ${collection} WHERE id = ?`,
2271
+ [op.recordId]
2046
2272
  );
2047
- await tx.execute(updateQuery.sql, updateQuery.params);
2273
+ const currentRow = currentRows[0];
2274
+ checkGuard(currentRow);
2275
+ if (currentRow) {
2276
+ const currentVersions = parseFieldVersions(currentRow._field_versions);
2277
+ const rowVersion = typeof currentRow._version === "string" ? currentRow._version : void 0;
2278
+ if (options?.forceMaterialize) {
2279
+ const merged = { ...currentVersions };
2280
+ for (const field of changedFields) {
2281
+ merged[field] = remoteVersion;
2282
+ }
2283
+ const fieldChanges = {
2284
+ ...serializedChanges,
2285
+ _field_versions: serializeFieldVersions(merged)
2286
+ };
2287
+ if (options?.reactivateIfDeleted) {
2288
+ fieldChanges._deleted = 0;
2289
+ }
2290
+ const forceQuery = buildFieldFastForwardUpdateQuery(
2291
+ collection,
2292
+ op.recordId,
2293
+ fieldChanges,
2294
+ remoteVersion,
2295
+ wallTime
2296
+ );
2297
+ await tx.execute(forceQuery.sql, forceQuery.params);
2298
+ } else {
2299
+ const { winners, merged } = resolvePerFieldLww(
2300
+ currentVersions,
2301
+ changedFields,
2302
+ remoteVersion,
2303
+ rowVersion
2304
+ );
2305
+ if (winners.length > 0 || options?.reactivateIfDeleted) {
2306
+ const winningChanges = {
2307
+ _field_versions: serializeFieldVersions(merged)
2308
+ };
2309
+ for (const field of winners) {
2310
+ winningChanges[field] = serializedChanges[field];
2311
+ }
2312
+ if (options?.reactivateIfDeleted) {
2313
+ winningChanges._deleted = 0;
2314
+ }
2315
+ const forceQuery = buildFieldFastForwardUpdateQuery(
2316
+ collection,
2317
+ op.recordId,
2318
+ winningChanges,
2319
+ remoteVersion,
2320
+ wallTime
2321
+ );
2322
+ await tx.execute(forceQuery.sql, forceQuery.params);
2323
+ }
2324
+ }
2325
+ }
2048
2326
  } else if (op.type === "delete") {
2049
2327
  const deleteQuery = buildLwwSoftDeleteQuery(
2050
2328
  collection,
@@ -2072,6 +2350,67 @@ var Store = class {
2072
2350
  this.subscriptionManager.notify(collection, op);
2073
2351
  return "applied";
2074
2352
  }
2353
+ /**
2354
+ * Materialize update/delete operations that were logged for a record BEFORE
2355
+ * its insert arrived (reordered delivery). Runs inside the insert's write
2356
+ * transaction, folding each orphan in timestamp order through the exact
2357
+ * per-field LWW / LWW-delete rules the normal apply paths use, so the final
2358
+ * row state equals what a device that received the operations in causal
2359
+ * order computed. Called only when the insert created a previously-absent
2360
+ * row — in that case every logged update/delete for the record is
2361
+ * necessarily an orphan (rows are soft-deleted, never removed, so a row
2362
+ * that is absent now was never materialized).
2363
+ */
2364
+ async foldOrphanedOperations(tx, collection, definition, recordId) {
2365
+ const orphanRows = await tx.query(
2366
+ `SELECT * FROM _kora_ops_${collection} WHERE record_id = ? AND type IN ('update', 'delete')`,
2367
+ [recordId]
2368
+ );
2369
+ if (orphanRows.length === 0) {
2370
+ return;
2371
+ }
2372
+ const orphans = orphanRows.map((r) => deserializeOperationWithCollection(r, collection)).sort((a, b) => HybridLogicalClock3.compare(a.timestamp, b.timestamp));
2373
+ for (const orphan of orphans) {
2374
+ const orphanVersion = serializeRowVersion(orphan.timestamp);
2375
+ const orphanWall = orphan.timestamp.wallTime;
2376
+ if (orphan.type === "update" && orphan.data) {
2377
+ const orphanChanges = serializeRecord(orphan.data, definition.fields);
2378
+ const rows = await tx.query(
2379
+ `SELECT _version, _field_versions FROM ${collection} WHERE id = ?`,
2380
+ [recordId]
2381
+ );
2382
+ const current = rows[0];
2383
+ if (!current) {
2384
+ continue;
2385
+ }
2386
+ const { winners, merged } = resolvePerFieldLww(
2387
+ parseFieldVersions(current._field_versions),
2388
+ Object.keys(orphanChanges),
2389
+ orphanVersion,
2390
+ typeof current._version === "string" ? current._version : void 0
2391
+ );
2392
+ if (winners.length > 0) {
2393
+ const winningChanges = {
2394
+ _field_versions: serializeFieldVersions(merged)
2395
+ };
2396
+ for (const field of winners) {
2397
+ winningChanges[field] = orphanChanges[field];
2398
+ }
2399
+ const query = buildFieldFastForwardUpdateQuery(
2400
+ collection,
2401
+ recordId,
2402
+ winningChanges,
2403
+ orphanVersion,
2404
+ orphanWall
2405
+ );
2406
+ await tx.execute(query.sql, query.params);
2407
+ }
2408
+ } else if (orphan.type === "delete") {
2409
+ const query = buildLwwSoftDeleteQuery(collection, recordId, orphanWall, orphanVersion);
2410
+ await tx.execute(query.sql, query.params);
2411
+ }
2412
+ }
2413
+ }
2075
2414
  /**
2076
2415
  * Get operations from a node within a sequence number range.
2077
2416
  * Implements the OperationLog interface for computeDelta.
@@ -2157,7 +2496,7 @@ var Store = class {
2157
2496
  */
2158
2497
  async exportAudit(options) {
2159
2498
  this.ensureOpen();
2160
- const { exportAudit: doExport } = await import("./export-audit-5GV6VRDZ.js");
2499
+ const { exportAudit: doExport } = await import("./export-audit-AJ7RORNK.js");
2161
2500
  return doExport(this.adapter, this.schema, this.nodeId, this.schema.version, options);
2162
2501
  }
2163
2502
  /**
@@ -2202,7 +2541,8 @@ var Store = class {
2202
2541
  relationEnforcer: this.relationEnforcer,
2203
2542
  causalTracker: this.causalTracker,
2204
2543
  inTransaction: options?.inTransaction ?? false,
2205
- extraCausalDeps: options?.extraCausalDeps
2544
+ extraCausalDeps: options?.extraCausalDeps,
2545
+ secretKeyProvider: this.secretKeyProvider
2206
2546
  };
2207
2547
  }
2208
2548
  /**
@@ -2227,6 +2567,31 @@ var Store = class {
2227
2567
  deleted: row._deleted === 1
2228
2568
  };
2229
2569
  }
2570
+ /**
2571
+ * Raw version state of a materialized row, for optimistic-concurrency guarded
2572
+ * applies: capture this snapshot before computing a merge, pass it as
2573
+ * `guardRowState`, and the apply refuses to write if the row changed since.
2574
+ * Returns null when no row exists (which is itself a valid guard state — the
2575
+ * apply then requires the row to still be absent).
2576
+ */
2577
+ async getRowVersionState(collection, recordId) {
2578
+ this.ensureOpen();
2579
+ if (!this.schema.collections[collection]) {
2580
+ return null;
2581
+ }
2582
+ const rows = await this.adapter.query(
2583
+ `SELECT _version, _field_versions FROM ${collection} WHERE id = ?`,
2584
+ [recordId]
2585
+ );
2586
+ const row = rows[0];
2587
+ if (!row) {
2588
+ return null;
2589
+ }
2590
+ return {
2591
+ version: typeof row._version === "string" ? row._version : null,
2592
+ fieldVersions: typeof row._field_versions === "string" ? row._field_versions : null
2593
+ };
2594
+ }
2230
2595
  /**
2231
2596
  * Latest operation from this device for a record (used for delete-vs-update merge on sync).
2232
2597
  */
@@ -2269,6 +2634,30 @@ var Store = class {
2269
2634
  (nodeId, fromSeq, toSeq) => this.getOperationRange(nodeId, fromSeq, toSeq)
2270
2635
  );
2271
2636
  }
2637
+ /**
2638
+ * Re-stamp never-acknowledged local operations after a fast device clock was
2639
+ * corrected, so sync can resume immediately. Safe because unacknowledged
2640
+ * operations are private to this device (like unpushed git commits);
2641
+ * acknowledged/shared operations are immutable and never touched.
2642
+ *
2643
+ * After the rewrite the store's HLC is advanced past the highest new
2644
+ * timestamp so subsequent local writes keep sorting after the rebased ops.
2645
+ * No data-change notifications are emitted: materialized values are
2646
+ * unchanged, only version stamps move.
2647
+ */
2648
+ async rebaseUnsyncedOperations(unsyncedOpIds, correctedNowMs) {
2649
+ this.ensureOpen();
2650
+ const result = await rebaseUnsyncedOperationsInLog(
2651
+ this.adapter,
2652
+ this.schema,
2653
+ unsyncedOpIds,
2654
+ correctedNowMs
2655
+ );
2656
+ if (result.newMaxTimestamp && this.clock) {
2657
+ this.clock.advanceTo(result.newMaxTimestamp);
2658
+ }
2659
+ return result;
2660
+ }
2272
2661
  /**
2273
2662
  * Count of local operations ahead of the server version vector.
2274
2663
  */
@@ -2318,7 +2707,7 @@ var Store = class {
2318
2707
  let latest = null;
2319
2708
  for (const row of rows) {
2320
2709
  const op = deserializeOperationWithCollection(row, collection);
2321
- if (!latest || HybridLogicalClock2.compare(op.timestamp, latest.timestamp) > 0) {
2710
+ if (!latest || HybridLogicalClock3.compare(op.timestamp, latest.timestamp) > 0) {
2322
2711
  latest = op;
2323
2712
  }
2324
2713
  }
@@ -2402,7 +2791,7 @@ var Store = class {
2402
2791
  */
2403
2792
  async exportBackup(options) {
2404
2793
  this.ensureOpen();
2405
- const { exportBackup: doExport } = await import("./backup-JN3LB4YT.js");
2794
+ const { exportBackup: doExport } = await import("./backup-ZV4Y424B.js");
2406
2795
  return doExport(this.adapter, this.schema, this.nodeId, this.schema.version, options);
2407
2796
  }
2408
2797
  /**
@@ -2414,7 +2803,7 @@ var Store = class {
2414
2803
  */
2415
2804
  async importBackup(data, options) {
2416
2805
  this.ensureOpen();
2417
- const { restoreBackup: doRestore } = await import("./backup-JN3LB4YT.js");
2806
+ const { restoreBackup: doRestore } = await import("./backup-ZV4Y424B.js");
2418
2807
  return doRestore(this.adapter, this.schema, data, options);
2419
2808
  }
2420
2809
  /**
@@ -3044,12 +3433,519 @@ function asRichTextSyncEngine(engine) {
3044
3433
  }
3045
3434
  return engine;
3046
3435
  }
3436
+
3437
+ // src/blob/opfs-blob-store.ts
3438
+ import { createBlobRef, hashBlob } from "@korajs/core";
3439
+ var OpfsBlobStore = class {
3440
+ constructor(dir) {
3441
+ this.dir = dir;
3442
+ }
3443
+ dir;
3444
+ shardFor(hash) {
3445
+ return hash.slice(0, 2);
3446
+ }
3447
+ async put(bytes, metadata = {}) {
3448
+ const ref = await createBlobRef(bytes, metadata);
3449
+ const shard = this.shardFor(ref.hash);
3450
+ if (await this.dir.has(shard, ref.hash)) {
3451
+ return ref;
3452
+ }
3453
+ const out = new Uint8Array(bytes.byteLength);
3454
+ out.set(bytes);
3455
+ await this.dir.write(shard, ref.hash, out);
3456
+ return ref;
3457
+ }
3458
+ async get(hash) {
3459
+ const bytes = await this.dir.read(this.shardFor(hash), hash);
3460
+ if (bytes === null) {
3461
+ return null;
3462
+ }
3463
+ const actual = await hashBlob(bytes);
3464
+ if (actual !== hash) {
3465
+ throw new BlobIntegrityError(hash, actual);
3466
+ }
3467
+ return bytes;
3468
+ }
3469
+ async has(hash) {
3470
+ return this.dir.has(this.shardFor(hash), hash);
3471
+ }
3472
+ async delete(hash) {
3473
+ return this.dir.remove(this.shardFor(hash), hash);
3474
+ }
3475
+ async size() {
3476
+ return this.dir.count();
3477
+ }
3478
+ async list() {
3479
+ return this.dir.keys();
3480
+ }
3481
+ };
3482
+ function getStorageManager() {
3483
+ const nav = globalThis.navigator;
3484
+ if (!nav?.storage || typeof nav.storage.getDirectory !== "function") {
3485
+ throw new Error(
3486
+ "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."
3487
+ );
3488
+ }
3489
+ return nav.storage;
3490
+ }
3491
+ function isNotFoundError(error) {
3492
+ return typeof error === "object" && error !== null && error.name === "NotFoundError";
3493
+ }
3494
+ async function createOpfsBlobDirectory(rootDirName = "kora-blobs") {
3495
+ const storage = getStorageManager();
3496
+ if (typeof storage.persist === "function") {
3497
+ try {
3498
+ await storage.persist();
3499
+ } catch {
3500
+ }
3501
+ }
3502
+ const opfsRoot = await storage.getDirectory();
3503
+ const root = await opfsRoot.getDirectoryHandle(rootDirName, { create: true });
3504
+ async function shardHandle(shard, create) {
3505
+ try {
3506
+ return await root.getDirectoryHandle(shard, { create });
3507
+ } catch (error) {
3508
+ if (isNotFoundError(error)) {
3509
+ return null;
3510
+ }
3511
+ throw error;
3512
+ }
3513
+ }
3514
+ return {
3515
+ async read(shard, name) {
3516
+ const dir = await shardHandle(shard, false);
3517
+ if (!dir) {
3518
+ return null;
3519
+ }
3520
+ let handle;
3521
+ try {
3522
+ handle = await dir.getFileHandle(name, { create: false });
3523
+ } catch (error) {
3524
+ if (isNotFoundError(error)) {
3525
+ return null;
3526
+ }
3527
+ throw error;
3528
+ }
3529
+ const file = await handle.getFile();
3530
+ return new Uint8Array(await file.arrayBuffer());
3531
+ },
3532
+ async write(shard, name, bytes) {
3533
+ const dir = await root.getDirectoryHandle(shard, { create: true });
3534
+ const handle = await dir.getFileHandle(name, { create: true });
3535
+ const writable = await handle.createWritable({ keepExistingData: false });
3536
+ try {
3537
+ await writable.write(bytes);
3538
+ } finally {
3539
+ await writable.close();
3540
+ }
3541
+ },
3542
+ async remove(shard, name) {
3543
+ const dir = await shardHandle(shard, false);
3544
+ if (!dir) {
3545
+ return false;
3546
+ }
3547
+ try {
3548
+ await dir.removeEntry(name);
3549
+ return true;
3550
+ } catch (error) {
3551
+ if (isNotFoundError(error)) {
3552
+ return false;
3553
+ }
3554
+ throw error;
3555
+ }
3556
+ },
3557
+ async has(shard, name) {
3558
+ const dir = await shardHandle(shard, false);
3559
+ if (!dir) {
3560
+ return false;
3561
+ }
3562
+ try {
3563
+ await dir.getFileHandle(name, { create: false });
3564
+ return true;
3565
+ } catch (error) {
3566
+ if (isNotFoundError(error)) {
3567
+ return false;
3568
+ }
3569
+ throw error;
3570
+ }
3571
+ },
3572
+ async count() {
3573
+ let total = 0;
3574
+ for await (const shard of root.keys()) {
3575
+ const dir = await shardHandle(shard, false);
3576
+ if (!dir) {
3577
+ continue;
3578
+ }
3579
+ for await (const name of dir.keys()) {
3580
+ if (!name.endsWith(".tmp")) {
3581
+ total++;
3582
+ }
3583
+ }
3584
+ }
3585
+ return total;
3586
+ },
3587
+ async keys() {
3588
+ const names = [];
3589
+ for await (const shard of root.keys()) {
3590
+ const dir = await shardHandle(shard, false);
3591
+ if (!dir) {
3592
+ continue;
3593
+ }
3594
+ for await (const name of dir.keys()) {
3595
+ if (!name.endsWith(".tmp")) {
3596
+ names.push(name);
3597
+ }
3598
+ }
3599
+ }
3600
+ return names;
3601
+ }
3602
+ };
3603
+ }
3604
+ async function createOpfsBlobStore(rootDirName = "kora-blobs") {
3605
+ return new OpfsBlobStore(await createOpfsBlobDirectory(rootDirName));
3606
+ }
3607
+
3608
+ // src/blob/blob-chunking.ts
3609
+ import { hashBlob as hashBlob2 } from "@korajs/core";
3610
+ var DEFAULT_CHUNK_SIZE = 256 * 1024;
3611
+ async function chunkBlob(bytes, chunkSize = DEFAULT_CHUNK_SIZE, metadata = {}) {
3612
+ if (chunkSize <= 0) {
3613
+ throw new Error(`chunkBlob requires a positive chunkSize, got ${chunkSize}`);
3614
+ }
3615
+ const chunkHashes = [];
3616
+ const chunks = /* @__PURE__ */ new Map();
3617
+ for (let offset = 0; offset < bytes.byteLength; offset += chunkSize) {
3618
+ const end = Math.min(offset + chunkSize, bytes.byteLength);
3619
+ const chunk = new Uint8Array(end - offset);
3620
+ chunk.set(bytes.subarray(offset, end));
3621
+ const hash = await hashBlob2(chunk);
3622
+ chunkHashes.push(hash);
3623
+ if (!chunks.has(hash)) {
3624
+ chunks.set(hash, chunk);
3625
+ }
3626
+ }
3627
+ const blobHash = await hashBlob2(bytes);
3628
+ const manifest = {
3629
+ blobHash,
3630
+ size: bytes.byteLength,
3631
+ chunkSize,
3632
+ chunkHashes
3633
+ };
3634
+ if (metadata.mimeType !== void 0) {
3635
+ manifest.mimeType = metadata.mimeType;
3636
+ }
3637
+ if (metadata.filename !== void 0) {
3638
+ manifest.filename = metadata.filename;
3639
+ }
3640
+ return { manifest, chunks };
3641
+ }
3642
+ async function reassembleBlob(manifest, chunkStore) {
3643
+ const parts = [];
3644
+ let total = 0;
3645
+ for (const hash of manifest.chunkHashes) {
3646
+ const chunk = await chunkStore.get(hash);
3647
+ if (chunk === null) {
3648
+ throw new Error(`Cannot reassemble blob ${manifest.blobHash}: missing chunk ${hash}`);
3649
+ }
3650
+ parts.push(chunk);
3651
+ total += chunk.byteLength;
3652
+ }
3653
+ const out = new Uint8Array(total);
3654
+ let offset = 0;
3655
+ for (const part of parts) {
3656
+ out.set(part, offset);
3657
+ offset += part.byteLength;
3658
+ }
3659
+ const actual = await hashBlob2(out);
3660
+ if (actual !== manifest.blobHash) {
3661
+ throw new BlobIntegrityError(manifest.blobHash, actual);
3662
+ }
3663
+ return out;
3664
+ }
3665
+
3666
+ // src/blob/blob-transfer.ts
3667
+ import { hashBlob as hashBlob3 } from "@korajs/core";
3668
+ async function receiveBlob(manifest, provider, stores) {
3669
+ let chunksFetched = 0;
3670
+ let chunksSkipped = 0;
3671
+ const seen = /* @__PURE__ */ new Set();
3672
+ for (const hash of manifest.chunkHashes) {
3673
+ if (seen.has(hash)) {
3674
+ continue;
3675
+ }
3676
+ seen.add(hash);
3677
+ if (await stores.chunkStore.has(hash)) {
3678
+ chunksSkipped++;
3679
+ continue;
3680
+ }
3681
+ const bytes = await provider.getChunk(hash);
3682
+ if (bytes === null) {
3683
+ throw new Error(`Blob transfer failed: provider could not supply chunk ${hash}`);
3684
+ }
3685
+ const actual = await hashBlob3(bytes);
3686
+ if (actual !== hash) {
3687
+ throw new BlobIntegrityError(hash, actual);
3688
+ }
3689
+ await stores.chunkStore.put(bytes);
3690
+ chunksFetched++;
3691
+ }
3692
+ const full = await reassembleBlob(manifest, stores.chunkStore);
3693
+ const ref = await stores.blobStore.put(full, {
3694
+ mimeType: manifest.mimeType,
3695
+ filename: manifest.filename
3696
+ });
3697
+ return { ref, chunksFetched, chunksSkipped };
3698
+ }
3699
+ async function prepareBlobForSend(bytes, chunkStore, options = {}) {
3700
+ const { manifest, chunks } = await chunkBlob(bytes, options.chunkSize, {
3701
+ mimeType: options.mimeType,
3702
+ filename: options.filename
3703
+ });
3704
+ for (const chunk of chunks.values()) {
3705
+ await chunkStore.put(chunk);
3706
+ }
3707
+ const provider = {
3708
+ getChunk: (hash) => chunkStore.get(hash)
3709
+ };
3710
+ return { manifest, provider };
3711
+ }
3712
+
3713
+ // src/blob/blob-manifest-transfer.ts
3714
+ import { hashBlob as hashBlob4 } from "@korajs/core";
3715
+ function serializeBlobManifest(manifest) {
3716
+ const canonical = {
3717
+ blobHash: manifest.blobHash,
3718
+ size: manifest.size,
3719
+ chunkSize: manifest.chunkSize,
3720
+ chunkHashes: manifest.chunkHashes
3721
+ };
3722
+ if (manifest.mimeType !== void 0) {
3723
+ canonical.mimeType = manifest.mimeType;
3724
+ }
3725
+ if (manifest.filename !== void 0) {
3726
+ canonical.filename = manifest.filename;
3727
+ }
3728
+ return new TextEncoder().encode(JSON.stringify(canonical));
3729
+ }
3730
+ function parseBlobManifest(bytes) {
3731
+ const parsed = JSON.parse(new TextDecoder().decode(bytes));
3732
+ if (typeof parsed !== "object" || parsed === null) {
3733
+ throw new Error("Invalid blob manifest: not an object");
3734
+ }
3735
+ const obj = parsed;
3736
+ if (typeof obj.blobHash !== "string") {
3737
+ throw new Error("Invalid blob manifest: missing blobHash");
3738
+ }
3739
+ if (typeof obj.size !== "number" || typeof obj.chunkSize !== "number") {
3740
+ throw new Error("Invalid blob manifest: missing size or chunkSize");
3741
+ }
3742
+ if (!Array.isArray(obj.chunkHashes) || obj.chunkHashes.some((h) => typeof h !== "string")) {
3743
+ throw new Error("Invalid blob manifest: chunkHashes must be an array of strings");
3744
+ }
3745
+ const manifest = {
3746
+ blobHash: obj.blobHash,
3747
+ size: obj.size,
3748
+ chunkSize: obj.chunkSize,
3749
+ chunkHashes: obj.chunkHashes
3750
+ };
3751
+ if (typeof obj.mimeType === "string") {
3752
+ manifest.mimeType = obj.mimeType;
3753
+ }
3754
+ if (typeof obj.filename === "string") {
3755
+ manifest.filename = obj.filename;
3756
+ }
3757
+ return manifest;
3758
+ }
3759
+ async function putBlobForTransfer(store, bytes, options = {}) {
3760
+ const { manifest, chunks } = await chunkBlob(bytes, options.chunkSize, {
3761
+ ...options.mimeType !== void 0 ? { mimeType: options.mimeType } : {},
3762
+ ...options.filename !== void 0 ? { filename: options.filename } : {}
3763
+ });
3764
+ for (const chunk of chunks.values()) {
3765
+ await store.put(chunk);
3766
+ }
3767
+ const baseRef = await store.put(bytes, {
3768
+ ...options.mimeType !== void 0 ? { mimeType: options.mimeType } : {},
3769
+ ...options.filename !== void 0 ? { filename: options.filename } : {}
3770
+ });
3771
+ const manifestRef = await store.put(serializeBlobManifest(manifest));
3772
+ return { ref: { ...baseRef, manifestHash: manifestRef.hash }, manifest };
3773
+ }
3774
+ async function fetchBlobManifest(provider, manifestHash) {
3775
+ const bytes = await provider.getChunk(manifestHash);
3776
+ if (bytes === null) {
3777
+ throw new Error(`Blob manifest ${manifestHash} could not be fetched from the provider`);
3778
+ }
3779
+ const actual = await hashBlob4(bytes);
3780
+ if (actual !== manifestHash) {
3781
+ throw new BlobIntegrityError(manifestHash, actual);
3782
+ }
3783
+ return parseBlobManifest(bytes);
3784
+ }
3785
+ async function resolveBlobManifest(provider, ref) {
3786
+ if (ref.manifestHash === void 0) {
3787
+ throw new Error(
3788
+ `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.`
3789
+ );
3790
+ }
3791
+ return fetchBlobManifest(provider, ref.manifestHash);
3792
+ }
3793
+
3794
+ // src/blob/server-blob-callbacks.ts
3795
+ import { hashBlob as hashBlob5 } from "@korajs/core";
3796
+ function toServerBlobCallbacks(store) {
3797
+ return {
3798
+ resolveBlobChunk: (hash) => store.get(hash),
3799
+ async persistBlobChunk(hash, bytes) {
3800
+ const actual = await hashBlob5(bytes);
3801
+ if (actual !== hash) {
3802
+ return;
3803
+ }
3804
+ await store.put(bytes);
3805
+ }
3806
+ };
3807
+ }
3808
+ function createMemoryServerBlobStore() {
3809
+ const store = new MemoryBlobStore();
3810
+ return { store, callbacks: toServerBlobCallbacks(store) };
3811
+ }
3812
+
3813
+ // src/blob/blob-gc.ts
3814
+ import { isBlobRef } from "@korajs/core";
3815
+ function extractBlobRefs(record) {
3816
+ const refs = [];
3817
+ for (const value of Object.values(record)) {
3818
+ if (isBlobRef(value)) {
3819
+ refs.push(value);
3820
+ }
3821
+ }
3822
+ return refs;
3823
+ }
3824
+ async function collectBlobGarbage(store, liveRefs, options = {}) {
3825
+ const live = /* @__PURE__ */ new Set();
3826
+ for (const ref of liveRefs) {
3827
+ live.add(ref.hash);
3828
+ if (ref.manifestHash === void 0) {
3829
+ continue;
3830
+ }
3831
+ live.add(ref.manifestHash);
3832
+ try {
3833
+ const manifestBytes = await store.get(ref.manifestHash);
3834
+ if (manifestBytes !== null) {
3835
+ const manifest = parseBlobManifest(manifestBytes);
3836
+ for (const chunkHash of manifest.chunkHashes) {
3837
+ live.add(chunkHash);
3838
+ }
3839
+ }
3840
+ } catch {
3841
+ }
3842
+ }
3843
+ const all = await store.list();
3844
+ const collectedHashes = [];
3845
+ for (const hash of all) {
3846
+ if (live.has(hash)) {
3847
+ continue;
3848
+ }
3849
+ collectedHashes.push(hash);
3850
+ if (!options.dryRun) {
3851
+ await store.delete(hash);
3852
+ }
3853
+ }
3854
+ return {
3855
+ scanned: all.length,
3856
+ live: live.size,
3857
+ collected: collectedHashes.length,
3858
+ collectedHashes
3859
+ };
3860
+ }
3861
+
3862
+ // src/blob/blob-chunk-transport.ts
3863
+ import { generateUUIDv7 as generateUUIDv74 } from "@korajs/core";
3864
+ function createRemoteChunkProvider(port, options = {}) {
3865
+ const timeoutMs = options.timeoutMs ?? 3e4;
3866
+ const pending = /* @__PURE__ */ new Map();
3867
+ port.onMessage((message) => {
3868
+ if (message.type !== "blob-chunk-response") {
3869
+ return;
3870
+ }
3871
+ const entry = pending.get(message.requestId);
3872
+ if (!entry) {
3873
+ return;
3874
+ }
3875
+ pending.delete(message.requestId);
3876
+ clearTimeout(entry.timer);
3877
+ entry.resolve(message.bytes);
3878
+ });
3879
+ return {
3880
+ pendingCount: () => pending.size,
3881
+ getChunk(hash) {
3882
+ const requestId = generateUUIDv74();
3883
+ return new Promise((resolve, reject) => {
3884
+ const timer = setTimeout(() => {
3885
+ pending.delete(requestId);
3886
+ reject(new Error(`Blob chunk request for ${hash} timed out after ${timeoutMs}ms`));
3887
+ }, timeoutMs);
3888
+ pending.set(requestId, { resolve, reject, timer });
3889
+ port.send({ type: "blob-chunk-request", requestId, hash });
3890
+ });
3891
+ }
3892
+ };
3893
+ }
3894
+ function serveBlobChunks(port, blobStore) {
3895
+ port.onMessage((message) => {
3896
+ if (message.type !== "blob-chunk-request") {
3897
+ return;
3898
+ }
3899
+ void blobStore.get(message.hash).then(
3900
+ (bytes) => {
3901
+ port.send({ type: "blob-chunk-response", requestId: message.requestId, bytes });
3902
+ },
3903
+ () => {
3904
+ port.send({ type: "blob-chunk-response", requestId: message.requestId, bytes: null });
3905
+ }
3906
+ );
3907
+ });
3908
+ }
3909
+ function createChunkPortPair() {
3910
+ const handlersA = [];
3911
+ const handlersB = [];
3912
+ const a = {
3913
+ send(message) {
3914
+ queueMicrotask(() => {
3915
+ for (const handler of handlersB) {
3916
+ handler(message);
3917
+ }
3918
+ });
3919
+ },
3920
+ onMessage(handler) {
3921
+ handlersA.push(handler);
3922
+ }
3923
+ };
3924
+ const b = {
3925
+ send(message) {
3926
+ queueMicrotask(() => {
3927
+ for (const handler of handlersA) {
3928
+ handler(message);
3929
+ }
3930
+ });
3931
+ },
3932
+ onMessage(handler) {
3933
+ handlersB.push(handler);
3934
+ }
3935
+ };
3936
+ return { a, b };
3937
+ }
3047
3938
  export {
3048
3939
  AdapterError,
3940
+ BlobIntegrityError,
3049
3941
  COMPACTION_BASELINE_META_KEY,
3050
3942
  Collection,
3943
+ DEFAULT_CHUNK_SIZE,
3051
3944
  InvalidStateTransitionError,
3052
3945
  LAST_ACKED_SERVER_VECTOR_META_KEY,
3946
+ MemoryBlobStore,
3947
+ OpfsBlobStore,
3948
+ OptimisticLockError,
3053
3949
  PersistenceError,
3054
3950
  QueryBuilder,
3055
3951
  QueryError,
@@ -3066,26 +3962,44 @@ export {
3066
3962
  WorkerTimeoutError,
3067
3963
  asRichTextSyncEngine,
3068
3964
  assertQueryReady,
3965
+ chunkBlob,
3966
+ collectBlobGarbage,
3069
3967
  collectOperationsAheadOfServer,
3070
3968
  compactOperationLog,
3071
3969
  computeAckCompactionWatermark,
3970
+ createChunkPortPair,
3971
+ createMemoryServerBlobStore,
3972
+ createOpfsBlobDirectory,
3973
+ createOpfsBlobStore,
3974
+ createRemoteChunkProvider,
3072
3975
  createRichTextController,
3073
3976
  decodeAuditExport,
3074
3977
  decodeRichtext,
3075
3978
  deserializeVersionVectorFromMeta,
3076
3979
  encodeRichtext,
3077
3980
  exportBackup,
3981
+ extractBlobRefs,
3982
+ fetchBlobManifest,
3078
3983
  getSharedQueryStoreCache,
3079
3984
  mergeVersionVectors,
3985
+ parseBlobManifest,
3080
3986
  persistedAuditTraceFromEvent,
3081
3987
  pluralize,
3988
+ prepareBlobForSend,
3989
+ putBlobForTransfer,
3082
3990
  readAuditExportManifest,
3083
3991
  readBackupManifest,
3992
+ reassembleBlob,
3993
+ receiveBlob,
3994
+ resolveBlobManifest,
3084
3995
  restoreBackup,
3085
3996
  richtextStatesEqual,
3086
3997
  richtextToPlainText,
3998
+ serializeBlobManifest,
3087
3999
  serializeVersionVectorToMeta,
4000
+ serveBlobChunks,
3088
4001
  singularize,
4002
+ toServerBlobCallbacks,
3089
4003
  validateStateTransition,
3090
4004
  validateUpdateStateMachine,
3091
4005
  verifyAuditExportChecksum,