@korajs/store 0.5.0 → 1.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) 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-5XZ7THNJ.js} +3 -3
  20. package/dist/{chunk-EZSTD2XC.js → chunk-5R4QMY3Z.js} +2 -2
  21. package/dist/{chunk-AGN3CIPB.js → chunk-EIVXWF4B.js} +2 -2
  22. package/dist/{chunk-56LL4XP6.js → chunk-ETDJEH2I.js} +42 -1
  23. package/dist/chunk-ETDJEH2I.js.map +1 -0
  24. package/dist/{chunk-XO4LMPAH.js → chunk-IOEFNHHP.js} +2 -35
  25. package/dist/chunk-IOEFNHHP.js.map +1 -0
  26. package/dist/{chunk-7BPETY5Y.js → chunk-OUQDC5Z6.js} +105 -8
  27. package/dist/chunk-OUQDC5Z6.js.map +1 -0
  28. package/dist/{chunk-LAWV6CFH.js → chunk-QL2XVZRN.js} +12 -1
  29. package/dist/chunk-QL2XVZRN.js.map +1 -0
  30. package/dist/{chunk-6AHYGQQT.js → chunk-RBPVEAVM.js} +4 -4
  31. package/dist/chunk-TVJEC5S2.js +40 -0
  32. package/dist/chunk-TVJEC5S2.js.map +1 -0
  33. package/dist/{export-audit-5GV6VRDZ.js → export-audit-VPGQWNR6.js} +3 -3
  34. package/dist/index.cjs +1102 -104
  35. package/dist/index.cjs.map +1 -1
  36. package/dist/index.d.cts +219 -7
  37. package/dist/index.d.ts +219 -7
  38. package/dist/index.js +905 -44
  39. package/dist/index.js.map +1 -1
  40. package/dist/internal.cjs +100 -30
  41. package/dist/internal.cjs.map +1 -1
  42. package/dist/internal.d.cts +3 -3
  43. package/dist/internal.d.ts +3 -3
  44. package/dist/internal.js +3 -3
  45. package/dist/{sqlite-wasm-channel-I9y_jObg.d.cts → sqlite-wasm-channel-DUDPclTE.d.cts} +2 -15
  46. package/dist/{sqlite-wasm-channel-I9y_jObg.d.ts → sqlite-wasm-channel-DUDPclTE.d.ts} +2 -15
  47. package/dist/sqlite-wasm-channel-H43KEZLM.js +12 -0
  48. package/dist/{types-DepnRDk_.d.cts → types-CJOVKxLw.d.cts} +43 -2
  49. package/dist/{types-DepnRDk_.d.ts → types-CJOVKxLw.d.ts} +43 -2
  50. package/dist/{types-CTr00g_d.d.cts → types-CyjXwrjW.d.cts} +1 -1
  51. package/dist/{types-BMyHmwvn.d.ts → types-DAxCladl.d.ts} +1 -1
  52. package/package.json +4 -2
  53. package/dist/chunk-56LL4XP6.js.map +0 -1
  54. package/dist/chunk-7BPETY5Y.js.map +0 -1
  55. package/dist/chunk-LAWV6CFH.js.map +0 -1
  56. package/dist/chunk-XO4LMPAH.js.map +0 -1
  57. package/dist/sqlite-wasm-channel-HDDW6UBH.js +0 -10
  58. /package/dist/{backup-JN3LB4YT.js.map → backup-5XZ7THNJ.js.map} +0 -0
  59. /package/dist/{chunk-EZSTD2XC.js.map → chunk-5R4QMY3Z.js.map} +0 -0
  60. /package/dist/{chunk-AGN3CIPB.js.map → chunk-EIVXWF4B.js.map} +0 -0
  61. /package/dist/{chunk-6AHYGQQT.js.map → chunk-RBPVEAVM.js.map} +0 -0
  62. /package/dist/{export-audit-5GV6VRDZ.js.map → export-audit-VPGQWNR6.js.map} +0 -0
  63. /package/dist/{sqlite-wasm-channel-HDDW6UBH.js.map → sqlite-wasm-channel-H43KEZLM.js.map} +0 -0
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  decodeAuditExport,
3
3
  readAuditExportManifest,
4
4
  verifyAuditExportChecksum
5
- } from "./chunk-AGN3CIPB.js";
5
+ } from "./chunk-EIVXWF4B.js";
6
6
  import {
7
7
  persistedAuditTraceFromEvent
8
8
  } from "./chunk-MYSJN3DF.js";
@@ -11,46 +11,53 @@ import {
11
11
  allocateNextSequenceInTransaction,
12
12
  allocateNextSequenceNumber,
13
13
  buildCountQuery,
14
+ buildFieldFastForwardUpdateQuery,
14
15
  buildInsertQuery,
15
16
  buildLwwSoftDeleteQuery,
16
- buildLwwUpdateQuery,
17
17
  buildSelectQuery,
18
18
  buildSoftDeleteQuery,
19
19
  buildUpdateQuery,
20
20
  executeDelete,
21
21
  executeInsert,
22
22
  executeUpdate,
23
+ fieldVersionsForFields,
23
24
  isIncomingNewerThanRow,
25
+ parseFieldVersions,
24
26
  readSequenceNumber,
27
+ resolvePerFieldLww,
28
+ serializeFieldVersions,
25
29
  serializeRowVersion,
26
30
  validateStateTransition,
27
31
  validateUpdateStateMachine
28
- } from "./chunk-7BPETY5Y.js";
32
+ } from "./chunk-OUQDC5Z6.js";
29
33
  import {
30
34
  AdapterError,
35
+ OptimisticLockError,
31
36
  PersistenceError,
32
37
  QueryError,
33
38
  RecordNotFoundError,
34
39
  StoreNotOpenError,
35
40
  WorkerInitError,
36
41
  WorkerTimeoutError
37
- } from "./chunk-LAWV6CFH.js";
42
+ } from "./chunk-QL2XVZRN.js";
38
43
  import {
39
44
  exportBackup,
40
45
  readBackupManifest,
41
46
  restoreBackup,
42
47
  verifyBackupChecksum
43
- } from "./chunk-EZSTD2XC.js";
48
+ } from "./chunk-5R4QMY3Z.js";
44
49
  import {
45
50
  decodeRichtext,
51
+ decodeRichtextFieldsFromOpData,
46
52
  deserializeOperationWithCollection,
47
53
  deserializeRecord,
48
54
  encodeRichtext,
55
+ encodeRichtextFieldsForOpData,
49
56
  richtextStatesEqual,
50
57
  richtextToPlainText,
51
58
  serializeOperation,
52
59
  serializeRecord
53
- } from "./chunk-56LL4XP6.js";
60
+ } from "./chunk-ETDJEH2I.js";
54
61
 
55
62
  // src/compaction/types.ts
56
63
  var COMPACTION_BASELINE_META_KEY = "compaction_baseline_at";
@@ -195,7 +202,7 @@ async function saveDeltaCursor(adapter, cursor) {
195
202
  // src/store/store.ts
196
203
  import {
197
204
  CausalTracker,
198
- HybridLogicalClock as HybridLogicalClock2,
205
+ HybridLogicalClock as HybridLogicalClock3,
199
206
  createVersionVector as createVersionVector3,
200
207
  generateUUIDv7 as generateUUIDv73,
201
208
  migrationStepsToSQL
@@ -885,7 +892,9 @@ function applyOperationToMemory(state, op, schema) {
885
892
  }
886
893
  colMap.set(op.recordId, {
887
894
  id: op.recordId,
888
- fields: { ...op.data },
895
+ // op.data stores binary richtext as tagged JSON; snapshots must
896
+ // expose record-shaped values (Uint8Array/string).
897
+ fields: decodeRichtextFieldsFromOpData(op.data, definition.fields),
889
898
  deleted: false,
890
899
  createdAt: wallTime,
891
900
  updatedAt: wallTime
@@ -896,9 +905,10 @@ function applyOperationToMemory(state, op, schema) {
896
905
  if (!op.data) {
897
906
  return;
898
907
  }
908
+ const decodedData = decodeRichtextFieldsFromOpData(op.data, definition.fields);
899
909
  const existing = colMap.get(op.recordId);
900
910
  if (existing && !existing.deleted) {
901
- existing.fields = { ...existing.fields, ...op.data };
911
+ existing.fields = { ...existing.fields, ...decodedData };
902
912
  existing.updatedAt = wallTime;
903
913
  break;
904
914
  }
@@ -907,7 +917,7 @@ function applyOperationToMemory(state, op, schema) {
907
917
  }
908
918
  colMap.set(op.recordId, {
909
919
  id: op.recordId,
910
- fields: { ...op.data },
920
+ fields: decodedData,
911
921
  deleted: false,
912
922
  createdAt: wallTime,
913
923
  updatedAt: wallTime
@@ -1458,6 +1468,137 @@ var SubscriptionManager = class {
1458
1468
  }
1459
1469
  };
1460
1470
 
1471
+ // src/sync/rebase-unsynced-operations.ts
1472
+ import { HybridLogicalClock as HybridLogicalClock2, MAX_LOGICAL } from "@korajs/core";
1473
+ import { computeOperationId } from "@korajs/core/internal";
1474
+ var LOGICAL_COUNTER_LIMIT = MAX_LOGICAL + 1;
1475
+ function emptyResult() {
1476
+ return { operations: [], idMapping: {}, rebasedCount: 0, newMaxTimestamp: null };
1477
+ }
1478
+ async function rebaseUnsyncedOperationsInLog(adapter, schema, unsyncedOpIds, correctedNowMs) {
1479
+ if (unsyncedOpIds.length === 0) {
1480
+ return emptyResult();
1481
+ }
1482
+ const rebaseIds = new Set(unsyncedOpIds);
1483
+ const rebased = [];
1484
+ let maxOtherSerializedTs = null;
1485
+ for (const collectionName of Object.keys(schema.collections)) {
1486
+ const rows = await adapter.query(`SELECT * FROM _kora_ops_${collectionName}`);
1487
+ for (const row of rows) {
1488
+ if (rebaseIds.has(row.id)) {
1489
+ rebased.push(deserializeOperationWithCollection(row, collectionName));
1490
+ } else if (maxOtherSerializedTs === null || row.timestamp > maxOtherSerializedTs) {
1491
+ maxOtherSerializedTs = row.timestamp;
1492
+ }
1493
+ }
1494
+ }
1495
+ if (rebased.length === 0) {
1496
+ return emptyResult();
1497
+ }
1498
+ const maxOtherWallTime = maxOtherSerializedTs === null ? Number.NEGATIVE_INFINITY : HybridLogicalClock2.deserialize(maxOtherSerializedTs).wallTime;
1499
+ const baseWallTime = Math.max(correctedNowMs, maxOtherWallTime + 1);
1500
+ rebased.sort((a, b) => HybridLogicalClock2.compare(a.timestamp, b.timestamp));
1501
+ const newTimestamps = rebased.map((op, i) => ({
1502
+ // Spill into wallTime past the 5-digit logical serialization limit so the
1503
+ // serialized order stays correct even for enormous offline queues.
1504
+ wallTime: baseWallTime + Math.floor(i / LOGICAL_COUNTER_LIMIT),
1505
+ logical: i % LOGICAL_COUNTER_LIMIT,
1506
+ nodeId: op.nodeId
1507
+ }));
1508
+ const idMapping = {};
1509
+ const newIds = [];
1510
+ for (let i = 0; i < rebased.length; i++) {
1511
+ const op = rebased[i];
1512
+ const ts = newTimestamps[i];
1513
+ if (!op || !ts) {
1514
+ continue;
1515
+ }
1516
+ const input = {
1517
+ nodeId: op.nodeId,
1518
+ type: op.type,
1519
+ collection: op.collection,
1520
+ recordId: op.recordId,
1521
+ data: op.data,
1522
+ previousData: op.previousData,
1523
+ sequenceNumber: op.sequenceNumber,
1524
+ causalDeps: op.causalDeps,
1525
+ schemaVersion: op.schemaVersion,
1526
+ ...op.atomicOps !== void 0 ? { atomicOps: op.atomicOps } : {}
1527
+ };
1528
+ const newId = await computeOperationId(input, HybridLogicalClock2.serialize(ts));
1529
+ idMapping[op.id] = newId;
1530
+ newIds.push(newId);
1531
+ }
1532
+ const newOperations = [];
1533
+ for (let i = 0; i < rebased.length; i++) {
1534
+ const op = rebased[i];
1535
+ const ts = newTimestamps[i];
1536
+ const newId = newIds[i];
1537
+ if (!op || !ts || newId === void 0) {
1538
+ continue;
1539
+ }
1540
+ newOperations.push({
1541
+ ...op,
1542
+ id: newId,
1543
+ timestamp: ts,
1544
+ causalDeps: op.causalDeps.map((dep) => idMapping[dep] ?? dep)
1545
+ });
1546
+ }
1547
+ await adapter.transaction(async (tx) => {
1548
+ for (let i = 0; i < rebased.length; i++) {
1549
+ const oldOp = rebased[i];
1550
+ const newOp = newOperations[i];
1551
+ if (!oldOp || !newOp) {
1552
+ continue;
1553
+ }
1554
+ const opsTable = `_kora_ops_${oldOp.collection}`;
1555
+ await tx.execute(`DELETE FROM ${opsTable} WHERE id = ?`, [oldOp.id]);
1556
+ const opInsert = buildInsertQuery(
1557
+ opsTable,
1558
+ serializeOperation(newOp)
1559
+ );
1560
+ await tx.execute(opInsert.sql, opInsert.params);
1561
+ const oldVersion = serializeRowVersion(oldOp.timestamp);
1562
+ const newVersion = serializeRowVersion(newOp.timestamp);
1563
+ await tx.execute(
1564
+ `UPDATE ${oldOp.collection} SET _version = ?, _updated_at = ? WHERE id = ? AND _version = ?`,
1565
+ [newVersion, newOp.timestamp.wallTime, oldOp.recordId, oldVersion]
1566
+ );
1567
+ const rows = await tx.query(
1568
+ `SELECT _field_versions FROM ${oldOp.collection} WHERE id = ?`,
1569
+ [oldOp.recordId]
1570
+ );
1571
+ const fieldVersions = parseFieldVersions(rows[0]?._field_versions);
1572
+ let changed = false;
1573
+ for (const [field, version] of Object.entries(fieldVersions)) {
1574
+ if (version === oldVersion) {
1575
+ fieldVersions[field] = newVersion;
1576
+ changed = true;
1577
+ }
1578
+ }
1579
+ if (changed) {
1580
+ await tx.execute(`UPDATE ${oldOp.collection} SET _field_versions = ? WHERE id = ?`, [
1581
+ serializeFieldVersions(fieldVersions),
1582
+ oldOp.recordId
1583
+ ]);
1584
+ }
1585
+ if (oldOp.type === "insert") {
1586
+ await tx.execute(
1587
+ `UPDATE ${oldOp.collection} SET _created_at = ? WHERE id = ? AND _created_at = ?`,
1588
+ [newOp.timestamp.wallTime, oldOp.recordId, oldOp.timestamp.wallTime]
1589
+ );
1590
+ }
1591
+ }
1592
+ });
1593
+ const newMaxTimestamp = newTimestamps[newTimestamps.length - 1] ?? null;
1594
+ return {
1595
+ operations: newOperations,
1596
+ idMapping,
1597
+ rebasedCount: newOperations.length,
1598
+ newMaxTimestamp
1599
+ };
1600
+ }
1601
+
1461
1602
  // src/transaction/transaction-context.ts
1462
1603
  import {
1463
1604
  createOperation as createOperation2,
@@ -1607,7 +1748,9 @@ var TransactionContext = class {
1607
1748
  type: "insert",
1608
1749
  collection: collectionName,
1609
1750
  recordId,
1610
- data: { ...validated },
1751
+ // Binary richtext values are tagged as canonical JSON before the
1752
+ // operation is content-hashed (see encodeRichtextFieldsForOpData).
1753
+ data: encodeRichtextFieldsForOpData(validated, definition.fields),
1611
1754
  previousData: null,
1612
1755
  sequenceNumber,
1613
1756
  causalDeps,
@@ -1678,8 +1821,10 @@ var TransactionContext = class {
1678
1821
  type: "update",
1679
1822
  collection: collectionName,
1680
1823
  recordId: id,
1681
- data: { ...resolvedData },
1682
- previousData,
1824
+ // Binary richtext values (new and previous) are tagged as canonical
1825
+ // JSON before the operation is content-hashed.
1826
+ data: encodeRichtextFieldsForOpData(resolvedData, definition.fields),
1827
+ previousData: encodeRichtextFieldsForOpData(previousData, definition.fields),
1683
1828
  sequenceNumber,
1684
1829
  causalDeps,
1685
1830
  schemaVersion: this.config.schema.version,
@@ -1786,14 +1931,16 @@ var TransactionContext = class {
1786
1931
  if (entry.operation.type === "insert" && entry.operation.data) {
1787
1932
  record = {
1788
1933
  id,
1789
- ...entry.operation.data,
1934
+ // op.data stores binary richtext as tagged JSON; the effective
1935
+ // record must expose record-shaped values (Uint8Array/string).
1936
+ ...decodeRichtextFieldsFromOpData(entry.operation.data, definition.fields),
1790
1937
  createdAt: entry.operation.timestamp.wallTime,
1791
1938
  updatedAt: entry.operation.timestamp.wallTime
1792
1939
  };
1793
1940
  } else if (entry.operation.type === "update" && entry.operation.data && record) {
1794
1941
  record = {
1795
1942
  ...record,
1796
- ...entry.operation.data,
1943
+ ...decodeRichtextFieldsFromOpData(entry.operation.data, definition.fields),
1797
1944
  updatedAt: entry.operation.timestamp.wallTime
1798
1945
  };
1799
1946
  } else if (entry.operation.type === "delete") {
@@ -1893,7 +2040,7 @@ var Store = class {
1893
2040
  await this.adapter.open(this.schema);
1894
2041
  await this.runMigrationsIfNeeded();
1895
2042
  this.nodeId = await this.loadOrGenerateNodeId();
1896
- this.clock = new HybridLogicalClock2(this.nodeId);
2043
+ this.clock = new HybridLogicalClock3(this.nodeId);
1897
2044
  this.causalTracker = new CausalTracker();
1898
2045
  this.sequenceManager = new SequenceManager(this.adapter, this.nodeId);
1899
2046
  this.sequenceNumber = await this.loadSequenceNumber();
@@ -1980,6 +2127,14 @@ var Store = class {
1980
2127
  /**
1981
2128
  * Get the node ID for this store instance.
1982
2129
  */
2130
+ /**
2131
+ * Records the offset between server time and this device's clock
2132
+ * (serverTime - localTime), learned at sync handshake. Lets the store's HLC
2133
+ * validate remote timestamps correctly even when the local clock is wrong.
2134
+ */
2135
+ setClockReferenceOffset(offsetMs) {
2136
+ this.clock?.setReferenceOffset(offsetMs);
2137
+ }
1983
2138
  getNodeId() {
1984
2139
  this.ensureOpen();
1985
2140
  return this.nodeId;
@@ -2006,45 +2161,158 @@ var Store = class {
2006
2161
  if (this.clock) {
2007
2162
  this.clock.receive(op.timestamp);
2008
2163
  }
2009
- const remoteVersion = serializeRowVersion(op.timestamp);
2010
- const wallTime = op.timestamp.wallTime;
2164
+ const materializeTimestamp = options?.materializeTimestamp ?? op.timestamp;
2165
+ const remoteVersion = serializeRowVersion(materializeTimestamp);
2166
+ const wallTime = materializeTimestamp.wallTime;
2167
+ const materializeSource = options?.materializeData ?? op.data;
2011
2168
  await this.adapter.transaction(async (tx) => {
2012
- if (op.type === "insert" && op.data) {
2169
+ const checkGuard = (row) => {
2170
+ const guard = options?.guardRowState;
2171
+ if (!guard) {
2172
+ return;
2173
+ }
2174
+ const version = typeof row?._version === "string" ? row._version : null;
2175
+ const fieldVersions = typeof row?._field_versions === "string" ? row._field_versions : null;
2176
+ if (version !== guard.version || fieldVersions !== guard.fieldVersions) {
2177
+ throw new OptimisticLockError(collection, op.recordId);
2178
+ }
2179
+ };
2180
+ if (op.type === "insert" && materializeSource) {
2181
+ const serializedData = serializeRecord(materializeSource, definition.fields);
2013
2182
  const existing2 = await tx.query(
2014
- `SELECT _updated_at, _version FROM ${collection} WHERE id = ?`,
2183
+ `SELECT _updated_at, _version, _field_versions, _deleted FROM ${collection} WHERE id = ?`,
2015
2184
  [op.recordId]
2016
2185
  );
2017
2186
  const row = existing2[0];
2018
- if (row && !isIncomingNewerThanRow(op.timestamp, row)) {
2019
- } else {
2020
- const serializedData = serializeRecord(op.data, definition.fields);
2187
+ checkGuard(row);
2188
+ if (!row) {
2021
2189
  const record = {
2022
2190
  id: op.recordId,
2023
2191
  ...serializedData,
2024
2192
  _created_at: wallTime,
2025
2193
  _updated_at: wallTime,
2026
- _version: remoteVersion
2194
+ _version: remoteVersion,
2195
+ // Stamp every inserted field so per-field LWW has a real baseline.
2196
+ _field_versions: serializeFieldVersions(
2197
+ fieldVersionsForFields(Object.keys(serializedData), remoteVersion)
2198
+ )
2027
2199
  };
2028
2200
  const insertQuery = buildInsertQuery(collection, record);
2029
2201
  await tx.execute(insertQuery.sql, insertQuery.params);
2202
+ await this.foldOrphanedOperations(tx, collection, definition, op.recordId);
2203
+ } else if (row._deleted === 1) {
2204
+ if (isIncomingNewerThanRow(materializeTimestamp, row)) {
2205
+ const fieldChanges = {
2206
+ ...serializedData,
2207
+ _deleted: 0,
2208
+ _field_versions: serializeFieldVersions(
2209
+ fieldVersionsForFields(Object.keys(serializedData), remoteVersion)
2210
+ )
2211
+ };
2212
+ const upsert = buildFieldFastForwardUpdateQuery(
2213
+ collection,
2214
+ op.recordId,
2215
+ fieldChanges,
2216
+ remoteVersion,
2217
+ wallTime,
2218
+ { maxCreatedAt: wallTime }
2219
+ );
2220
+ await tx.execute(upsert.sql, upsert.params);
2221
+ }
2222
+ } else {
2223
+ const currentVersions = parseFieldVersions(row._field_versions);
2224
+ const rowVersion = typeof row._version === "string" ? row._version : void 0;
2225
+ const fieldChanges = {};
2226
+ let fieldVersionsJson;
2227
+ if (options?.forceMaterialize) {
2228
+ const merged = { ...currentVersions };
2229
+ for (const field of Object.keys(serializedData)) {
2230
+ fieldChanges[field] = serializedData[field];
2231
+ merged[field] = remoteVersion;
2232
+ }
2233
+ fieldVersionsJson = serializeFieldVersions(merged);
2234
+ } else {
2235
+ const { winners, merged } = resolvePerFieldLww(
2236
+ currentVersions,
2237
+ Object.keys(serializedData),
2238
+ remoteVersion,
2239
+ rowVersion
2240
+ );
2241
+ for (const field of winners) {
2242
+ fieldChanges[field] = serializedData[field];
2243
+ }
2244
+ fieldVersionsJson = serializeFieldVersions(merged);
2245
+ }
2246
+ const upsert = buildFieldFastForwardUpdateQuery(
2247
+ collection,
2248
+ op.recordId,
2249
+ { ...fieldChanges, _field_versions: fieldVersionsJson },
2250
+ remoteVersion,
2251
+ wallTime,
2252
+ { maxCreatedAt: wallTime }
2253
+ );
2254
+ await tx.execute(upsert.sql, upsert.params);
2030
2255
  }
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
2256
+ } else if (op.type === "update" && materializeSource) {
2257
+ const serializedChanges = serializeRecord(materializeSource, definition.fields);
2258
+ const changedFields = Object.keys(serializedChanges);
2259
+ const currentRows = await tx.query(
2260
+ `SELECT _version, _field_versions FROM ${collection} WHERE id = ?`,
2261
+ [op.recordId]
2046
2262
  );
2047
- await tx.execute(updateQuery.sql, updateQuery.params);
2263
+ const currentRow = currentRows[0];
2264
+ checkGuard(currentRow);
2265
+ if (currentRow) {
2266
+ const currentVersions = parseFieldVersions(currentRow._field_versions);
2267
+ const rowVersion = typeof currentRow._version === "string" ? currentRow._version : void 0;
2268
+ if (options?.forceMaterialize) {
2269
+ const merged = { ...currentVersions };
2270
+ for (const field of changedFields) {
2271
+ merged[field] = remoteVersion;
2272
+ }
2273
+ const fieldChanges = {
2274
+ ...serializedChanges,
2275
+ _field_versions: serializeFieldVersions(merged)
2276
+ };
2277
+ if (options?.reactivateIfDeleted) {
2278
+ fieldChanges._deleted = 0;
2279
+ }
2280
+ const forceQuery = buildFieldFastForwardUpdateQuery(
2281
+ collection,
2282
+ op.recordId,
2283
+ fieldChanges,
2284
+ remoteVersion,
2285
+ wallTime
2286
+ );
2287
+ await tx.execute(forceQuery.sql, forceQuery.params);
2288
+ } else {
2289
+ const { winners, merged } = resolvePerFieldLww(
2290
+ currentVersions,
2291
+ changedFields,
2292
+ remoteVersion,
2293
+ rowVersion
2294
+ );
2295
+ if (winners.length > 0 || options?.reactivateIfDeleted) {
2296
+ const winningChanges = {
2297
+ _field_versions: serializeFieldVersions(merged)
2298
+ };
2299
+ for (const field of winners) {
2300
+ winningChanges[field] = serializedChanges[field];
2301
+ }
2302
+ if (options?.reactivateIfDeleted) {
2303
+ winningChanges._deleted = 0;
2304
+ }
2305
+ const forceQuery = buildFieldFastForwardUpdateQuery(
2306
+ collection,
2307
+ op.recordId,
2308
+ winningChanges,
2309
+ remoteVersion,
2310
+ wallTime
2311
+ );
2312
+ await tx.execute(forceQuery.sql, forceQuery.params);
2313
+ }
2314
+ }
2315
+ }
2048
2316
  } else if (op.type === "delete") {
2049
2317
  const deleteQuery = buildLwwSoftDeleteQuery(
2050
2318
  collection,
@@ -2072,6 +2340,67 @@ var Store = class {
2072
2340
  this.subscriptionManager.notify(collection, op);
2073
2341
  return "applied";
2074
2342
  }
2343
+ /**
2344
+ * Materialize update/delete operations that were logged for a record BEFORE
2345
+ * its insert arrived (reordered delivery). Runs inside the insert's write
2346
+ * transaction, folding each orphan in timestamp order through the exact
2347
+ * per-field LWW / LWW-delete rules the normal apply paths use, so the final
2348
+ * row state equals what a device that received the operations in causal
2349
+ * order computed. Called only when the insert created a previously-absent
2350
+ * row — in that case every logged update/delete for the record is
2351
+ * necessarily an orphan (rows are soft-deleted, never removed, so a row
2352
+ * that is absent now was never materialized).
2353
+ */
2354
+ async foldOrphanedOperations(tx, collection, definition, recordId) {
2355
+ const orphanRows = await tx.query(
2356
+ `SELECT * FROM _kora_ops_${collection} WHERE record_id = ? AND type IN ('update', 'delete')`,
2357
+ [recordId]
2358
+ );
2359
+ if (orphanRows.length === 0) {
2360
+ return;
2361
+ }
2362
+ const orphans = orphanRows.map((r) => deserializeOperationWithCollection(r, collection)).sort((a, b) => HybridLogicalClock3.compare(a.timestamp, b.timestamp));
2363
+ for (const orphan of orphans) {
2364
+ const orphanVersion = serializeRowVersion(orphan.timestamp);
2365
+ const orphanWall = orphan.timestamp.wallTime;
2366
+ if (orphan.type === "update" && orphan.data) {
2367
+ const orphanChanges = serializeRecord(orphan.data, definition.fields);
2368
+ const rows = await tx.query(
2369
+ `SELECT _version, _field_versions FROM ${collection} WHERE id = ?`,
2370
+ [recordId]
2371
+ );
2372
+ const current = rows[0];
2373
+ if (!current) {
2374
+ continue;
2375
+ }
2376
+ const { winners, merged } = resolvePerFieldLww(
2377
+ parseFieldVersions(current._field_versions),
2378
+ Object.keys(orphanChanges),
2379
+ orphanVersion,
2380
+ typeof current._version === "string" ? current._version : void 0
2381
+ );
2382
+ if (winners.length > 0) {
2383
+ const winningChanges = {
2384
+ _field_versions: serializeFieldVersions(merged)
2385
+ };
2386
+ for (const field of winners) {
2387
+ winningChanges[field] = orphanChanges[field];
2388
+ }
2389
+ const query = buildFieldFastForwardUpdateQuery(
2390
+ collection,
2391
+ recordId,
2392
+ winningChanges,
2393
+ orphanVersion,
2394
+ orphanWall
2395
+ );
2396
+ await tx.execute(query.sql, query.params);
2397
+ }
2398
+ } else if (orphan.type === "delete") {
2399
+ const query = buildLwwSoftDeleteQuery(collection, recordId, orphanWall, orphanVersion);
2400
+ await tx.execute(query.sql, query.params);
2401
+ }
2402
+ }
2403
+ }
2075
2404
  /**
2076
2405
  * Get operations from a node within a sequence number range.
2077
2406
  * Implements the OperationLog interface for computeDelta.
@@ -2157,7 +2486,7 @@ var Store = class {
2157
2486
  */
2158
2487
  async exportAudit(options) {
2159
2488
  this.ensureOpen();
2160
- const { exportAudit: doExport } = await import("./export-audit-5GV6VRDZ.js");
2489
+ const { exportAudit: doExport } = await import("./export-audit-VPGQWNR6.js");
2161
2490
  return doExport(this.adapter, this.schema, this.nodeId, this.schema.version, options);
2162
2491
  }
2163
2492
  /**
@@ -2227,6 +2556,31 @@ var Store = class {
2227
2556
  deleted: row._deleted === 1
2228
2557
  };
2229
2558
  }
2559
+ /**
2560
+ * Raw version state of a materialized row, for optimistic-concurrency guarded
2561
+ * applies: capture this snapshot before computing a merge, pass it as
2562
+ * `guardRowState`, and the apply refuses to write if the row changed since.
2563
+ * Returns null when no row exists (which is itself a valid guard state — the
2564
+ * apply then requires the row to still be absent).
2565
+ */
2566
+ async getRowVersionState(collection, recordId) {
2567
+ this.ensureOpen();
2568
+ if (!this.schema.collections[collection]) {
2569
+ return null;
2570
+ }
2571
+ const rows = await this.adapter.query(
2572
+ `SELECT _version, _field_versions FROM ${collection} WHERE id = ?`,
2573
+ [recordId]
2574
+ );
2575
+ const row = rows[0];
2576
+ if (!row) {
2577
+ return null;
2578
+ }
2579
+ return {
2580
+ version: typeof row._version === "string" ? row._version : null,
2581
+ fieldVersions: typeof row._field_versions === "string" ? row._field_versions : null
2582
+ };
2583
+ }
2230
2584
  /**
2231
2585
  * Latest operation from this device for a record (used for delete-vs-update merge on sync).
2232
2586
  */
@@ -2269,6 +2623,30 @@ var Store = class {
2269
2623
  (nodeId, fromSeq, toSeq) => this.getOperationRange(nodeId, fromSeq, toSeq)
2270
2624
  );
2271
2625
  }
2626
+ /**
2627
+ * Re-stamp never-acknowledged local operations after a fast device clock was
2628
+ * corrected, so sync can resume immediately. Safe because unacknowledged
2629
+ * operations are private to this device (like unpushed git commits);
2630
+ * acknowledged/shared operations are immutable and never touched.
2631
+ *
2632
+ * After the rewrite the store's HLC is advanced past the highest new
2633
+ * timestamp so subsequent local writes keep sorting after the rebased ops.
2634
+ * No data-change notifications are emitted: materialized values are
2635
+ * unchanged, only version stamps move.
2636
+ */
2637
+ async rebaseUnsyncedOperations(unsyncedOpIds, correctedNowMs) {
2638
+ this.ensureOpen();
2639
+ const result = await rebaseUnsyncedOperationsInLog(
2640
+ this.adapter,
2641
+ this.schema,
2642
+ unsyncedOpIds,
2643
+ correctedNowMs
2644
+ );
2645
+ if (result.newMaxTimestamp && this.clock) {
2646
+ this.clock.advanceTo(result.newMaxTimestamp);
2647
+ }
2648
+ return result;
2649
+ }
2272
2650
  /**
2273
2651
  * Count of local operations ahead of the server version vector.
2274
2652
  */
@@ -2318,7 +2696,7 @@ var Store = class {
2318
2696
  let latest = null;
2319
2697
  for (const row of rows) {
2320
2698
  const op = deserializeOperationWithCollection(row, collection);
2321
- if (!latest || HybridLogicalClock2.compare(op.timestamp, latest.timestamp) > 0) {
2699
+ if (!latest || HybridLogicalClock3.compare(op.timestamp, latest.timestamp) > 0) {
2322
2700
  latest = op;
2323
2701
  }
2324
2702
  }
@@ -2402,7 +2780,7 @@ var Store = class {
2402
2780
  */
2403
2781
  async exportBackup(options) {
2404
2782
  this.ensureOpen();
2405
- const { exportBackup: doExport } = await import("./backup-JN3LB4YT.js");
2783
+ const { exportBackup: doExport } = await import("./backup-5XZ7THNJ.js");
2406
2784
  return doExport(this.adapter, this.schema, this.nodeId, this.schema.version, options);
2407
2785
  }
2408
2786
  /**
@@ -2414,7 +2792,7 @@ var Store = class {
2414
2792
  */
2415
2793
  async importBackup(data, options) {
2416
2794
  this.ensureOpen();
2417
- const { restoreBackup: doRestore } = await import("./backup-JN3LB4YT.js");
2795
+ const { restoreBackup: doRestore } = await import("./backup-5XZ7THNJ.js");
2418
2796
  return doRestore(this.adapter, this.schema, data, options);
2419
2797
  }
2420
2798
  /**
@@ -2568,15 +2946,494 @@ var Store = class {
2568
2946
  }
2569
2947
  }
2570
2948
  };
2949
+
2950
+ // src/reactivity/query-store.ts
2951
+ var EMPTY_ARRAY = Object.freeze([]);
2952
+ var QueryStore = class {
2953
+ snapshot = EMPTY_ARRAY;
2954
+ listeners = /* @__PURE__ */ new Set();
2955
+ unsubscribeQuery = null;
2956
+ active = false;
2957
+ queryBuilder;
2958
+ constructor(queryBuilder) {
2959
+ this.queryBuilder = queryBuilder;
2960
+ }
2961
+ /**
2962
+ * Subscribe to snapshot changes.
2963
+ *
2964
+ * @returns Unsubscribe function
2965
+ */
2966
+ subscribe = (onStoreChange) => {
2967
+ this.listeners.add(onStoreChange);
2968
+ if (!this.active) {
2969
+ this.startSubscription();
2970
+ }
2971
+ return () => {
2972
+ this.listeners.delete(onStoreChange);
2973
+ if (this.listeners.size === 0) {
2974
+ this.stopSubscription();
2975
+ }
2976
+ };
2977
+ };
2978
+ /** Synchronous read of the latest query results. */
2979
+ getSnapshot = () => {
2980
+ return this.snapshot;
2981
+ };
2982
+ /** Tear down listeners and the underlying query subscription. */
2983
+ destroy() {
2984
+ this.stopSubscription();
2985
+ this.listeners.clear();
2986
+ this.snapshot = EMPTY_ARRAY;
2987
+ }
2988
+ startSubscription() {
2989
+ this.active = true;
2990
+ this.unsubscribeQuery = this.queryBuilder.subscribe((results) => {
2991
+ if (!this.active) return;
2992
+ this.snapshot = Object.freeze([...results]);
2993
+ this.notifyListeners();
2994
+ });
2995
+ }
2996
+ stopSubscription() {
2997
+ this.active = false;
2998
+ if (this.unsubscribeQuery) {
2999
+ this.unsubscribeQuery();
3000
+ this.unsubscribeQuery = null;
3001
+ }
3002
+ }
3003
+ notifyListeners() {
3004
+ for (const listener of this.listeners) {
3005
+ listener();
3006
+ }
3007
+ }
3008
+ };
3009
+
3010
+ // src/reactivity/assert-query-ready.ts
3011
+ import { AppNotReadyError } from "@korajs/core";
3012
+ var LEGACY_PENDING_COLLECTION = "__pending__";
3013
+ function assertQueryReady(query) {
3014
+ const descriptor = query.getDescriptor();
3015
+ if (descriptor.collection === LEGACY_PENDING_COLLECTION) {
3016
+ throw new AppNotReadyError(
3017
+ "Cannot use useQuery() before app.ready. Await app.ready or wrap your UI in <KoraProvider app={app}>."
3018
+ );
3019
+ }
3020
+ }
3021
+
3022
+ // src/reactivity/query-store-cache.ts
3023
+ var QueryStoreCache = class {
3024
+ constructor(scopeKey = "default") {
3025
+ this.scopeKey = scopeKey;
3026
+ }
3027
+ scopeKey;
3028
+ entries = /* @__PURE__ */ new Map();
3029
+ getOrCreate(queryBuilder) {
3030
+ const key = this.getKey(queryBuilder);
3031
+ const existing = this.entries.get(key);
3032
+ if (existing) {
3033
+ existing.refCount++;
3034
+ return existing.queryStore;
3035
+ }
3036
+ const queryStore = new QueryStore(queryBuilder);
3037
+ this.entries.set(key, {
3038
+ queryStore,
3039
+ refCount: 1
3040
+ });
3041
+ return queryStore;
3042
+ }
3043
+ release(queryBuilder) {
3044
+ const key = this.getKey(queryBuilder);
3045
+ const entry = this.entries.get(key);
3046
+ if (!entry) {
3047
+ return;
3048
+ }
3049
+ entry.refCount--;
3050
+ if (entry.refCount <= 0) {
3051
+ entry.queryStore.destroy();
3052
+ this.entries.delete(key);
3053
+ }
3054
+ }
3055
+ clear() {
3056
+ for (const entry of this.entries.values()) {
3057
+ entry.queryStore.destroy();
3058
+ }
3059
+ this.entries.clear();
3060
+ }
3061
+ get size() {
3062
+ return this.entries.size;
3063
+ }
3064
+ getKey(queryBuilder) {
3065
+ return `${this.scopeKey}:${JSON.stringify(queryBuilder.getDescriptor())}`;
3066
+ }
3067
+ };
3068
+ var sharedCache = null;
3069
+ function getSharedQueryStoreCache() {
3070
+ if (!sharedCache) {
3071
+ sharedCache = new QueryStoreCache();
3072
+ }
3073
+ return sharedCache;
3074
+ }
3075
+
3076
+ // src/richtext/create-richtext-controller.ts
3077
+ import * as Y from "yjs";
3078
+ var LOAD_ORIGIN = "kora-load";
3079
+ var REMOTE_ORIGIN = "kora-remote";
3080
+ var DOC_CHANNEL_ORIGIN = "kora-doc-channel";
3081
+ var TEXT_KEY = "content";
3082
+ var COMPACT_AFTER_DELTAS = 20;
3083
+ var PERSIST_DEBOUNCE_MS = 400;
3084
+ function createRichTextController(options) {
3085
+ const {
3086
+ collection,
3087
+ collectionName,
3088
+ recordId,
3089
+ fieldName,
3090
+ store,
3091
+ syncEngine = null,
3092
+ useDocChannel
3093
+ } = options;
3094
+ const doc = new Y.Doc();
3095
+ const text = doc.getText(TEXT_KEY);
3096
+ const undoManager = new Y.UndoManager(text);
3097
+ let user = options.user;
3098
+ let disposed = false;
3099
+ let ready = false;
3100
+ let error = null;
3101
+ let canUndo = false;
3102
+ let canRedo = false;
3103
+ let cursors = [];
3104
+ const listeners = /* @__PURE__ */ new Set();
3105
+ let snapshot = {
3106
+ ready: false,
3107
+ error: null,
3108
+ canUndo: false,
3109
+ canRedo: false,
3110
+ cursors: []
3111
+ };
3112
+ const baseUpdateRef = { current: null };
3113
+ const pendingDeltasRef = { current: [] };
3114
+ const docChannelActiveRef = { current: false };
3115
+ let persistTimer = null;
3116
+ let recordUnsubscribe = null;
3117
+ let docChannelUnsubscribe = null;
3118
+ let awarenessUnsubscribe = null;
3119
+ const refreshSnapshot = () => {
3120
+ snapshot = {
3121
+ ready,
3122
+ error,
3123
+ canUndo,
3124
+ canRedo,
3125
+ cursors: [...cursors]
3126
+ };
3127
+ };
3128
+ const notify = () => {
3129
+ refreshSnapshot();
3130
+ for (const listener of listeners) {
3131
+ listener();
3132
+ }
3133
+ };
3134
+ const syncHistoryState = () => {
3135
+ canUndo = undoManager.undoStack.length > 0;
3136
+ canRedo = undoManager.redoStack.length > 0;
3137
+ notify();
3138
+ };
3139
+ const getSnapshot = () => snapshot;
3140
+ const resolveAwarenessUser = () => {
3141
+ if (user) {
3142
+ return user;
3143
+ }
3144
+ if (syncEngine) {
3145
+ const existing = syncEngine.getAwarenessManager().getLocalState()?.user;
3146
+ if (existing) {
3147
+ return existing;
3148
+ }
3149
+ }
3150
+ return {
3151
+ name: "Anonymous",
3152
+ color: "#6366f1"
3153
+ };
3154
+ };
3155
+ const setCursor = (anchor, head) => {
3156
+ if (!syncEngine) {
3157
+ return;
3158
+ }
3159
+ const awareness = syncEngine.getAwarenessManager();
3160
+ const state = {
3161
+ user: resolveAwarenessUser(),
3162
+ cursor: {
3163
+ collection: collectionName,
3164
+ recordId,
3165
+ field: fieldName,
3166
+ anchor,
3167
+ head
3168
+ }
3169
+ };
3170
+ awareness.setLocalState(state);
3171
+ };
3172
+ const clearCursor = () => {
3173
+ if (!syncEngine) {
3174
+ return;
3175
+ }
3176
+ const awareness = syncEngine.getAwarenessManager();
3177
+ const current = awareness.getLocalState();
3178
+ if (!current?.cursor) {
3179
+ return;
3180
+ }
3181
+ if (current.cursor.collection !== collectionName || current.cursor.recordId !== recordId || current.cursor.field !== fieldName) {
3182
+ return;
3183
+ }
3184
+ awareness.setLocalState({ user: current.user });
3185
+ };
3186
+ const applyRemoteSnapshot = (value) => {
3187
+ const encoded = encodeRichtextInput(value);
3188
+ if (!encoded) {
3189
+ return;
3190
+ }
3191
+ const currentSnapshot = Y.encodeStateAsUpdate(doc);
3192
+ if (updatesEqual(currentSnapshot, encoded)) {
3193
+ return;
3194
+ }
3195
+ Y.applyUpdate(doc, encoded, REMOTE_ORIGIN);
3196
+ baseUpdateRef.current = encoded;
3197
+ pendingDeltasRef.current = [];
3198
+ syncHistoryState();
3199
+ };
3200
+ const flushPersist = async () => {
3201
+ const snapshot2 = composeRichtextSnapshot(baseUpdateRef.current, pendingDeltasRef.current);
3202
+ if (pendingDeltasRef.current.length >= COMPACT_AFTER_DELTAS) {
3203
+ baseUpdateRef.current = snapshot2;
3204
+ pendingDeltasRef.current = [];
3205
+ }
3206
+ try {
3207
+ await collection.update(recordId, {
3208
+ [fieldName]: snapshot2
3209
+ });
3210
+ } catch (cause) {
3211
+ if (!disposed) {
3212
+ error = cause instanceof Error ? cause : new Error(String(cause));
3213
+ notify();
3214
+ }
3215
+ }
3216
+ };
3217
+ const schedulePersist = () => {
3218
+ if (persistTimer) {
3219
+ clearTimeout(persistTimer);
3220
+ }
3221
+ persistTimer = setTimeout(() => {
3222
+ persistTimer = null;
3223
+ void flushPersist();
3224
+ }, PERSIST_DEBOUNCE_MS);
3225
+ };
3226
+ const onDocUpdate = (update, origin) => {
3227
+ syncHistoryState();
3228
+ if (origin === LOAD_ORIGIN || origin === REMOTE_ORIGIN || origin === DOC_CHANNEL_ORIGIN) {
3229
+ return;
3230
+ }
3231
+ pendingDeltasRef.current.push(update);
3232
+ if (docChannelActiveRef.current && syncEngine) {
3233
+ syncEngine.getRichtextDocChannel?.().send(collectionName, recordId, fieldName, update);
3234
+ schedulePersist();
3235
+ return;
3236
+ }
3237
+ void flushPersist();
3238
+ };
3239
+ const subscribeRecordChanges = () => {
3240
+ recordUnsubscribe?.();
3241
+ recordUnsubscribe = store.collection(collectionName).where({ id: recordId }).subscribe((results) => {
3242
+ const record = results[0];
3243
+ if (!record) {
3244
+ return;
3245
+ }
3246
+ applyRemoteSnapshot(record[fieldName]);
3247
+ });
3248
+ };
3249
+ const subscribeDocChannel = () => {
3250
+ docChannelUnsubscribe?.();
3251
+ if (!ready || !syncEngine || !docChannelActiveRef.current) {
3252
+ return;
3253
+ }
3254
+ const channel = syncEngine.getRichtextDocChannel?.();
3255
+ if (!channel) {
3256
+ return;
3257
+ }
3258
+ docChannelUnsubscribe = channel.subscribe(collectionName, recordId, fieldName, (update) => {
3259
+ Y.applyUpdate(doc, update, DOC_CHANNEL_ORIGIN);
3260
+ syncHistoryState();
3261
+ });
3262
+ };
3263
+ const updateCursors = () => {
3264
+ if (!syncEngine) {
3265
+ cursors = [];
3266
+ notify();
3267
+ return;
3268
+ }
3269
+ const awareness = syncEngine.getAwarenessManager();
3270
+ const localClientId = awareness.clientId;
3271
+ const states = awareness.getStates();
3272
+ const fieldCursors = [];
3273
+ for (const [clientId, state] of states) {
3274
+ if (clientId === localClientId) continue;
3275
+ if (!state.cursor) continue;
3276
+ if (state.cursor.collection !== collectionName || state.cursor.recordId !== recordId || state.cursor.field !== fieldName) {
3277
+ continue;
3278
+ }
3279
+ fieldCursors.push({
3280
+ clientId,
3281
+ userName: state.user.name,
3282
+ color: state.user.color,
3283
+ anchor: state.cursor.anchor,
3284
+ head: state.cursor.head
3285
+ });
3286
+ }
3287
+ cursors = fieldCursors;
3288
+ notify();
3289
+ };
3290
+ const subscribeAwareness = () => {
3291
+ awarenessUnsubscribe?.();
3292
+ if (!syncEngine) {
3293
+ cursors = [];
3294
+ notify();
3295
+ return;
3296
+ }
3297
+ const awareness = syncEngine.getAwarenessManager();
3298
+ awarenessUnsubscribe = awareness.on("change", () => {
3299
+ updateCursors();
3300
+ });
3301
+ updateCursors();
3302
+ };
3303
+ const initialize = async () => {
3304
+ ready = false;
3305
+ error = null;
3306
+ notify();
3307
+ try {
3308
+ const record = await collection.findById(recordId);
3309
+ if (disposed) return;
3310
+ doc.transact(() => {
3311
+ const target = doc.getText(TEXT_KEY);
3312
+ target.delete(0, target.length);
3313
+ }, LOAD_ORIGIN);
3314
+ const encoded = encodeRichtextInput(record?.[fieldName]);
3315
+ baseUpdateRef.current = encoded;
3316
+ pendingDeltasRef.current = [];
3317
+ if (encoded) {
3318
+ Y.applyUpdate(doc, encoded, LOAD_ORIGIN);
3319
+ }
3320
+ const channel = syncEngine?.getRichtextDocChannel?.();
3321
+ docChannelActiveRef.current = channel?.shouldUseChannel(encoded?.length ?? 0, useDocChannel) ?? false;
3322
+ ready = true;
3323
+ syncHistoryState();
3324
+ subscribeRecordChanges();
3325
+ subscribeDocChannel();
3326
+ subscribeAwareness();
3327
+ notify();
3328
+ } catch (cause) {
3329
+ if (disposed) return;
3330
+ error = cause instanceof Error ? cause : new Error(String(cause));
3331
+ notify();
3332
+ }
3333
+ };
3334
+ doc.on("update", onDocUpdate);
3335
+ void initialize();
3336
+ return {
3337
+ doc,
3338
+ text,
3339
+ getSnapshot,
3340
+ subscribe(listener) {
3341
+ listeners.add(listener);
3342
+ return () => {
3343
+ listeners.delete(listener);
3344
+ };
3345
+ },
3346
+ undo() {
3347
+ undoManager.undo();
3348
+ syncHistoryState();
3349
+ },
3350
+ redo() {
3351
+ undoManager.redo();
3352
+ syncHistoryState();
3353
+ },
3354
+ setCursor,
3355
+ clearCursor,
3356
+ setUser(nextUser) {
3357
+ user = nextUser;
3358
+ },
3359
+ destroy() {
3360
+ if (disposed) {
3361
+ return;
3362
+ }
3363
+ disposed = true;
3364
+ if (persistTimer) {
3365
+ clearTimeout(persistTimer);
3366
+ persistTimer = null;
3367
+ }
3368
+ doc.off("update", onDocUpdate);
3369
+ recordUnsubscribe?.();
3370
+ docChannelUnsubscribe?.();
3371
+ awarenessUnsubscribe?.();
3372
+ clearCursor();
3373
+ undoManager.destroy();
3374
+ baseUpdateRef.current = null;
3375
+ pendingDeltasRef.current = [];
3376
+ docChannelActiveRef.current = false;
3377
+ listeners.clear();
3378
+ }
3379
+ };
3380
+ }
3381
+ function encodeRichtextInput(value) {
3382
+ if (value === null || value === void 0) {
3383
+ return null;
3384
+ }
3385
+ try {
3386
+ return encodeRichtext(value);
3387
+ } catch {
3388
+ if (typeof value === "string") {
3389
+ const fallbackDoc = new Y.Doc();
3390
+ fallbackDoc.getText(TEXT_KEY).insert(0, value);
3391
+ return Y.encodeStateAsUpdate(fallbackDoc);
3392
+ }
3393
+ throw new Error("Richtext record value must be a string, Uint8Array, ArrayBuffer, or null.");
3394
+ }
3395
+ }
3396
+ function composeRichtextSnapshot(base, deltas) {
3397
+ const mergedDoc = new Y.Doc();
3398
+ if (base) {
3399
+ Y.applyUpdate(mergedDoc, base);
3400
+ }
3401
+ for (const delta of deltas) {
3402
+ Y.applyUpdate(mergedDoc, delta);
3403
+ }
3404
+ return Y.encodeStateAsUpdate(mergedDoc);
3405
+ }
3406
+ function updatesEqual(left, right) {
3407
+ if (left.length !== right.length) {
3408
+ return false;
3409
+ }
3410
+ for (let index = 0; index < left.length; index++) {
3411
+ if (left[index] !== right[index]) {
3412
+ return false;
3413
+ }
3414
+ }
3415
+ return true;
3416
+ }
3417
+
3418
+ // src/richtext/as-rich-text-sync-engine.ts
3419
+ function asRichTextSyncEngine(engine) {
3420
+ if (engine == null) {
3421
+ return null;
3422
+ }
3423
+ return engine;
3424
+ }
2571
3425
  export {
2572
3426
  AdapterError,
2573
3427
  COMPACTION_BASELINE_META_KEY,
2574
3428
  Collection,
2575
3429
  InvalidStateTransitionError,
2576
3430
  LAST_ACKED_SERVER_VECTOR_META_KEY,
3431
+ OptimisticLockError,
2577
3432
  PersistenceError,
2578
3433
  QueryBuilder,
2579
3434
  QueryError,
3435
+ QueryStore,
3436
+ QueryStoreCache,
2580
3437
  RecordNotFoundError,
2581
3438
  SequenceManager,
2582
3439
  Store,
@@ -2586,14 +3443,18 @@ export {
2586
3443
  TransactionContext,
2587
3444
  WorkerInitError,
2588
3445
  WorkerTimeoutError,
3446
+ asRichTextSyncEngine,
3447
+ assertQueryReady,
2589
3448
  collectOperationsAheadOfServer,
2590
3449
  compactOperationLog,
2591
3450
  computeAckCompactionWatermark,
3451
+ createRichTextController,
2592
3452
  decodeAuditExport,
2593
3453
  decodeRichtext,
2594
3454
  deserializeVersionVectorFromMeta,
2595
3455
  encodeRichtext,
2596
3456
  exportBackup,
3457
+ getSharedQueryStoreCache,
2597
3458
  mergeVersionVectors,
2598
3459
  persistedAuditTraceFromEvent,
2599
3460
  pluralize,