@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.cjs CHANGED
@@ -30,6 +30,38 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
30
30
  ));
31
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
32
 
33
+ // src/serialization/op-data-encoding.ts
34
+ function encodeRichtextFieldsForOpData(data, fields) {
35
+ const result = { ...data };
36
+ for (const [key, value] of Object.entries(data)) {
37
+ if (fields[key]?.kind !== "richtext") {
38
+ continue;
39
+ }
40
+ if (typeof value === "string" || value instanceof Uint8Array || value instanceof ArrayBuffer) {
41
+ result[key] = (0, import_core4.encodeBytesForOpData)(value);
42
+ }
43
+ }
44
+ return result;
45
+ }
46
+ function decodeRichtextFieldsFromOpData(data, fields) {
47
+ const result = { ...data };
48
+ for (const [key, value] of Object.entries(data)) {
49
+ if (fields[key]?.kind !== "richtext" || value === null || value === void 0) {
50
+ continue;
51
+ }
52
+ result[key] = (0, import_core4.decodeBytesFromOpData)(value);
53
+ }
54
+ return result;
55
+ }
56
+ var import_core4, decodeRichtextOpDataValue;
57
+ var init_op_data_encoding = __esm({
58
+ "src/serialization/op-data-encoding.ts"() {
59
+ "use strict";
60
+ import_core4 = require("@korajs/core");
61
+ decodeRichtextOpDataValue = import_core4.decodeBytesFromOpData;
62
+ }
63
+ });
64
+
33
65
  // src/serialization/richtext-serializer.ts
34
66
  function encodeRichtext(value) {
35
67
  if (value === null || value === void 0) {
@@ -46,6 +78,10 @@ function encodeRichtext(value) {
46
78
  if (value instanceof ArrayBuffer) {
47
79
  return new Uint8Array(value);
48
80
  }
81
+ if ((0, import_core4.isKoraBytesValue)(value) || (0, import_core4.isLegacyNumericByteObject)(value)) {
82
+ const decoded = decodeRichtextOpDataValue(value);
83
+ return decoded instanceof Uint8Array ? decoded : null;
84
+ }
49
85
  throw new Error("Richtext value must be a string, Uint8Array, ArrayBuffer, null, or undefined.");
50
86
  }
51
87
  function decodeRichtext(value) {
@@ -84,6 +120,7 @@ var init_richtext_serializer = __esm({
84
120
  "src/serialization/richtext-serializer.ts"() {
85
121
  "use strict";
86
122
  Y = __toESM(require("yjs"), 1);
123
+ init_op_data_encoding();
87
124
  TEXT_KEY = "content";
88
125
  }
89
126
  });
@@ -147,7 +184,7 @@ function serializeOperation(op) {
147
184
  record_id: op.recordId,
148
185
  data: dataPayload ? JSON.stringify(dataPayload) : null,
149
186
  previous_data: op.previousData ? JSON.stringify(op.previousData) : null,
150
- timestamp: import_core4.HybridLogicalClock.serialize(op.timestamp),
187
+ timestamp: import_core5.HybridLogicalClock.serialize(op.timestamp),
151
188
  sequence_number: op.sequenceNumber,
152
189
  causal_deps: JSON.stringify(op.causalDeps),
153
190
  schema_version: op.schemaVersion
@@ -181,7 +218,7 @@ function deserializeOperation(row) {
181
218
  recordId: row.record_id,
182
219
  data,
183
220
  previousData: row.previous_data ? JSON.parse(row.previous_data) : null,
184
- timestamp: import_core4.HybridLogicalClock.deserialize(row.timestamp),
221
+ timestamp: import_core5.HybridLogicalClock.deserialize(row.timestamp),
185
222
  sequenceNumber: row.sequence_number,
186
223
  causalDeps: JSON.parse(row.causal_deps),
187
224
  schemaVersion: row.schema_version,
@@ -224,11 +261,11 @@ function deserializeValue(value, descriptor) {
224
261
  return value;
225
262
  }
226
263
  }
227
- var import_core4, ATOMIC_OPS_KEY, TX_ID_KEY, MUTATION_NAME_KEY;
264
+ var import_core5, ATOMIC_OPS_KEY, TX_ID_KEY, MUTATION_NAME_KEY;
228
265
  var init_serializer = __esm({
229
266
  "src/serialization/serializer.ts"() {
230
267
  "use strict";
231
- import_core4 = require("@korajs/core");
268
+ import_core5 = require("@korajs/core");
232
269
  init_richtext_serializer();
233
270
  ATOMIC_OPS_KEY = "__kora_atomic_ops__";
234
271
  TX_ID_KEY = "__kora_tx_id__";
@@ -609,11 +646,11 @@ async function computeSha256(data) {
609
646
  const hashArray = Array.from(new Uint8Array(hashBuffer));
610
647
  return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
611
648
  }
612
- var import_core5, BACKUP_VERSION;
649
+ var import_core6, BACKUP_VERSION;
613
650
  var init_backup = __esm({
614
651
  "src/backup/backup.ts"() {
615
652
  "use strict";
616
- import_core5 = require("@korajs/core");
653
+ import_core6 = require("@korajs/core");
617
654
  init_serializer();
618
655
  BACKUP_VERSION = 1;
619
656
  }
@@ -670,7 +707,7 @@ __export(audit_trace_store_exports, {
670
707
  });
671
708
  function persistedAuditTraceFromEvent(event) {
672
709
  return {
673
- id: (0, import_core16.generateUUIDv7)(),
710
+ id: (0, import_core18.generateUUIDv7)(),
674
711
  recordedAt: Date.now(),
675
712
  eventType: event.type,
676
713
  constraint: event.type === "constraint:violated" ? event.constraint : void 0,
@@ -736,11 +773,11 @@ function auditTraceIndexFields(trace) {
736
773
  recordId: trace.operationA.recordId
737
774
  };
738
775
  }
739
- var import_core16;
776
+ var import_core18;
740
777
  var init_audit_trace_store = __esm({
741
778
  "src/audit/audit-trace-store.ts"() {
742
779
  "use strict";
743
- import_core16 = require("@korajs/core");
780
+ import_core18 = require("@korajs/core");
744
781
  init_audit_json();
745
782
  }
746
783
  });
@@ -940,9 +977,12 @@ __export(index_exports, {
940
977
  Collection: () => Collection,
941
978
  InvalidStateTransitionError: () => InvalidStateTransitionError,
942
979
  LAST_ACKED_SERVER_VECTOR_META_KEY: () => LAST_ACKED_SERVER_VECTOR_META_KEY,
980
+ OptimisticLockError: () => OptimisticLockError,
943
981
  PersistenceError: () => PersistenceError,
944
982
  QueryBuilder: () => QueryBuilder,
945
983
  QueryError: () => QueryError,
984
+ QueryStore: () => QueryStore,
985
+ QueryStoreCache: () => QueryStoreCache,
946
986
  RecordNotFoundError: () => RecordNotFoundError,
947
987
  SequenceManager: () => SequenceManager,
948
988
  Store: () => Store,
@@ -952,14 +992,18 @@ __export(index_exports, {
952
992
  TransactionContext: () => TransactionContext,
953
993
  WorkerInitError: () => WorkerInitError,
954
994
  WorkerTimeoutError: () => WorkerTimeoutError,
995
+ asRichTextSyncEngine: () => asRichTextSyncEngine,
996
+ assertQueryReady: () => assertQueryReady,
955
997
  collectOperationsAheadOfServer: () => collectOperationsAheadOfServer,
956
998
  compactOperationLog: () => compactOperationLog,
957
999
  computeAckCompactionWatermark: () => computeAckCompactionWatermark,
1000
+ createRichTextController: () => createRichTextController,
958
1001
  decodeAuditExport: () => decodeAuditExport,
959
1002
  decodeRichtext: () => decodeRichtext,
960
1003
  deserializeVersionVectorFromMeta: () => deserializeVersionVectorFromMeta,
961
1004
  encodeRichtext: () => encodeRichtext,
962
1005
  exportBackup: () => exportBackup,
1006
+ getSharedQueryStoreCache: () => getSharedQueryStoreCache,
963
1007
  mergeVersionVectors: () => mergeVersionVectors,
964
1008
  persistedAuditTraceFromEvent: () => persistedAuditTraceFromEvent,
965
1009
  pluralize: () => pluralize,
@@ -994,6 +1038,16 @@ var RecordNotFoundError = class extends import_core.KoraError {
994
1038
  this.name = "RecordNotFoundError";
995
1039
  }
996
1040
  };
1041
+ var OptimisticLockError = class extends import_core.KoraError {
1042
+ constructor(collection, recordId) {
1043
+ super(
1044
+ `Row version changed while merging record "${recordId}" in "${collection}"; retry with fresh state`,
1045
+ "OPTIMISTIC_LOCK",
1046
+ { collection, recordId }
1047
+ );
1048
+ this.name = "OptimisticLockError";
1049
+ }
1050
+ };
997
1051
  var AdapterError = class extends import_core.KoraError {
998
1052
  constructor(message, context) {
999
1053
  super(message, "ADAPTER_ERROR", context);
@@ -1170,27 +1224,27 @@ async function saveDeltaCursor(adapter, cursor) {
1170
1224
  }
1171
1225
 
1172
1226
  // src/store/store.ts
1173
- var import_core17 = require("@korajs/core");
1227
+ var import_core19 = require("@korajs/core");
1174
1228
  init_backup();
1175
1229
 
1176
1230
  // src/mutations/execute-delete.ts
1177
- var import_core7 = require("@korajs/core");
1231
+ var import_core8 = require("@korajs/core");
1178
1232
 
1179
1233
  // src/lww/row-version.ts
1180
- var import_core6 = require("@korajs/core");
1234
+ var import_core7 = require("@korajs/core");
1181
1235
  var EMPTY_ROW_VERSION = "";
1182
1236
  function serializeRowVersion(timestamp) {
1183
- return import_core6.HybridLogicalClock.serialize(timestamp);
1237
+ return import_core7.HybridLogicalClock.serialize(timestamp);
1184
1238
  }
1185
1239
  function rowVersionFromRecord(row) {
1186
1240
  const version = row._version;
1187
1241
  if (typeof version === "string" && version.length > 0) {
1188
- return import_core6.HybridLogicalClock.deserialize(version);
1242
+ return import_core7.HybridLogicalClock.deserialize(version);
1189
1243
  }
1190
1244
  return { wallTime: row._updated_at, logical: 0, nodeId: "" };
1191
1245
  }
1192
1246
  function isIncomingNewerThanRow(incoming, row) {
1193
- return import_core6.HybridLogicalClock.compare(incoming, rowVersionFromRecord(row)) > 0;
1247
+ return import_core7.HybridLogicalClock.compare(incoming, rowVersionFromRecord(row)) > 0;
1194
1248
  }
1195
1249
  function lwwVersionWhereClause(remoteVersion) {
1196
1250
  return {
@@ -1250,6 +1304,28 @@ function buildUpdateQuery(collection, id, changes) {
1250
1304
  const sql = `UPDATE ${collection} SET ${setClauses.join(", ")} WHERE id = ?`;
1251
1305
  return { sql, params };
1252
1306
  }
1307
+ function buildFieldFastForwardUpdateQuery(collection, id, fieldChanges, remoteVersion, wallTime, options) {
1308
+ const fieldClauses = Object.keys(fieldChanges).map((col) => `${col} = ?`);
1309
+ const setClauses = [
1310
+ ...fieldClauses,
1311
+ "_version = CASE WHEN _version >= ? THEN _version ELSE ? END",
1312
+ "_updated_at = CASE WHEN _updated_at >= ? THEN _updated_at ELSE ? END"
1313
+ ];
1314
+ const params = [
1315
+ ...Object.values(fieldChanges),
1316
+ remoteVersion,
1317
+ remoteVersion,
1318
+ wallTime,
1319
+ wallTime
1320
+ ];
1321
+ if (options?.maxCreatedAt !== void 0) {
1322
+ setClauses.push("_created_at = CASE WHEN _created_at >= ? THEN _created_at ELSE ? END");
1323
+ params.push(options.maxCreatedAt, options.maxCreatedAt);
1324
+ }
1325
+ params.push(id);
1326
+ const sql = `UPDATE ${collection} SET ${setClauses.join(", ")} WHERE id = ?`;
1327
+ return { sql, params };
1328
+ }
1253
1329
  function buildSoftDeleteQuery(collection, id, updatedAt, version) {
1254
1330
  if (version !== void 0) {
1255
1331
  return {
@@ -1262,13 +1338,6 @@ function buildSoftDeleteQuery(collection, id, updatedAt, version) {
1262
1338
  params: [updatedAt, id]
1263
1339
  };
1264
1340
  }
1265
- function buildLwwUpdateQuery(collection, id, changes, remoteVersion) {
1266
- const setClauses = Object.keys(changes).map((col) => `${col} = ?`);
1267
- const lww = lwwVersionWhereClause(remoteVersion);
1268
- const sql = `UPDATE ${collection} SET ${setClauses.join(", ")} WHERE id = ? AND ${lww.sql}`;
1269
- const params = [...Object.values(changes), id, ...lww.params];
1270
- return { sql, params };
1271
- }
1272
1341
  function buildLwwSoftDeleteQuery(collection, id, updatedAt, version) {
1273
1342
  const lww = lwwVersionWhereClause(version);
1274
1343
  return {
@@ -1433,7 +1502,7 @@ async function executeDelete(ctx, id, options) {
1433
1502
  await ctx.adapter.transaction(async (tx) => {
1434
1503
  if (!operation) {
1435
1504
  const sequenceNumber = await allocateNextSequenceInTransaction(tx, ctx.nodeId);
1436
- operation = await (0, import_core7.createOperation)(
1505
+ operation = await (0, import_core8.createOperation)(
1437
1506
  {
1438
1507
  nodeId: ctx.nodeId,
1439
1508
  type: "delete",
@@ -1480,11 +1549,65 @@ async function executeDelete(ctx, id, options) {
1480
1549
  }
1481
1550
 
1482
1551
  // src/mutations/execute-insert.ts
1483
- var import_core8 = require("@korajs/core");
1552
+ var import_core9 = require("@korajs/core");
1553
+
1554
+ // src/lww/field-versions.ts
1555
+ function parseFieldVersions(raw) {
1556
+ if (typeof raw !== "string" || raw.length === 0) {
1557
+ return {};
1558
+ }
1559
+ try {
1560
+ const parsed = JSON.parse(raw);
1561
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
1562
+ return {};
1563
+ }
1564
+ const result = {};
1565
+ for (const [key, value] of Object.entries(parsed)) {
1566
+ if (typeof value === "string") {
1567
+ result[key] = value;
1568
+ }
1569
+ }
1570
+ return result;
1571
+ } catch {
1572
+ return {};
1573
+ }
1574
+ }
1575
+ function serializeFieldVersions(versions) {
1576
+ return JSON.stringify(versions);
1577
+ }
1578
+ function fieldVersionsForFields(fieldNames, version) {
1579
+ const versions = {};
1580
+ for (const field of fieldNames) {
1581
+ versions[field] = version;
1582
+ }
1583
+ return versions;
1584
+ }
1585
+ function effectiveFieldVersion(current, field, rowVersion) {
1586
+ const own = current[field];
1587
+ if (own !== void 0) {
1588
+ return own;
1589
+ }
1590
+ return rowVersion ?? EMPTY_ROW_VERSION;
1591
+ }
1592
+ function resolvePerFieldLww(current, incomingFields, incomingVersion, rowVersion) {
1593
+ const winners = [];
1594
+ const merged = { ...current };
1595
+ for (const field of incomingFields) {
1596
+ const stored = effectiveFieldVersion(current, field, rowVersion);
1597
+ if (incomingVersion > stored) {
1598
+ winners.push(field);
1599
+ merged[field] = incomingVersion;
1600
+ }
1601
+ }
1602
+ return { winners, merged };
1603
+ }
1604
+
1605
+ // src/mutations/execute-insert.ts
1606
+ init_op_data_encoding();
1484
1607
  init_serializer();
1485
1608
  async function executeInsert(ctx, data) {
1486
- const validated = (0, import_core8.validateRecord)(ctx.collection, ctx.definition, data, "insert");
1487
- const recordId = (0, import_core8.generateUUIDv7)();
1609
+ const validated = (0, import_core9.validateRecord)(ctx.collection, ctx.definition, data, "insert");
1610
+ const recordId = (0, import_core9.generateUUIDv7)();
1488
1611
  for (const [fieldName, descriptor] of Object.entries(ctx.definition.fields)) {
1489
1612
  if (descriptor.auto && descriptor.kind === "timestamp") {
1490
1613
  validated[fieldName] = Date.now();
@@ -1495,13 +1618,16 @@ async function executeInsert(ctx, data) {
1495
1618
  let record;
1496
1619
  await ctx.adapter.transaction(async (tx) => {
1497
1620
  const sequenceNumber = await allocateNextSequenceInTransaction(tx, ctx.nodeId);
1498
- operation = await (0, import_core8.createOperation)(
1621
+ operation = await (0, import_core9.createOperation)(
1499
1622
  {
1500
1623
  nodeId: ctx.nodeId,
1501
1624
  type: "insert",
1502
1625
  collection: ctx.collection,
1503
1626
  recordId,
1504
- data: { ...validated },
1627
+ // Binary richtext values are tagged as canonical JSON BEFORE the
1628
+ // operation is content-hashed, so the hash input, persisted JSON,
1629
+ // and wire payload are the identical value.
1630
+ data: encodeRichtextFieldsForOpData(validated, ctx.definition.fields),
1505
1631
  previousData: null,
1506
1632
  sequenceNumber,
1507
1633
  causalDeps,
@@ -1517,7 +1643,12 @@ async function executeInsert(ctx, data) {
1517
1643
  ...serializedData,
1518
1644
  _created_at: operation.timestamp.wallTime,
1519
1645
  _updated_at: operation.timestamp.wallTime,
1520
- _version: version
1646
+ _version: version,
1647
+ // Stamp every inserted field with this operation's version so later
1648
+ // per-field LWW compares against a real writer, not the fallback.
1649
+ _field_versions: serializeFieldVersions(
1650
+ fieldVersionsForFields(Object.keys(serializedData), version)
1651
+ )
1521
1652
  };
1522
1653
  const builtInsert = buildInsertQuery(ctx.collection, record);
1523
1654
  const opInsert = buildInsertQuery(
@@ -1537,12 +1668,13 @@ async function executeInsert(ctx, data) {
1537
1668
  }
1538
1669
 
1539
1670
  // src/mutations/execute-update.ts
1540
- var import_core10 = require("@korajs/core");
1671
+ var import_core11 = require("@korajs/core");
1672
+ init_op_data_encoding();
1541
1673
  init_serializer();
1542
1674
 
1543
1675
  // src/state-machine/state-validator.ts
1544
- var import_core9 = require("@korajs/core");
1545
- var InvalidStateTransitionError = class extends import_core9.KoraError {
1676
+ var import_core10 = require("@korajs/core");
1677
+ var InvalidStateTransitionError = class extends import_core10.KoraError {
1546
1678
  constructor(collection, recordId, field, fromState, toState, allowedStates) {
1547
1679
  super(
1548
1680
  `Invalid state transition in collection "${collection}": cannot transition field "${field}" from "${fromState}" to "${toState}". Allowed transitions from "${fromState}": ${allowedStates.length > 0 ? allowedStates.join(", ") : "(none -- terminal state)"}`,
@@ -1576,7 +1708,7 @@ function validateStateTransition(collectionName, recordId, stateMachine, current
1576
1708
  collection: collectionName,
1577
1709
  transitions: stateMachine.transitions
1578
1710
  };
1579
- const transitionResult = (0, import_core9.validateTransition)(constraint, currentState, newState);
1711
+ const transitionResult = (0, import_core10.validateTransition)(constraint, currentState, newState);
1580
1712
  if (transitionResult.valid) {
1581
1713
  return { valid: true, allowedStates: transitionResult.allowedTargets };
1582
1714
  }
@@ -1632,7 +1764,7 @@ async function executeUpdate(ctx, id, data) {
1632
1764
  if (!currentRow) {
1633
1765
  throw new RecordNotFoundError(ctx.collection, id);
1634
1766
  }
1635
- let validated = (0, import_core10.validateRecord)(ctx.collection, ctx.definition, data, "update");
1767
+ let validated = (0, import_core11.validateRecord)(ctx.collection, ctx.definition, data, "update");
1636
1768
  const currentRecord = deserializeRecord(currentRow, ctx.definition.fields);
1637
1769
  validated = validateUpdateStateMachine(
1638
1770
  ctx.collection,
@@ -1650,9 +1782,9 @@ async function executeUpdate(ctx, id, data) {
1650
1782
  for (const key of Object.keys(validated)) {
1651
1783
  const value = validated[key];
1652
1784
  previousData[key] = currentRecord[key];
1653
- if ((0, import_core10.isAtomicOp)(value)) {
1654
- resolvedData[key] = (0, import_core10.resolveAtomicOp)(currentRecord[key], value);
1655
- atomicOps[key] = (0, import_core10.toAtomicOp)(value);
1785
+ if ((0, import_core11.isAtomicOp)(value)) {
1786
+ resolvedData[key] = (0, import_core11.resolveAtomicOp)(currentRecord[key], value);
1787
+ atomicOps[key] = (0, import_core11.toAtomicOp)(value);
1656
1788
  } else {
1657
1789
  resolvedData[key] = value;
1658
1790
  }
@@ -1662,14 +1794,17 @@ async function executeUpdate(ctx, id, data) {
1662
1794
  let operation;
1663
1795
  await ctx.adapter.transaction(async (tx) => {
1664
1796
  const sequenceNumber = await allocateNextSequenceInTransaction(tx, ctx.nodeId);
1665
- operation = await (0, import_core10.createOperation)(
1797
+ operation = await (0, import_core11.createOperation)(
1666
1798
  {
1667
1799
  nodeId: ctx.nodeId,
1668
1800
  type: "update",
1669
1801
  collection: ctx.collection,
1670
1802
  recordId: id,
1671
- data: { ...resolvedData },
1672
- previousData,
1803
+ // Binary richtext values (new and previous) are tagged as canonical
1804
+ // JSON BEFORE the operation is content-hashed, so the hash input,
1805
+ // persisted JSON, and wire payload are the identical value.
1806
+ data: encodeRichtextFieldsForOpData(resolvedData, ctx.definition.fields),
1807
+ previousData: encodeRichtextFieldsForOpData(previousData, ctx.definition.fields),
1673
1808
  sequenceNumber,
1674
1809
  causalDeps,
1675
1810
  schemaVersion: ctx.schema.version,
@@ -1680,10 +1815,15 @@ async function executeUpdate(ctx, id, data) {
1680
1815
  ctx.causalTracker?.afterOperation(ctx.collection, operation.id, ctx.inTransaction);
1681
1816
  const serializedChanges = serializeRecord(resolvedData, ctx.definition.fields);
1682
1817
  const version = serializeRowVersion(operation.timestamp);
1818
+ const mergedFieldVersions = parseFieldVersions(currentRow._field_versions);
1819
+ for (const changedField of Object.keys(serializedChanges)) {
1820
+ mergedFieldVersions[changedField] = version;
1821
+ }
1683
1822
  const updateQuery = buildUpdateQuery(ctx.collection, id, {
1684
1823
  ...serializedChanges,
1685
1824
  _updated_at: operation.timestamp.wallTime,
1686
- _version: version
1825
+ _version: version,
1826
+ _field_versions: serializeFieldVersions(mergedFieldVersions)
1687
1827
  });
1688
1828
  const opInsert = buildInsertQuery(
1689
1829
  `_kora_ops_${ctx.collection}`,
@@ -2083,7 +2223,7 @@ var QueryError2 = class extends Error {
2083
2223
  };
2084
2224
 
2085
2225
  // src/relations/relation-enforcer.ts
2086
- var import_core11 = require("@korajs/core");
2226
+ var import_core12 = require("@korajs/core");
2087
2227
  init_serializer();
2088
2228
 
2089
2229
  // src/relations/relation-lookup.ts
@@ -2108,7 +2248,7 @@ function getIncomingRelations(lookup, collection) {
2108
2248
  }
2109
2249
 
2110
2250
  // src/relations/relation-enforcer.ts
2111
- var ReferentialIntegrityError = class extends import_core11.KoraError {
2251
+ var ReferentialIntegrityError = class extends import_core12.KoraError {
2112
2252
  constructor(collection, recordId, referencingCollection, relationName, referencingCount) {
2113
2253
  super(
2114
2254
  `Cannot delete record "${recordId}" from "${collection}": ${referencingCount} record(s) in "${referencingCollection}" reference it via relation "${relationName}" with onDelete: 'restrict'. Delete or reassign the referencing records first.`,
@@ -2197,7 +2337,7 @@ var RelationEnforcer = class {
2197
2337
  for (const row of referencingRows) {
2198
2338
  const now = Date.now();
2199
2339
  const sequenceNumber = await allocateNextSequenceInTransaction(tx, this.nodeId);
2200
- const operation = await (0, import_core11.createOperation)(
2340
+ const operation = await (0, import_core12.createOperation)(
2201
2341
  {
2202
2342
  nodeId: this.nodeId,
2203
2343
  type: "delete",
@@ -2246,7 +2386,7 @@ var RelationEnforcer = class {
2246
2386
  const sequenceNumber = await allocateNextSequenceInTransaction(tx, this.nodeId);
2247
2387
  const updateData = { [foreignKeyField]: null };
2248
2388
  const previousData = { [foreignKeyField]: deletedRecordId };
2249
- const operation = await (0, import_core11.createOperation)(
2389
+ const operation = await (0, import_core12.createOperation)(
2250
2390
  {
2251
2391
  nodeId: this.nodeId,
2252
2392
  type: "update",
@@ -2307,8 +2447,9 @@ var RelationEnforcer = class {
2307
2447
  };
2308
2448
 
2309
2449
  // src/replay/replay-to.ts
2310
- var import_core12 = require("@korajs/core");
2450
+ var import_core13 = require("@korajs/core");
2311
2451
  var import_internal = require("@korajs/core/internal");
2452
+ init_op_data_encoding();
2312
2453
  function collectCausalClosure(allOps, targetOperationId) {
2313
2454
  const opMap = /* @__PURE__ */ new Map();
2314
2455
  for (const op of allOps) {
@@ -2316,7 +2457,7 @@ function collectCausalClosure(allOps, targetOperationId) {
2316
2457
  }
2317
2458
  const target = opMap.get(targetOperationId);
2318
2459
  if (!target) {
2319
- throw new import_core12.OperationError(
2460
+ throw new import_core13.OperationError(
2320
2461
  `Operation "${targetOperationId}" not found in the local operation log`,
2321
2462
  {
2322
2463
  operationId: targetOperationId
@@ -2348,7 +2489,7 @@ function buildReplaySnapshot(schema, allOps, targetOperationId) {
2348
2489
  const operationsApplied = collectCausalClosure(allOps, targetOperationId);
2349
2490
  const targetOperation = operationsApplied.find((op) => op.id === targetOperationId);
2350
2491
  if (!targetOperation) {
2351
- throw new import_core12.OperationError(`Operation "${targetOperationId}" not found after causal sort`, {
2492
+ throw new import_core13.OperationError(`Operation "${targetOperationId}" not found after causal sort`, {
2352
2493
  operationId: targetOperationId
2353
2494
  });
2354
2495
  }
@@ -2393,7 +2534,9 @@ function applyOperationToMemory(state, op, schema) {
2393
2534
  }
2394
2535
  colMap.set(op.recordId, {
2395
2536
  id: op.recordId,
2396
- fields: { ...op.data },
2537
+ // op.data stores binary richtext as tagged JSON; snapshots must
2538
+ // expose record-shaped values (Uint8Array/string).
2539
+ fields: decodeRichtextFieldsFromOpData(op.data, definition.fields),
2397
2540
  deleted: false,
2398
2541
  createdAt: wallTime,
2399
2542
  updatedAt: wallTime
@@ -2404,9 +2547,10 @@ function applyOperationToMemory(state, op, schema) {
2404
2547
  if (!op.data) {
2405
2548
  return;
2406
2549
  }
2550
+ const decodedData = decodeRichtextFieldsFromOpData(op.data, definition.fields);
2407
2551
  const existing = colMap.get(op.recordId);
2408
2552
  if (existing && !existing.deleted) {
2409
- existing.fields = { ...existing.fields, ...op.data };
2553
+ existing.fields = { ...existing.fields, ...decodedData };
2410
2554
  existing.updatedAt = wallTime;
2411
2555
  break;
2412
2556
  }
@@ -2415,7 +2559,7 @@ function applyOperationToMemory(state, op, schema) {
2415
2559
  }
2416
2560
  colMap.set(op.recordId, {
2417
2561
  id: op.recordId,
2418
- fields: { ...op.data },
2562
+ fields: decodedData,
2419
2563
  deleted: false,
2420
2564
  createdAt: wallTime,
2421
2565
  updatedAt: wallTime
@@ -2471,7 +2615,7 @@ function toCollectionRecord(record, definition) {
2471
2615
  }
2472
2616
 
2473
2617
  // src/sequences/sequence-manager.ts
2474
- var import_core13 = require("@korajs/core");
2618
+ var import_core14 = require("@korajs/core");
2475
2619
  var SequenceManager = class {
2476
2620
  adapter;
2477
2621
  nodeId;
@@ -2489,7 +2633,7 @@ var SequenceManager = class {
2489
2633
  async next(name, config) {
2490
2634
  const scope = config?.scope ?? "";
2491
2635
  const startAt = config?.startAt ?? 1;
2492
- const format = config?.format ?? (0, import_core13.defaultSequenceFormat)(name);
2636
+ const format = config?.format ?? (0, import_core14.defaultSequenceFormat)(name);
2493
2637
  let counter = 0;
2494
2638
  await this.adapter.transaction(async (tx) => {
2495
2639
  const rows = await tx.query(
@@ -2511,7 +2655,7 @@ var SequenceManager = class {
2511
2655
  );
2512
2656
  }
2513
2657
  });
2514
- return (0, import_core13.formatSequenceValue)(format, counter, this.nodeId);
2658
+ return (0, import_core14.formatSequenceValue)(format, counter, this.nodeId);
2515
2659
  }
2516
2660
  /**
2517
2661
  * Get the current counter value without incrementing.
@@ -2969,8 +3113,141 @@ var SubscriptionManager = class {
2969
3113
  }
2970
3114
  };
2971
3115
 
3116
+ // src/sync/rebase-unsynced-operations.ts
3117
+ var import_core15 = require("@korajs/core");
3118
+ var import_internal2 = require("@korajs/core/internal");
3119
+ init_serializer();
3120
+ var LOGICAL_COUNTER_LIMIT = import_core15.MAX_LOGICAL + 1;
3121
+ function emptyResult() {
3122
+ return { operations: [], idMapping: {}, rebasedCount: 0, newMaxTimestamp: null };
3123
+ }
3124
+ async function rebaseUnsyncedOperationsInLog(adapter, schema, unsyncedOpIds, correctedNowMs) {
3125
+ if (unsyncedOpIds.length === 0) {
3126
+ return emptyResult();
3127
+ }
3128
+ const rebaseIds = new Set(unsyncedOpIds);
3129
+ const rebased = [];
3130
+ let maxOtherSerializedTs = null;
3131
+ for (const collectionName of Object.keys(schema.collections)) {
3132
+ const rows = await adapter.query(`SELECT * FROM _kora_ops_${collectionName}`);
3133
+ for (const row of rows) {
3134
+ if (rebaseIds.has(row.id)) {
3135
+ rebased.push(deserializeOperationWithCollection(row, collectionName));
3136
+ } else if (maxOtherSerializedTs === null || row.timestamp > maxOtherSerializedTs) {
3137
+ maxOtherSerializedTs = row.timestamp;
3138
+ }
3139
+ }
3140
+ }
3141
+ if (rebased.length === 0) {
3142
+ return emptyResult();
3143
+ }
3144
+ const maxOtherWallTime = maxOtherSerializedTs === null ? Number.NEGATIVE_INFINITY : import_core15.HybridLogicalClock.deserialize(maxOtherSerializedTs).wallTime;
3145
+ const baseWallTime = Math.max(correctedNowMs, maxOtherWallTime + 1);
3146
+ rebased.sort((a, b) => import_core15.HybridLogicalClock.compare(a.timestamp, b.timestamp));
3147
+ const newTimestamps = rebased.map((op, i) => ({
3148
+ // Spill into wallTime past the 5-digit logical serialization limit so the
3149
+ // serialized order stays correct even for enormous offline queues.
3150
+ wallTime: baseWallTime + Math.floor(i / LOGICAL_COUNTER_LIMIT),
3151
+ logical: i % LOGICAL_COUNTER_LIMIT,
3152
+ nodeId: op.nodeId
3153
+ }));
3154
+ const idMapping = {};
3155
+ const newIds = [];
3156
+ for (let i = 0; i < rebased.length; i++) {
3157
+ const op = rebased[i];
3158
+ const ts = newTimestamps[i];
3159
+ if (!op || !ts) {
3160
+ continue;
3161
+ }
3162
+ const input = {
3163
+ nodeId: op.nodeId,
3164
+ type: op.type,
3165
+ collection: op.collection,
3166
+ recordId: op.recordId,
3167
+ data: op.data,
3168
+ previousData: op.previousData,
3169
+ sequenceNumber: op.sequenceNumber,
3170
+ causalDeps: op.causalDeps,
3171
+ schemaVersion: op.schemaVersion,
3172
+ ...op.atomicOps !== void 0 ? { atomicOps: op.atomicOps } : {}
3173
+ };
3174
+ const newId = await (0, import_internal2.computeOperationId)(input, import_core15.HybridLogicalClock.serialize(ts));
3175
+ idMapping[op.id] = newId;
3176
+ newIds.push(newId);
3177
+ }
3178
+ const newOperations = [];
3179
+ for (let i = 0; i < rebased.length; i++) {
3180
+ const op = rebased[i];
3181
+ const ts = newTimestamps[i];
3182
+ const newId = newIds[i];
3183
+ if (!op || !ts || newId === void 0) {
3184
+ continue;
3185
+ }
3186
+ newOperations.push({
3187
+ ...op,
3188
+ id: newId,
3189
+ timestamp: ts,
3190
+ causalDeps: op.causalDeps.map((dep) => idMapping[dep] ?? dep)
3191
+ });
3192
+ }
3193
+ await adapter.transaction(async (tx) => {
3194
+ for (let i = 0; i < rebased.length; i++) {
3195
+ const oldOp = rebased[i];
3196
+ const newOp = newOperations[i];
3197
+ if (!oldOp || !newOp) {
3198
+ continue;
3199
+ }
3200
+ const opsTable = `_kora_ops_${oldOp.collection}`;
3201
+ await tx.execute(`DELETE FROM ${opsTable} WHERE id = ?`, [oldOp.id]);
3202
+ const opInsert = buildInsertQuery(
3203
+ opsTable,
3204
+ serializeOperation(newOp)
3205
+ );
3206
+ await tx.execute(opInsert.sql, opInsert.params);
3207
+ const oldVersion = serializeRowVersion(oldOp.timestamp);
3208
+ const newVersion = serializeRowVersion(newOp.timestamp);
3209
+ await tx.execute(
3210
+ `UPDATE ${oldOp.collection} SET _version = ?, _updated_at = ? WHERE id = ? AND _version = ?`,
3211
+ [newVersion, newOp.timestamp.wallTime, oldOp.recordId, oldVersion]
3212
+ );
3213
+ const rows = await tx.query(
3214
+ `SELECT _field_versions FROM ${oldOp.collection} WHERE id = ?`,
3215
+ [oldOp.recordId]
3216
+ );
3217
+ const fieldVersions = parseFieldVersions(rows[0]?._field_versions);
3218
+ let changed = false;
3219
+ for (const [field, version] of Object.entries(fieldVersions)) {
3220
+ if (version === oldVersion) {
3221
+ fieldVersions[field] = newVersion;
3222
+ changed = true;
3223
+ }
3224
+ }
3225
+ if (changed) {
3226
+ await tx.execute(`UPDATE ${oldOp.collection} SET _field_versions = ? WHERE id = ?`, [
3227
+ serializeFieldVersions(fieldVersions),
3228
+ oldOp.recordId
3229
+ ]);
3230
+ }
3231
+ if (oldOp.type === "insert") {
3232
+ await tx.execute(
3233
+ `UPDATE ${oldOp.collection} SET _created_at = ? WHERE id = ? AND _created_at = ?`,
3234
+ [newOp.timestamp.wallTime, oldOp.recordId, oldOp.timestamp.wallTime]
3235
+ );
3236
+ }
3237
+ }
3238
+ });
3239
+ const newMaxTimestamp = newTimestamps[newTimestamps.length - 1] ?? null;
3240
+ return {
3241
+ operations: newOperations,
3242
+ idMapping,
3243
+ rebasedCount: newOperations.length,
3244
+ newMaxTimestamp
3245
+ };
3246
+ }
3247
+
2972
3248
  // src/transaction/transaction-context.ts
2973
- var import_core14 = require("@korajs/core");
3249
+ var import_core16 = require("@korajs/core");
3250
+ init_op_data_encoding();
2974
3251
  init_serializer();
2975
3252
  var TransactionContext = class {
2976
3253
  transactionId;
@@ -2981,7 +3258,7 @@ var TransactionContext = class {
2981
3258
  config;
2982
3259
  constructor(config) {
2983
3260
  this.config = config;
2984
- this.transactionId = (0, import_core14.generateUUIDv7)();
3261
+ this.transactionId = (0, import_core16.generateUUIDv7)();
2985
3262
  }
2986
3263
  /**
2987
3264
  * Set a human-readable mutation name for this transaction.
@@ -3096,8 +3373,8 @@ var TransactionContext = class {
3096
3373
  }
3097
3374
  async insert(collectionName, definition, data) {
3098
3375
  this.ensureActive();
3099
- const validated = (0, import_core14.validateRecord)(collectionName, definition, data, "insert");
3100
- const recordId = (0, import_core14.generateUUIDv7)();
3376
+ const validated = (0, import_core16.validateRecord)(collectionName, definition, data, "insert");
3377
+ const recordId = (0, import_core16.generateUUIDv7)();
3101
3378
  const now = Date.now();
3102
3379
  for (const [fieldName, descriptor] of Object.entries(definition.fields)) {
3103
3380
  if (descriptor.auto && descriptor.kind === "timestamp") {
@@ -3106,13 +3383,15 @@ var TransactionContext = class {
3106
3383
  }
3107
3384
  const sequenceNumber = await this.config.sequenceAllocator.allocate();
3108
3385
  const causalDeps = this.config.causalTracker?.nextCausalDeps(collectionName, true) ?? [];
3109
- const operation = await (0, import_core14.createOperation)(
3386
+ const operation = await (0, import_core16.createOperation)(
3110
3387
  {
3111
3388
  nodeId: this.config.nodeId,
3112
3389
  type: "insert",
3113
3390
  collection: collectionName,
3114
3391
  recordId,
3115
- data: { ...validated },
3392
+ // Binary richtext values are tagged as canonical JSON before the
3393
+ // operation is content-hashed (see encodeRichtextFieldsForOpData).
3394
+ data: encodeRichtextFieldsForOpData(validated, definition.fields),
3116
3395
  previousData: null,
3117
3396
  sequenceNumber,
3118
3397
  causalDeps,
@@ -3159,7 +3438,7 @@ var TransactionContext = class {
3159
3438
  if (!currentRecord) {
3160
3439
  throw new RecordNotFoundError(collectionName, id);
3161
3440
  }
3162
- const validated = (0, import_core14.validateRecord)(collectionName, definition, data, "update");
3441
+ const validated = (0, import_core16.validateRecord)(collectionName, definition, data, "update");
3163
3442
  const now = Date.now();
3164
3443
  const previousData = {};
3165
3444
  const resolvedData = {};
@@ -3167,9 +3446,9 @@ var TransactionContext = class {
3167
3446
  for (const key of Object.keys(validated)) {
3168
3447
  const value = validated[key];
3169
3448
  previousData[key] = currentRecord[key];
3170
- if ((0, import_core14.isAtomicOp)(value)) {
3171
- resolvedData[key] = (0, import_core14.resolveAtomicOp)(currentRecord[key], value);
3172
- atomicOps[key] = (0, import_core14.toAtomicOp)(value);
3449
+ if ((0, import_core16.isAtomicOp)(value)) {
3450
+ resolvedData[key] = (0, import_core16.resolveAtomicOp)(currentRecord[key], value);
3451
+ atomicOps[key] = (0, import_core16.toAtomicOp)(value);
3173
3452
  } else {
3174
3453
  resolvedData[key] = value;
3175
3454
  }
@@ -3177,14 +3456,16 @@ var TransactionContext = class {
3177
3456
  const hasAtomicOps = Object.keys(atomicOps).length > 0;
3178
3457
  const sequenceNumber = await this.config.sequenceAllocator.allocate();
3179
3458
  const causalDeps = this.config.causalTracker?.nextCausalDeps(collectionName, true) ?? [];
3180
- const operation = await (0, import_core14.createOperation)(
3459
+ const operation = await (0, import_core16.createOperation)(
3181
3460
  {
3182
3461
  nodeId: this.config.nodeId,
3183
3462
  type: "update",
3184
3463
  collection: collectionName,
3185
3464
  recordId: id,
3186
- data: { ...resolvedData },
3187
- previousData,
3465
+ // Binary richtext values (new and previous) are tagged as canonical
3466
+ // JSON before the operation is content-hashed.
3467
+ data: encodeRichtextFieldsForOpData(resolvedData, definition.fields),
3468
+ previousData: encodeRichtextFieldsForOpData(previousData, definition.fields),
3188
3469
  sequenceNumber,
3189
3470
  causalDeps,
3190
3471
  schemaVersion: this.config.schema.version,
@@ -3230,7 +3511,7 @@ var TransactionContext = class {
3230
3511
  const now = Date.now();
3231
3512
  const sequenceNumber = await this.config.sequenceAllocator.allocate();
3232
3513
  const causalDeps = this.config.causalTracker?.nextCausalDeps(collectionName, true) ?? [];
3233
- const operation = await (0, import_core14.createOperation)(
3514
+ const operation = await (0, import_core16.createOperation)(
3234
3515
  {
3235
3516
  nodeId: this.config.nodeId,
3236
3517
  type: "delete",
@@ -3291,14 +3572,16 @@ var TransactionContext = class {
3291
3572
  if (entry.operation.type === "insert" && entry.operation.data) {
3292
3573
  record = {
3293
3574
  id,
3294
- ...entry.operation.data,
3575
+ // op.data stores binary richtext as tagged JSON; the effective
3576
+ // record must expose record-shaped values (Uint8Array/string).
3577
+ ...decodeRichtextFieldsFromOpData(entry.operation.data, definition.fields),
3295
3578
  createdAt: entry.operation.timestamp.wallTime,
3296
3579
  updatedAt: entry.operation.timestamp.wallTime
3297
3580
  };
3298
3581
  } else if (entry.operation.type === "update" && entry.operation.data && record) {
3299
3582
  record = {
3300
3583
  ...record,
3301
- ...entry.operation.data,
3584
+ ...decodeRichtextFieldsFromOpData(entry.operation.data, definition.fields),
3302
3585
  updatedAt: entry.operation.timestamp.wallTime
3303
3586
  };
3304
3587
  } else if (entry.operation.type === "delete") {
@@ -3333,7 +3616,7 @@ var TransactionSequenceAllocator = class {
3333
3616
  };
3334
3617
 
3335
3618
  // src/store/tab-node-id.ts
3336
- var import_core15 = require("@korajs/core");
3619
+ var import_core17 = require("@korajs/core");
3337
3620
  function perTabStorageKey(dbName) {
3338
3621
  return `kora-tab-node-${dbName}`;
3339
3622
  }
@@ -3354,7 +3637,7 @@ function resolvePerTabNodeId(dbName) {
3354
3637
  if (existing) {
3355
3638
  return existing;
3356
3639
  }
3357
- const nodeId = (0, import_core15.generateUUIDv7)();
3640
+ const nodeId = (0, import_core17.generateUUIDv7)();
3358
3641
  savePerTabNodeId(dbName, nodeId);
3359
3642
  return nodeId;
3360
3643
  }
@@ -3364,7 +3647,7 @@ var Store = class {
3364
3647
  opened = false;
3365
3648
  nodeId = "";
3366
3649
  sequenceNumber = 0;
3367
- versionVector = (0, import_core17.createVersionVector)();
3650
+ versionVector = (0, import_core19.createVersionVector)();
3368
3651
  clock = null;
3369
3652
  collections = /* @__PURE__ */ new Map();
3370
3653
  subscriptionManager;
@@ -3398,8 +3681,8 @@ var Store = class {
3398
3681
  await this.adapter.open(this.schema);
3399
3682
  await this.runMigrationsIfNeeded();
3400
3683
  this.nodeId = await this.loadOrGenerateNodeId();
3401
- this.clock = new import_core17.HybridLogicalClock(this.nodeId);
3402
- this.causalTracker = new import_core17.CausalTracker();
3684
+ this.clock = new import_core19.HybridLogicalClock(this.nodeId);
3685
+ this.causalTracker = new import_core19.CausalTracker();
3403
3686
  this.sequenceManager = new SequenceManager(this.adapter, this.nodeId);
3404
3687
  this.sequenceNumber = await this.loadSequenceNumber();
3405
3688
  this.versionVector = await this.loadVersionVector();
@@ -3485,6 +3768,14 @@ var Store = class {
3485
3768
  /**
3486
3769
  * Get the node ID for this store instance.
3487
3770
  */
3771
+ /**
3772
+ * Records the offset between server time and this device's clock
3773
+ * (serverTime - localTime), learned at sync handshake. Lets the store's HLC
3774
+ * validate remote timestamps correctly even when the local clock is wrong.
3775
+ */
3776
+ setClockReferenceOffset(offsetMs) {
3777
+ this.clock?.setReferenceOffset(offsetMs);
3778
+ }
3488
3779
  getNodeId() {
3489
3780
  this.ensureOpen();
3490
3781
  return this.nodeId;
@@ -3511,45 +3802,158 @@ var Store = class {
3511
3802
  if (this.clock) {
3512
3803
  this.clock.receive(op.timestamp);
3513
3804
  }
3514
- const remoteVersion = serializeRowVersion(op.timestamp);
3515
- const wallTime = op.timestamp.wallTime;
3805
+ const materializeTimestamp = options?.materializeTimestamp ?? op.timestamp;
3806
+ const remoteVersion = serializeRowVersion(materializeTimestamp);
3807
+ const wallTime = materializeTimestamp.wallTime;
3808
+ const materializeSource = options?.materializeData ?? op.data;
3516
3809
  await this.adapter.transaction(async (tx) => {
3517
- if (op.type === "insert" && op.data) {
3810
+ const checkGuard = (row) => {
3811
+ const guard = options?.guardRowState;
3812
+ if (!guard) {
3813
+ return;
3814
+ }
3815
+ const version = typeof row?._version === "string" ? row._version : null;
3816
+ const fieldVersions = typeof row?._field_versions === "string" ? row._field_versions : null;
3817
+ if (version !== guard.version || fieldVersions !== guard.fieldVersions) {
3818
+ throw new OptimisticLockError(collection, op.recordId);
3819
+ }
3820
+ };
3821
+ if (op.type === "insert" && materializeSource) {
3822
+ const serializedData = serializeRecord(materializeSource, definition.fields);
3518
3823
  const existing2 = await tx.query(
3519
- `SELECT _updated_at, _version FROM ${collection} WHERE id = ?`,
3824
+ `SELECT _updated_at, _version, _field_versions, _deleted FROM ${collection} WHERE id = ?`,
3520
3825
  [op.recordId]
3521
3826
  );
3522
3827
  const row = existing2[0];
3523
- if (row && !isIncomingNewerThanRow(op.timestamp, row)) {
3524
- } else {
3525
- const serializedData = serializeRecord(op.data, definition.fields);
3828
+ checkGuard(row);
3829
+ if (!row) {
3526
3830
  const record = {
3527
3831
  id: op.recordId,
3528
3832
  ...serializedData,
3529
3833
  _created_at: wallTime,
3530
3834
  _updated_at: wallTime,
3531
- _version: remoteVersion
3835
+ _version: remoteVersion,
3836
+ // Stamp every inserted field so per-field LWW has a real baseline.
3837
+ _field_versions: serializeFieldVersions(
3838
+ fieldVersionsForFields(Object.keys(serializedData), remoteVersion)
3839
+ )
3532
3840
  };
3533
3841
  const insertQuery = buildInsertQuery(collection, record);
3534
3842
  await tx.execute(insertQuery.sql, insertQuery.params);
3843
+ await this.foldOrphanedOperations(tx, collection, definition, op.recordId);
3844
+ } else if (row._deleted === 1) {
3845
+ if (isIncomingNewerThanRow(materializeTimestamp, row)) {
3846
+ const fieldChanges = {
3847
+ ...serializedData,
3848
+ _deleted: 0,
3849
+ _field_versions: serializeFieldVersions(
3850
+ fieldVersionsForFields(Object.keys(serializedData), remoteVersion)
3851
+ )
3852
+ };
3853
+ const upsert = buildFieldFastForwardUpdateQuery(
3854
+ collection,
3855
+ op.recordId,
3856
+ fieldChanges,
3857
+ remoteVersion,
3858
+ wallTime,
3859
+ { maxCreatedAt: wallTime }
3860
+ );
3861
+ await tx.execute(upsert.sql, upsert.params);
3862
+ }
3863
+ } else {
3864
+ const currentVersions = parseFieldVersions(row._field_versions);
3865
+ const rowVersion = typeof row._version === "string" ? row._version : void 0;
3866
+ const fieldChanges = {};
3867
+ let fieldVersionsJson;
3868
+ if (options?.forceMaterialize) {
3869
+ const merged = { ...currentVersions };
3870
+ for (const field of Object.keys(serializedData)) {
3871
+ fieldChanges[field] = serializedData[field];
3872
+ merged[field] = remoteVersion;
3873
+ }
3874
+ fieldVersionsJson = serializeFieldVersions(merged);
3875
+ } else {
3876
+ const { winners, merged } = resolvePerFieldLww(
3877
+ currentVersions,
3878
+ Object.keys(serializedData),
3879
+ remoteVersion,
3880
+ rowVersion
3881
+ );
3882
+ for (const field of winners) {
3883
+ fieldChanges[field] = serializedData[field];
3884
+ }
3885
+ fieldVersionsJson = serializeFieldVersions(merged);
3886
+ }
3887
+ const upsert = buildFieldFastForwardUpdateQuery(
3888
+ collection,
3889
+ op.recordId,
3890
+ { ...fieldChanges, _field_versions: fieldVersionsJson },
3891
+ remoteVersion,
3892
+ wallTime,
3893
+ { maxCreatedAt: wallTime }
3894
+ );
3895
+ await tx.execute(upsert.sql, upsert.params);
3535
3896
  }
3536
- } else if (op.type === "update" && op.data) {
3537
- const serializedChanges = serializeRecord(op.data, definition.fields);
3538
- const updatePayload = {
3539
- ...serializedChanges,
3540
- _updated_at: wallTime,
3541
- _version: remoteVersion
3542
- };
3543
- if (options?.reactivateIfDeleted) {
3544
- updatePayload._deleted = 0;
3545
- }
3546
- const updateQuery = buildLwwUpdateQuery(
3547
- collection,
3548
- op.recordId,
3549
- updatePayload,
3550
- remoteVersion
3897
+ } else if (op.type === "update" && materializeSource) {
3898
+ const serializedChanges = serializeRecord(materializeSource, definition.fields);
3899
+ const changedFields = Object.keys(serializedChanges);
3900
+ const currentRows = await tx.query(
3901
+ `SELECT _version, _field_versions FROM ${collection} WHERE id = ?`,
3902
+ [op.recordId]
3551
3903
  );
3552
- await tx.execute(updateQuery.sql, updateQuery.params);
3904
+ const currentRow = currentRows[0];
3905
+ checkGuard(currentRow);
3906
+ if (currentRow) {
3907
+ const currentVersions = parseFieldVersions(currentRow._field_versions);
3908
+ const rowVersion = typeof currentRow._version === "string" ? currentRow._version : void 0;
3909
+ if (options?.forceMaterialize) {
3910
+ const merged = { ...currentVersions };
3911
+ for (const field of changedFields) {
3912
+ merged[field] = remoteVersion;
3913
+ }
3914
+ const fieldChanges = {
3915
+ ...serializedChanges,
3916
+ _field_versions: serializeFieldVersions(merged)
3917
+ };
3918
+ if (options?.reactivateIfDeleted) {
3919
+ fieldChanges._deleted = 0;
3920
+ }
3921
+ const forceQuery = buildFieldFastForwardUpdateQuery(
3922
+ collection,
3923
+ op.recordId,
3924
+ fieldChanges,
3925
+ remoteVersion,
3926
+ wallTime
3927
+ );
3928
+ await tx.execute(forceQuery.sql, forceQuery.params);
3929
+ } else {
3930
+ const { winners, merged } = resolvePerFieldLww(
3931
+ currentVersions,
3932
+ changedFields,
3933
+ remoteVersion,
3934
+ rowVersion
3935
+ );
3936
+ if (winners.length > 0 || options?.reactivateIfDeleted) {
3937
+ const winningChanges = {
3938
+ _field_versions: serializeFieldVersions(merged)
3939
+ };
3940
+ for (const field of winners) {
3941
+ winningChanges[field] = serializedChanges[field];
3942
+ }
3943
+ if (options?.reactivateIfDeleted) {
3944
+ winningChanges._deleted = 0;
3945
+ }
3946
+ const forceQuery = buildFieldFastForwardUpdateQuery(
3947
+ collection,
3948
+ op.recordId,
3949
+ winningChanges,
3950
+ remoteVersion,
3951
+ wallTime
3952
+ );
3953
+ await tx.execute(forceQuery.sql, forceQuery.params);
3954
+ }
3955
+ }
3956
+ }
3553
3957
  } else if (op.type === "delete") {
3554
3958
  const deleteQuery = buildLwwSoftDeleteQuery(
3555
3959
  collection,
@@ -3577,6 +3981,67 @@ var Store = class {
3577
3981
  this.subscriptionManager.notify(collection, op);
3578
3982
  return "applied";
3579
3983
  }
3984
+ /**
3985
+ * Materialize update/delete operations that were logged for a record BEFORE
3986
+ * its insert arrived (reordered delivery). Runs inside the insert's write
3987
+ * transaction, folding each orphan in timestamp order through the exact
3988
+ * per-field LWW / LWW-delete rules the normal apply paths use, so the final
3989
+ * row state equals what a device that received the operations in causal
3990
+ * order computed. Called only when the insert created a previously-absent
3991
+ * row — in that case every logged update/delete for the record is
3992
+ * necessarily an orphan (rows are soft-deleted, never removed, so a row
3993
+ * that is absent now was never materialized).
3994
+ */
3995
+ async foldOrphanedOperations(tx, collection, definition, recordId) {
3996
+ const orphanRows = await tx.query(
3997
+ `SELECT * FROM _kora_ops_${collection} WHERE record_id = ? AND type IN ('update', 'delete')`,
3998
+ [recordId]
3999
+ );
4000
+ if (orphanRows.length === 0) {
4001
+ return;
4002
+ }
4003
+ const orphans = orphanRows.map((r) => deserializeOperationWithCollection(r, collection)).sort((a, b) => import_core19.HybridLogicalClock.compare(a.timestamp, b.timestamp));
4004
+ for (const orphan of orphans) {
4005
+ const orphanVersion = serializeRowVersion(orphan.timestamp);
4006
+ const orphanWall = orphan.timestamp.wallTime;
4007
+ if (orphan.type === "update" && orphan.data) {
4008
+ const orphanChanges = serializeRecord(orphan.data, definition.fields);
4009
+ const rows = await tx.query(
4010
+ `SELECT _version, _field_versions FROM ${collection} WHERE id = ?`,
4011
+ [recordId]
4012
+ );
4013
+ const current = rows[0];
4014
+ if (!current) {
4015
+ continue;
4016
+ }
4017
+ const { winners, merged } = resolvePerFieldLww(
4018
+ parseFieldVersions(current._field_versions),
4019
+ Object.keys(orphanChanges),
4020
+ orphanVersion,
4021
+ typeof current._version === "string" ? current._version : void 0
4022
+ );
4023
+ if (winners.length > 0) {
4024
+ const winningChanges = {
4025
+ _field_versions: serializeFieldVersions(merged)
4026
+ };
4027
+ for (const field of winners) {
4028
+ winningChanges[field] = orphanChanges[field];
4029
+ }
4030
+ const query = buildFieldFastForwardUpdateQuery(
4031
+ collection,
4032
+ recordId,
4033
+ winningChanges,
4034
+ orphanVersion,
4035
+ orphanWall
4036
+ );
4037
+ await tx.execute(query.sql, query.params);
4038
+ }
4039
+ } else if (orphan.type === "delete") {
4040
+ const query = buildLwwSoftDeleteQuery(collection, recordId, orphanWall, orphanVersion);
4041
+ await tx.execute(query.sql, query.params);
4042
+ }
4043
+ }
4044
+ }
3580
4045
  /**
3581
4046
  * Get operations from a node within a sequence number range.
3582
4047
  * Implements the OperationLog interface for computeDelta.
@@ -3732,6 +4197,31 @@ var Store = class {
3732
4197
  deleted: row._deleted === 1
3733
4198
  };
3734
4199
  }
4200
+ /**
4201
+ * Raw version state of a materialized row, for optimistic-concurrency guarded
4202
+ * applies: capture this snapshot before computing a merge, pass it as
4203
+ * `guardRowState`, and the apply refuses to write if the row changed since.
4204
+ * Returns null when no row exists (which is itself a valid guard state — the
4205
+ * apply then requires the row to still be absent).
4206
+ */
4207
+ async getRowVersionState(collection, recordId) {
4208
+ this.ensureOpen();
4209
+ if (!this.schema.collections[collection]) {
4210
+ return null;
4211
+ }
4212
+ const rows = await this.adapter.query(
4213
+ `SELECT _version, _field_versions FROM ${collection} WHERE id = ?`,
4214
+ [recordId]
4215
+ );
4216
+ const row = rows[0];
4217
+ if (!row) {
4218
+ return null;
4219
+ }
4220
+ return {
4221
+ version: typeof row._version === "string" ? row._version : null,
4222
+ fieldVersions: typeof row._field_versions === "string" ? row._field_versions : null
4223
+ };
4224
+ }
3735
4225
  /**
3736
4226
  * Latest operation from this device for a record (used for delete-vs-update merge on sync).
3737
4227
  */
@@ -3774,6 +4264,30 @@ var Store = class {
3774
4264
  (nodeId, fromSeq, toSeq) => this.getOperationRange(nodeId, fromSeq, toSeq)
3775
4265
  );
3776
4266
  }
4267
+ /**
4268
+ * Re-stamp never-acknowledged local operations after a fast device clock was
4269
+ * corrected, so sync can resume immediately. Safe because unacknowledged
4270
+ * operations are private to this device (like unpushed git commits);
4271
+ * acknowledged/shared operations are immutable and never touched.
4272
+ *
4273
+ * After the rewrite the store's HLC is advanced past the highest new
4274
+ * timestamp so subsequent local writes keep sorting after the rebased ops.
4275
+ * No data-change notifications are emitted: materialized values are
4276
+ * unchanged, only version stamps move.
4277
+ */
4278
+ async rebaseUnsyncedOperations(unsyncedOpIds, correctedNowMs) {
4279
+ this.ensureOpen();
4280
+ const result = await rebaseUnsyncedOperationsInLog(
4281
+ this.adapter,
4282
+ this.schema,
4283
+ unsyncedOpIds,
4284
+ correctedNowMs
4285
+ );
4286
+ if (result.newMaxTimestamp && this.clock) {
4287
+ this.clock.advanceTo(result.newMaxTimestamp);
4288
+ }
4289
+ return result;
4290
+ }
3777
4291
  /**
3778
4292
  * Count of local operations ahead of the server version vector.
3779
4293
  */
@@ -3788,7 +4302,7 @@ var Store = class {
3788
4302
  async compact(strategy) {
3789
4303
  this.ensureOpen();
3790
4304
  if (strategy.mode === "never") {
3791
- return compactOperationLog(this.adapter, this.schema, strategy, (0, import_core17.createVersionVector)());
4305
+ return compactOperationLog(this.adapter, this.schema, strategy, (0, import_core19.createVersionVector)());
3792
4306
  }
3793
4307
  const serverVector = strategy.serverVector ?? await loadLastAckedServerVector(this.adapter);
3794
4308
  return compactOperationLog(this.adapter, this.schema, strategy, serverVector);
@@ -3823,7 +4337,7 @@ var Store = class {
3823
4337
  let latest = null;
3824
4338
  for (const row of rows) {
3825
4339
  const op = deserializeOperationWithCollection(row, collection);
3826
- if (!latest || import_core17.HybridLogicalClock.compare(op.timestamp, latest.timestamp) > 0) {
4340
+ if (!latest || import_core19.HybridLogicalClock.compare(op.timestamp, latest.timestamp) > 0) {
3827
4341
  latest = op;
3828
4342
  }
3829
4343
  }
@@ -3966,7 +4480,7 @@ var Store = class {
3966
4480
  for (let v = storedVersion + 1; v <= targetVersion; v++) {
3967
4481
  const migration = migrations[v];
3968
4482
  if (!migration) continue;
3969
- const sqlStatements = (0, import_core17.migrationStepsToSQL)(migration.steps);
4483
+ const sqlStatements = (0, import_core19.migrationStepsToSQL)(migration.steps);
3970
4484
  for (const sql of sqlStatements) {
3971
4485
  try {
3972
4486
  await this.adapter.execute(sql);
@@ -4044,7 +4558,7 @@ var Store = class {
4044
4558
  if (rows[0]) {
4045
4559
  return rows[0].value;
4046
4560
  }
4047
- const newNodeId = (0, import_core17.generateUUIDv7)();
4561
+ const newNodeId = (0, import_core19.generateUUIDv7)();
4048
4562
  await this.adapter.execute("INSERT INTO _kora_meta (key, value) VALUES ('node_id', ?)", [
4049
4563
  newNodeId
4050
4564
  ]);
@@ -4061,7 +4575,7 @@ var Store = class {
4061
4575
  const rows = await this.adapter.query(
4062
4576
  "SELECT node_id, sequence_number FROM _kora_version_vector"
4063
4577
  );
4064
- const vector = (0, import_core17.createVersionVector)();
4578
+ const vector = (0, import_core19.createVersionVector)();
4065
4579
  for (const row of rows) {
4066
4580
  vector.set(row.node_id, row.sequence_number);
4067
4581
  }
@@ -4074,6 +4588,483 @@ var Store = class {
4074
4588
  }
4075
4589
  };
4076
4590
 
4591
+ // src/reactivity/query-store.ts
4592
+ var EMPTY_ARRAY = Object.freeze([]);
4593
+ var QueryStore = class {
4594
+ snapshot = EMPTY_ARRAY;
4595
+ listeners = /* @__PURE__ */ new Set();
4596
+ unsubscribeQuery = null;
4597
+ active = false;
4598
+ queryBuilder;
4599
+ constructor(queryBuilder) {
4600
+ this.queryBuilder = queryBuilder;
4601
+ }
4602
+ /**
4603
+ * Subscribe to snapshot changes.
4604
+ *
4605
+ * @returns Unsubscribe function
4606
+ */
4607
+ subscribe = (onStoreChange) => {
4608
+ this.listeners.add(onStoreChange);
4609
+ if (!this.active) {
4610
+ this.startSubscription();
4611
+ }
4612
+ return () => {
4613
+ this.listeners.delete(onStoreChange);
4614
+ if (this.listeners.size === 0) {
4615
+ this.stopSubscription();
4616
+ }
4617
+ };
4618
+ };
4619
+ /** Synchronous read of the latest query results. */
4620
+ getSnapshot = () => {
4621
+ return this.snapshot;
4622
+ };
4623
+ /** Tear down listeners and the underlying query subscription. */
4624
+ destroy() {
4625
+ this.stopSubscription();
4626
+ this.listeners.clear();
4627
+ this.snapshot = EMPTY_ARRAY;
4628
+ }
4629
+ startSubscription() {
4630
+ this.active = true;
4631
+ this.unsubscribeQuery = this.queryBuilder.subscribe((results) => {
4632
+ if (!this.active) return;
4633
+ this.snapshot = Object.freeze([...results]);
4634
+ this.notifyListeners();
4635
+ });
4636
+ }
4637
+ stopSubscription() {
4638
+ this.active = false;
4639
+ if (this.unsubscribeQuery) {
4640
+ this.unsubscribeQuery();
4641
+ this.unsubscribeQuery = null;
4642
+ }
4643
+ }
4644
+ notifyListeners() {
4645
+ for (const listener of this.listeners) {
4646
+ listener();
4647
+ }
4648
+ }
4649
+ };
4650
+
4651
+ // src/reactivity/assert-query-ready.ts
4652
+ var import_core20 = require("@korajs/core");
4653
+ var LEGACY_PENDING_COLLECTION = "__pending__";
4654
+ function assertQueryReady(query) {
4655
+ const descriptor = query.getDescriptor();
4656
+ if (descriptor.collection === LEGACY_PENDING_COLLECTION) {
4657
+ throw new import_core20.AppNotReadyError(
4658
+ "Cannot use useQuery() before app.ready. Await app.ready or wrap your UI in <KoraProvider app={app}>."
4659
+ );
4660
+ }
4661
+ }
4662
+
4663
+ // src/reactivity/query-store-cache.ts
4664
+ var QueryStoreCache = class {
4665
+ constructor(scopeKey = "default") {
4666
+ this.scopeKey = scopeKey;
4667
+ }
4668
+ scopeKey;
4669
+ entries = /* @__PURE__ */ new Map();
4670
+ getOrCreate(queryBuilder) {
4671
+ const key = this.getKey(queryBuilder);
4672
+ const existing = this.entries.get(key);
4673
+ if (existing) {
4674
+ existing.refCount++;
4675
+ return existing.queryStore;
4676
+ }
4677
+ const queryStore = new QueryStore(queryBuilder);
4678
+ this.entries.set(key, {
4679
+ queryStore,
4680
+ refCount: 1
4681
+ });
4682
+ return queryStore;
4683
+ }
4684
+ release(queryBuilder) {
4685
+ const key = this.getKey(queryBuilder);
4686
+ const entry = this.entries.get(key);
4687
+ if (!entry) {
4688
+ return;
4689
+ }
4690
+ entry.refCount--;
4691
+ if (entry.refCount <= 0) {
4692
+ entry.queryStore.destroy();
4693
+ this.entries.delete(key);
4694
+ }
4695
+ }
4696
+ clear() {
4697
+ for (const entry of this.entries.values()) {
4698
+ entry.queryStore.destroy();
4699
+ }
4700
+ this.entries.clear();
4701
+ }
4702
+ get size() {
4703
+ return this.entries.size;
4704
+ }
4705
+ getKey(queryBuilder) {
4706
+ return `${this.scopeKey}:${JSON.stringify(queryBuilder.getDescriptor())}`;
4707
+ }
4708
+ };
4709
+ var sharedCache = null;
4710
+ function getSharedQueryStoreCache() {
4711
+ if (!sharedCache) {
4712
+ sharedCache = new QueryStoreCache();
4713
+ }
4714
+ return sharedCache;
4715
+ }
4716
+
4717
+ // src/richtext/create-richtext-controller.ts
4718
+ var Y2 = __toESM(require("yjs"), 1);
4719
+ init_richtext_serializer();
4720
+ var LOAD_ORIGIN = "kora-load";
4721
+ var REMOTE_ORIGIN = "kora-remote";
4722
+ var DOC_CHANNEL_ORIGIN = "kora-doc-channel";
4723
+ var TEXT_KEY2 = "content";
4724
+ var COMPACT_AFTER_DELTAS = 20;
4725
+ var PERSIST_DEBOUNCE_MS = 400;
4726
+ function createRichTextController(options) {
4727
+ const {
4728
+ collection,
4729
+ collectionName,
4730
+ recordId,
4731
+ fieldName,
4732
+ store,
4733
+ syncEngine = null,
4734
+ useDocChannel
4735
+ } = options;
4736
+ const doc = new Y2.Doc();
4737
+ const text = doc.getText(TEXT_KEY2);
4738
+ const undoManager = new Y2.UndoManager(text);
4739
+ let user = options.user;
4740
+ let disposed = false;
4741
+ let ready = false;
4742
+ let error = null;
4743
+ let canUndo = false;
4744
+ let canRedo = false;
4745
+ let cursors = [];
4746
+ const listeners = /* @__PURE__ */ new Set();
4747
+ let snapshot = {
4748
+ ready: false,
4749
+ error: null,
4750
+ canUndo: false,
4751
+ canRedo: false,
4752
+ cursors: []
4753
+ };
4754
+ const baseUpdateRef = { current: null };
4755
+ const pendingDeltasRef = { current: [] };
4756
+ const docChannelActiveRef = { current: false };
4757
+ let persistTimer = null;
4758
+ let recordUnsubscribe = null;
4759
+ let docChannelUnsubscribe = null;
4760
+ let awarenessUnsubscribe = null;
4761
+ const refreshSnapshot = () => {
4762
+ snapshot = {
4763
+ ready,
4764
+ error,
4765
+ canUndo,
4766
+ canRedo,
4767
+ cursors: [...cursors]
4768
+ };
4769
+ };
4770
+ const notify = () => {
4771
+ refreshSnapshot();
4772
+ for (const listener of listeners) {
4773
+ listener();
4774
+ }
4775
+ };
4776
+ const syncHistoryState = () => {
4777
+ canUndo = undoManager.undoStack.length > 0;
4778
+ canRedo = undoManager.redoStack.length > 0;
4779
+ notify();
4780
+ };
4781
+ const getSnapshot = () => snapshot;
4782
+ const resolveAwarenessUser = () => {
4783
+ if (user) {
4784
+ return user;
4785
+ }
4786
+ if (syncEngine) {
4787
+ const existing = syncEngine.getAwarenessManager().getLocalState()?.user;
4788
+ if (existing) {
4789
+ return existing;
4790
+ }
4791
+ }
4792
+ return {
4793
+ name: "Anonymous",
4794
+ color: "#6366f1"
4795
+ };
4796
+ };
4797
+ const setCursor = (anchor, head) => {
4798
+ if (!syncEngine) {
4799
+ return;
4800
+ }
4801
+ const awareness = syncEngine.getAwarenessManager();
4802
+ const state = {
4803
+ user: resolveAwarenessUser(),
4804
+ cursor: {
4805
+ collection: collectionName,
4806
+ recordId,
4807
+ field: fieldName,
4808
+ anchor,
4809
+ head
4810
+ }
4811
+ };
4812
+ awareness.setLocalState(state);
4813
+ };
4814
+ const clearCursor = () => {
4815
+ if (!syncEngine) {
4816
+ return;
4817
+ }
4818
+ const awareness = syncEngine.getAwarenessManager();
4819
+ const current = awareness.getLocalState();
4820
+ if (!current?.cursor) {
4821
+ return;
4822
+ }
4823
+ if (current.cursor.collection !== collectionName || current.cursor.recordId !== recordId || current.cursor.field !== fieldName) {
4824
+ return;
4825
+ }
4826
+ awareness.setLocalState({ user: current.user });
4827
+ };
4828
+ const applyRemoteSnapshot = (value) => {
4829
+ const encoded = encodeRichtextInput(value);
4830
+ if (!encoded) {
4831
+ return;
4832
+ }
4833
+ const currentSnapshot = Y2.encodeStateAsUpdate(doc);
4834
+ if (updatesEqual(currentSnapshot, encoded)) {
4835
+ return;
4836
+ }
4837
+ Y2.applyUpdate(doc, encoded, REMOTE_ORIGIN);
4838
+ baseUpdateRef.current = encoded;
4839
+ pendingDeltasRef.current = [];
4840
+ syncHistoryState();
4841
+ };
4842
+ const flushPersist = async () => {
4843
+ const snapshot2 = composeRichtextSnapshot(baseUpdateRef.current, pendingDeltasRef.current);
4844
+ if (pendingDeltasRef.current.length >= COMPACT_AFTER_DELTAS) {
4845
+ baseUpdateRef.current = snapshot2;
4846
+ pendingDeltasRef.current = [];
4847
+ }
4848
+ try {
4849
+ await collection.update(recordId, {
4850
+ [fieldName]: snapshot2
4851
+ });
4852
+ } catch (cause) {
4853
+ if (!disposed) {
4854
+ error = cause instanceof Error ? cause : new Error(String(cause));
4855
+ notify();
4856
+ }
4857
+ }
4858
+ };
4859
+ const schedulePersist = () => {
4860
+ if (persistTimer) {
4861
+ clearTimeout(persistTimer);
4862
+ }
4863
+ persistTimer = setTimeout(() => {
4864
+ persistTimer = null;
4865
+ void flushPersist();
4866
+ }, PERSIST_DEBOUNCE_MS);
4867
+ };
4868
+ const onDocUpdate = (update, origin) => {
4869
+ syncHistoryState();
4870
+ if (origin === LOAD_ORIGIN || origin === REMOTE_ORIGIN || origin === DOC_CHANNEL_ORIGIN) {
4871
+ return;
4872
+ }
4873
+ pendingDeltasRef.current.push(update);
4874
+ if (docChannelActiveRef.current && syncEngine) {
4875
+ syncEngine.getRichtextDocChannel?.().send(collectionName, recordId, fieldName, update);
4876
+ schedulePersist();
4877
+ return;
4878
+ }
4879
+ void flushPersist();
4880
+ };
4881
+ const subscribeRecordChanges = () => {
4882
+ recordUnsubscribe?.();
4883
+ recordUnsubscribe = store.collection(collectionName).where({ id: recordId }).subscribe((results) => {
4884
+ const record = results[0];
4885
+ if (!record) {
4886
+ return;
4887
+ }
4888
+ applyRemoteSnapshot(record[fieldName]);
4889
+ });
4890
+ };
4891
+ const subscribeDocChannel = () => {
4892
+ docChannelUnsubscribe?.();
4893
+ if (!ready || !syncEngine || !docChannelActiveRef.current) {
4894
+ return;
4895
+ }
4896
+ const channel = syncEngine.getRichtextDocChannel?.();
4897
+ if (!channel) {
4898
+ return;
4899
+ }
4900
+ docChannelUnsubscribe = channel.subscribe(collectionName, recordId, fieldName, (update) => {
4901
+ Y2.applyUpdate(doc, update, DOC_CHANNEL_ORIGIN);
4902
+ syncHistoryState();
4903
+ });
4904
+ };
4905
+ const updateCursors = () => {
4906
+ if (!syncEngine) {
4907
+ cursors = [];
4908
+ notify();
4909
+ return;
4910
+ }
4911
+ const awareness = syncEngine.getAwarenessManager();
4912
+ const localClientId = awareness.clientId;
4913
+ const states = awareness.getStates();
4914
+ const fieldCursors = [];
4915
+ for (const [clientId, state] of states) {
4916
+ if (clientId === localClientId) continue;
4917
+ if (!state.cursor) continue;
4918
+ if (state.cursor.collection !== collectionName || state.cursor.recordId !== recordId || state.cursor.field !== fieldName) {
4919
+ continue;
4920
+ }
4921
+ fieldCursors.push({
4922
+ clientId,
4923
+ userName: state.user.name,
4924
+ color: state.user.color,
4925
+ anchor: state.cursor.anchor,
4926
+ head: state.cursor.head
4927
+ });
4928
+ }
4929
+ cursors = fieldCursors;
4930
+ notify();
4931
+ };
4932
+ const subscribeAwareness = () => {
4933
+ awarenessUnsubscribe?.();
4934
+ if (!syncEngine) {
4935
+ cursors = [];
4936
+ notify();
4937
+ return;
4938
+ }
4939
+ const awareness = syncEngine.getAwarenessManager();
4940
+ awarenessUnsubscribe = awareness.on("change", () => {
4941
+ updateCursors();
4942
+ });
4943
+ updateCursors();
4944
+ };
4945
+ const initialize = async () => {
4946
+ ready = false;
4947
+ error = null;
4948
+ notify();
4949
+ try {
4950
+ const record = await collection.findById(recordId);
4951
+ if (disposed) return;
4952
+ doc.transact(() => {
4953
+ const target = doc.getText(TEXT_KEY2);
4954
+ target.delete(0, target.length);
4955
+ }, LOAD_ORIGIN);
4956
+ const encoded = encodeRichtextInput(record?.[fieldName]);
4957
+ baseUpdateRef.current = encoded;
4958
+ pendingDeltasRef.current = [];
4959
+ if (encoded) {
4960
+ Y2.applyUpdate(doc, encoded, LOAD_ORIGIN);
4961
+ }
4962
+ const channel = syncEngine?.getRichtextDocChannel?.();
4963
+ docChannelActiveRef.current = channel?.shouldUseChannel(encoded?.length ?? 0, useDocChannel) ?? false;
4964
+ ready = true;
4965
+ syncHistoryState();
4966
+ subscribeRecordChanges();
4967
+ subscribeDocChannel();
4968
+ subscribeAwareness();
4969
+ notify();
4970
+ } catch (cause) {
4971
+ if (disposed) return;
4972
+ error = cause instanceof Error ? cause : new Error(String(cause));
4973
+ notify();
4974
+ }
4975
+ };
4976
+ doc.on("update", onDocUpdate);
4977
+ void initialize();
4978
+ return {
4979
+ doc,
4980
+ text,
4981
+ getSnapshot,
4982
+ subscribe(listener) {
4983
+ listeners.add(listener);
4984
+ return () => {
4985
+ listeners.delete(listener);
4986
+ };
4987
+ },
4988
+ undo() {
4989
+ undoManager.undo();
4990
+ syncHistoryState();
4991
+ },
4992
+ redo() {
4993
+ undoManager.redo();
4994
+ syncHistoryState();
4995
+ },
4996
+ setCursor,
4997
+ clearCursor,
4998
+ setUser(nextUser) {
4999
+ user = nextUser;
5000
+ },
5001
+ destroy() {
5002
+ if (disposed) {
5003
+ return;
5004
+ }
5005
+ disposed = true;
5006
+ if (persistTimer) {
5007
+ clearTimeout(persistTimer);
5008
+ persistTimer = null;
5009
+ }
5010
+ doc.off("update", onDocUpdate);
5011
+ recordUnsubscribe?.();
5012
+ docChannelUnsubscribe?.();
5013
+ awarenessUnsubscribe?.();
5014
+ clearCursor();
5015
+ undoManager.destroy();
5016
+ baseUpdateRef.current = null;
5017
+ pendingDeltasRef.current = [];
5018
+ docChannelActiveRef.current = false;
5019
+ listeners.clear();
5020
+ }
5021
+ };
5022
+ }
5023
+ function encodeRichtextInput(value) {
5024
+ if (value === null || value === void 0) {
5025
+ return null;
5026
+ }
5027
+ try {
5028
+ return encodeRichtext(value);
5029
+ } catch {
5030
+ if (typeof value === "string") {
5031
+ const fallbackDoc = new Y2.Doc();
5032
+ fallbackDoc.getText(TEXT_KEY2).insert(0, value);
5033
+ return Y2.encodeStateAsUpdate(fallbackDoc);
5034
+ }
5035
+ throw new Error("Richtext record value must be a string, Uint8Array, ArrayBuffer, or null.");
5036
+ }
5037
+ }
5038
+ function composeRichtextSnapshot(base, deltas) {
5039
+ const mergedDoc = new Y2.Doc();
5040
+ if (base) {
5041
+ Y2.applyUpdate(mergedDoc, base);
5042
+ }
5043
+ for (const delta of deltas) {
5044
+ Y2.applyUpdate(mergedDoc, delta);
5045
+ }
5046
+ return Y2.encodeStateAsUpdate(mergedDoc);
5047
+ }
5048
+ function updatesEqual(left, right) {
5049
+ if (left.length !== right.length) {
5050
+ return false;
5051
+ }
5052
+ for (let index = 0; index < left.length; index++) {
5053
+ if (left[index] !== right[index]) {
5054
+ return false;
5055
+ }
5056
+ }
5057
+ return true;
5058
+ }
5059
+
5060
+ // src/richtext/as-rich-text-sync-engine.ts
5061
+ function asRichTextSyncEngine(engine) {
5062
+ if (engine == null) {
5063
+ return null;
5064
+ }
5065
+ return engine;
5066
+ }
5067
+
4077
5068
  // src/index.ts
4078
5069
  init_richtext_serializer();
4079
5070
 
@@ -4091,9 +5082,12 @@ init_export_audit();
4091
5082
  Collection,
4092
5083
  InvalidStateTransitionError,
4093
5084
  LAST_ACKED_SERVER_VECTOR_META_KEY,
5085
+ OptimisticLockError,
4094
5086
  PersistenceError,
4095
5087
  QueryBuilder,
4096
5088
  QueryError,
5089
+ QueryStore,
5090
+ QueryStoreCache,
4097
5091
  RecordNotFoundError,
4098
5092
  SequenceManager,
4099
5093
  Store,
@@ -4103,14 +5097,18 @@ init_export_audit();
4103
5097
  TransactionContext,
4104
5098
  WorkerInitError,
4105
5099
  WorkerTimeoutError,
5100
+ asRichTextSyncEngine,
5101
+ assertQueryReady,
4106
5102
  collectOperationsAheadOfServer,
4107
5103
  compactOperationLog,
4108
5104
  computeAckCompactionWatermark,
5105
+ createRichTextController,
4109
5106
  decodeAuditExport,
4110
5107
  decodeRichtext,
4111
5108
  deserializeVersionVectorFromMeta,
4112
5109
  encodeRichtext,
4113
5110
  exportBackup,
5111
+ getSharedQueryStoreCache,
4114
5112
  mergeVersionVectors,
4115
5113
  persistedAuditTraceFromEvent,
4116
5114
  pluralize,