@korajs/store 0.6.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 +615 -106
  35. package/dist/index.cjs.map +1 -1
  36. package/dist/index.d.cts +77 -7
  37. package/dist/index.d.ts +77 -7
  38. package/dist/index.js +423 -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,6 +977,7 @@ __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,
@@ -1000,6 +1038,16 @@ var RecordNotFoundError = class extends import_core.KoraError {
1000
1038
  this.name = "RecordNotFoundError";
1001
1039
  }
1002
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
+ };
1003
1051
  var AdapterError = class extends import_core.KoraError {
1004
1052
  constructor(message, context) {
1005
1053
  super(message, "ADAPTER_ERROR", context);
@@ -1176,27 +1224,27 @@ async function saveDeltaCursor(adapter, cursor) {
1176
1224
  }
1177
1225
 
1178
1226
  // src/store/store.ts
1179
- var import_core17 = require("@korajs/core");
1227
+ var import_core19 = require("@korajs/core");
1180
1228
  init_backup();
1181
1229
 
1182
1230
  // src/mutations/execute-delete.ts
1183
- var import_core7 = require("@korajs/core");
1231
+ var import_core8 = require("@korajs/core");
1184
1232
 
1185
1233
  // src/lww/row-version.ts
1186
- var import_core6 = require("@korajs/core");
1234
+ var import_core7 = require("@korajs/core");
1187
1235
  var EMPTY_ROW_VERSION = "";
1188
1236
  function serializeRowVersion(timestamp) {
1189
- return import_core6.HybridLogicalClock.serialize(timestamp);
1237
+ return import_core7.HybridLogicalClock.serialize(timestamp);
1190
1238
  }
1191
1239
  function rowVersionFromRecord(row) {
1192
1240
  const version = row._version;
1193
1241
  if (typeof version === "string" && version.length > 0) {
1194
- return import_core6.HybridLogicalClock.deserialize(version);
1242
+ return import_core7.HybridLogicalClock.deserialize(version);
1195
1243
  }
1196
1244
  return { wallTime: row._updated_at, logical: 0, nodeId: "" };
1197
1245
  }
1198
1246
  function isIncomingNewerThanRow(incoming, row) {
1199
- return import_core6.HybridLogicalClock.compare(incoming, rowVersionFromRecord(row)) > 0;
1247
+ return import_core7.HybridLogicalClock.compare(incoming, rowVersionFromRecord(row)) > 0;
1200
1248
  }
1201
1249
  function lwwVersionWhereClause(remoteVersion) {
1202
1250
  return {
@@ -1256,6 +1304,28 @@ function buildUpdateQuery(collection, id, changes) {
1256
1304
  const sql = `UPDATE ${collection} SET ${setClauses.join(", ")} WHERE id = ?`;
1257
1305
  return { sql, params };
1258
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
+ }
1259
1329
  function buildSoftDeleteQuery(collection, id, updatedAt, version) {
1260
1330
  if (version !== void 0) {
1261
1331
  return {
@@ -1268,13 +1338,6 @@ function buildSoftDeleteQuery(collection, id, updatedAt, version) {
1268
1338
  params: [updatedAt, id]
1269
1339
  };
1270
1340
  }
1271
- function buildLwwUpdateQuery(collection, id, changes, remoteVersion) {
1272
- const setClauses = Object.keys(changes).map((col) => `${col} = ?`);
1273
- const lww = lwwVersionWhereClause(remoteVersion);
1274
- const sql = `UPDATE ${collection} SET ${setClauses.join(", ")} WHERE id = ? AND ${lww.sql}`;
1275
- const params = [...Object.values(changes), id, ...lww.params];
1276
- return { sql, params };
1277
- }
1278
1341
  function buildLwwSoftDeleteQuery(collection, id, updatedAt, version) {
1279
1342
  const lww = lwwVersionWhereClause(version);
1280
1343
  return {
@@ -1439,7 +1502,7 @@ async function executeDelete(ctx, id, options) {
1439
1502
  await ctx.adapter.transaction(async (tx) => {
1440
1503
  if (!operation) {
1441
1504
  const sequenceNumber = await allocateNextSequenceInTransaction(tx, ctx.nodeId);
1442
- operation = await (0, import_core7.createOperation)(
1505
+ operation = await (0, import_core8.createOperation)(
1443
1506
  {
1444
1507
  nodeId: ctx.nodeId,
1445
1508
  type: "delete",
@@ -1486,11 +1549,65 @@ async function executeDelete(ctx, id, options) {
1486
1549
  }
1487
1550
 
1488
1551
  // src/mutations/execute-insert.ts
1489
- 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();
1490
1607
  init_serializer();
1491
1608
  async function executeInsert(ctx, data) {
1492
- const validated = (0, import_core8.validateRecord)(ctx.collection, ctx.definition, data, "insert");
1493
- 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)();
1494
1611
  for (const [fieldName, descriptor] of Object.entries(ctx.definition.fields)) {
1495
1612
  if (descriptor.auto && descriptor.kind === "timestamp") {
1496
1613
  validated[fieldName] = Date.now();
@@ -1501,13 +1618,16 @@ async function executeInsert(ctx, data) {
1501
1618
  let record;
1502
1619
  await ctx.adapter.transaction(async (tx) => {
1503
1620
  const sequenceNumber = await allocateNextSequenceInTransaction(tx, ctx.nodeId);
1504
- operation = await (0, import_core8.createOperation)(
1621
+ operation = await (0, import_core9.createOperation)(
1505
1622
  {
1506
1623
  nodeId: ctx.nodeId,
1507
1624
  type: "insert",
1508
1625
  collection: ctx.collection,
1509
1626
  recordId,
1510
- 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),
1511
1631
  previousData: null,
1512
1632
  sequenceNumber,
1513
1633
  causalDeps,
@@ -1523,7 +1643,12 @@ async function executeInsert(ctx, data) {
1523
1643
  ...serializedData,
1524
1644
  _created_at: operation.timestamp.wallTime,
1525
1645
  _updated_at: operation.timestamp.wallTime,
1526
- _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
+ )
1527
1652
  };
1528
1653
  const builtInsert = buildInsertQuery(ctx.collection, record);
1529
1654
  const opInsert = buildInsertQuery(
@@ -1543,12 +1668,13 @@ async function executeInsert(ctx, data) {
1543
1668
  }
1544
1669
 
1545
1670
  // src/mutations/execute-update.ts
1546
- var import_core10 = require("@korajs/core");
1671
+ var import_core11 = require("@korajs/core");
1672
+ init_op_data_encoding();
1547
1673
  init_serializer();
1548
1674
 
1549
1675
  // src/state-machine/state-validator.ts
1550
- var import_core9 = require("@korajs/core");
1551
- var InvalidStateTransitionError = class extends import_core9.KoraError {
1676
+ var import_core10 = require("@korajs/core");
1677
+ var InvalidStateTransitionError = class extends import_core10.KoraError {
1552
1678
  constructor(collection, recordId, field, fromState, toState, allowedStates) {
1553
1679
  super(
1554
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)"}`,
@@ -1582,7 +1708,7 @@ function validateStateTransition(collectionName, recordId, stateMachine, current
1582
1708
  collection: collectionName,
1583
1709
  transitions: stateMachine.transitions
1584
1710
  };
1585
- const transitionResult = (0, import_core9.validateTransition)(constraint, currentState, newState);
1711
+ const transitionResult = (0, import_core10.validateTransition)(constraint, currentState, newState);
1586
1712
  if (transitionResult.valid) {
1587
1713
  return { valid: true, allowedStates: transitionResult.allowedTargets };
1588
1714
  }
@@ -1638,7 +1764,7 @@ async function executeUpdate(ctx, id, data) {
1638
1764
  if (!currentRow) {
1639
1765
  throw new RecordNotFoundError(ctx.collection, id);
1640
1766
  }
1641
- 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");
1642
1768
  const currentRecord = deserializeRecord(currentRow, ctx.definition.fields);
1643
1769
  validated = validateUpdateStateMachine(
1644
1770
  ctx.collection,
@@ -1656,9 +1782,9 @@ async function executeUpdate(ctx, id, data) {
1656
1782
  for (const key of Object.keys(validated)) {
1657
1783
  const value = validated[key];
1658
1784
  previousData[key] = currentRecord[key];
1659
- if ((0, import_core10.isAtomicOp)(value)) {
1660
- resolvedData[key] = (0, import_core10.resolveAtomicOp)(currentRecord[key], value);
1661
- 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);
1662
1788
  } else {
1663
1789
  resolvedData[key] = value;
1664
1790
  }
@@ -1668,14 +1794,17 @@ async function executeUpdate(ctx, id, data) {
1668
1794
  let operation;
1669
1795
  await ctx.adapter.transaction(async (tx) => {
1670
1796
  const sequenceNumber = await allocateNextSequenceInTransaction(tx, ctx.nodeId);
1671
- operation = await (0, import_core10.createOperation)(
1797
+ operation = await (0, import_core11.createOperation)(
1672
1798
  {
1673
1799
  nodeId: ctx.nodeId,
1674
1800
  type: "update",
1675
1801
  collection: ctx.collection,
1676
1802
  recordId: id,
1677
- data: { ...resolvedData },
1678
- 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),
1679
1808
  sequenceNumber,
1680
1809
  causalDeps,
1681
1810
  schemaVersion: ctx.schema.version,
@@ -1686,10 +1815,15 @@ async function executeUpdate(ctx, id, data) {
1686
1815
  ctx.causalTracker?.afterOperation(ctx.collection, operation.id, ctx.inTransaction);
1687
1816
  const serializedChanges = serializeRecord(resolvedData, ctx.definition.fields);
1688
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
+ }
1689
1822
  const updateQuery = buildUpdateQuery(ctx.collection, id, {
1690
1823
  ...serializedChanges,
1691
1824
  _updated_at: operation.timestamp.wallTime,
1692
- _version: version
1825
+ _version: version,
1826
+ _field_versions: serializeFieldVersions(mergedFieldVersions)
1693
1827
  });
1694
1828
  const opInsert = buildInsertQuery(
1695
1829
  `_kora_ops_${ctx.collection}`,
@@ -2089,7 +2223,7 @@ var QueryError2 = class extends Error {
2089
2223
  };
2090
2224
 
2091
2225
  // src/relations/relation-enforcer.ts
2092
- var import_core11 = require("@korajs/core");
2226
+ var import_core12 = require("@korajs/core");
2093
2227
  init_serializer();
2094
2228
 
2095
2229
  // src/relations/relation-lookup.ts
@@ -2114,7 +2248,7 @@ function getIncomingRelations(lookup, collection) {
2114
2248
  }
2115
2249
 
2116
2250
  // src/relations/relation-enforcer.ts
2117
- var ReferentialIntegrityError = class extends import_core11.KoraError {
2251
+ var ReferentialIntegrityError = class extends import_core12.KoraError {
2118
2252
  constructor(collection, recordId, referencingCollection, relationName, referencingCount) {
2119
2253
  super(
2120
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.`,
@@ -2203,7 +2337,7 @@ var RelationEnforcer = class {
2203
2337
  for (const row of referencingRows) {
2204
2338
  const now = Date.now();
2205
2339
  const sequenceNumber = await allocateNextSequenceInTransaction(tx, this.nodeId);
2206
- const operation = await (0, import_core11.createOperation)(
2340
+ const operation = await (0, import_core12.createOperation)(
2207
2341
  {
2208
2342
  nodeId: this.nodeId,
2209
2343
  type: "delete",
@@ -2252,7 +2386,7 @@ var RelationEnforcer = class {
2252
2386
  const sequenceNumber = await allocateNextSequenceInTransaction(tx, this.nodeId);
2253
2387
  const updateData = { [foreignKeyField]: null };
2254
2388
  const previousData = { [foreignKeyField]: deletedRecordId };
2255
- const operation = await (0, import_core11.createOperation)(
2389
+ const operation = await (0, import_core12.createOperation)(
2256
2390
  {
2257
2391
  nodeId: this.nodeId,
2258
2392
  type: "update",
@@ -2313,8 +2447,9 @@ var RelationEnforcer = class {
2313
2447
  };
2314
2448
 
2315
2449
  // src/replay/replay-to.ts
2316
- var import_core12 = require("@korajs/core");
2450
+ var import_core13 = require("@korajs/core");
2317
2451
  var import_internal = require("@korajs/core/internal");
2452
+ init_op_data_encoding();
2318
2453
  function collectCausalClosure(allOps, targetOperationId) {
2319
2454
  const opMap = /* @__PURE__ */ new Map();
2320
2455
  for (const op of allOps) {
@@ -2322,7 +2457,7 @@ function collectCausalClosure(allOps, targetOperationId) {
2322
2457
  }
2323
2458
  const target = opMap.get(targetOperationId);
2324
2459
  if (!target) {
2325
- throw new import_core12.OperationError(
2460
+ throw new import_core13.OperationError(
2326
2461
  `Operation "${targetOperationId}" not found in the local operation log`,
2327
2462
  {
2328
2463
  operationId: targetOperationId
@@ -2354,7 +2489,7 @@ function buildReplaySnapshot(schema, allOps, targetOperationId) {
2354
2489
  const operationsApplied = collectCausalClosure(allOps, targetOperationId);
2355
2490
  const targetOperation = operationsApplied.find((op) => op.id === targetOperationId);
2356
2491
  if (!targetOperation) {
2357
- 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`, {
2358
2493
  operationId: targetOperationId
2359
2494
  });
2360
2495
  }
@@ -2399,7 +2534,9 @@ function applyOperationToMemory(state, op, schema) {
2399
2534
  }
2400
2535
  colMap.set(op.recordId, {
2401
2536
  id: op.recordId,
2402
- 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),
2403
2540
  deleted: false,
2404
2541
  createdAt: wallTime,
2405
2542
  updatedAt: wallTime
@@ -2410,9 +2547,10 @@ function applyOperationToMemory(state, op, schema) {
2410
2547
  if (!op.data) {
2411
2548
  return;
2412
2549
  }
2550
+ const decodedData = decodeRichtextFieldsFromOpData(op.data, definition.fields);
2413
2551
  const existing = colMap.get(op.recordId);
2414
2552
  if (existing && !existing.deleted) {
2415
- existing.fields = { ...existing.fields, ...op.data };
2553
+ existing.fields = { ...existing.fields, ...decodedData };
2416
2554
  existing.updatedAt = wallTime;
2417
2555
  break;
2418
2556
  }
@@ -2421,7 +2559,7 @@ function applyOperationToMemory(state, op, schema) {
2421
2559
  }
2422
2560
  colMap.set(op.recordId, {
2423
2561
  id: op.recordId,
2424
- fields: { ...op.data },
2562
+ fields: decodedData,
2425
2563
  deleted: false,
2426
2564
  createdAt: wallTime,
2427
2565
  updatedAt: wallTime
@@ -2477,7 +2615,7 @@ function toCollectionRecord(record, definition) {
2477
2615
  }
2478
2616
 
2479
2617
  // src/sequences/sequence-manager.ts
2480
- var import_core13 = require("@korajs/core");
2618
+ var import_core14 = require("@korajs/core");
2481
2619
  var SequenceManager = class {
2482
2620
  adapter;
2483
2621
  nodeId;
@@ -2495,7 +2633,7 @@ var SequenceManager = class {
2495
2633
  async next(name, config) {
2496
2634
  const scope = config?.scope ?? "";
2497
2635
  const startAt = config?.startAt ?? 1;
2498
- const format = config?.format ?? (0, import_core13.defaultSequenceFormat)(name);
2636
+ const format = config?.format ?? (0, import_core14.defaultSequenceFormat)(name);
2499
2637
  let counter = 0;
2500
2638
  await this.adapter.transaction(async (tx) => {
2501
2639
  const rows = await tx.query(
@@ -2517,7 +2655,7 @@ var SequenceManager = class {
2517
2655
  );
2518
2656
  }
2519
2657
  });
2520
- return (0, import_core13.formatSequenceValue)(format, counter, this.nodeId);
2658
+ return (0, import_core14.formatSequenceValue)(format, counter, this.nodeId);
2521
2659
  }
2522
2660
  /**
2523
2661
  * Get the current counter value without incrementing.
@@ -2975,8 +3113,141 @@ var SubscriptionManager = class {
2975
3113
  }
2976
3114
  };
2977
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
+
2978
3248
  // src/transaction/transaction-context.ts
2979
- var import_core14 = require("@korajs/core");
3249
+ var import_core16 = require("@korajs/core");
3250
+ init_op_data_encoding();
2980
3251
  init_serializer();
2981
3252
  var TransactionContext = class {
2982
3253
  transactionId;
@@ -2987,7 +3258,7 @@ var TransactionContext = class {
2987
3258
  config;
2988
3259
  constructor(config) {
2989
3260
  this.config = config;
2990
- this.transactionId = (0, import_core14.generateUUIDv7)();
3261
+ this.transactionId = (0, import_core16.generateUUIDv7)();
2991
3262
  }
2992
3263
  /**
2993
3264
  * Set a human-readable mutation name for this transaction.
@@ -3102,8 +3373,8 @@ var TransactionContext = class {
3102
3373
  }
3103
3374
  async insert(collectionName, definition, data) {
3104
3375
  this.ensureActive();
3105
- const validated = (0, import_core14.validateRecord)(collectionName, definition, data, "insert");
3106
- const recordId = (0, import_core14.generateUUIDv7)();
3376
+ const validated = (0, import_core16.validateRecord)(collectionName, definition, data, "insert");
3377
+ const recordId = (0, import_core16.generateUUIDv7)();
3107
3378
  const now = Date.now();
3108
3379
  for (const [fieldName, descriptor] of Object.entries(definition.fields)) {
3109
3380
  if (descriptor.auto && descriptor.kind === "timestamp") {
@@ -3112,13 +3383,15 @@ var TransactionContext = class {
3112
3383
  }
3113
3384
  const sequenceNumber = await this.config.sequenceAllocator.allocate();
3114
3385
  const causalDeps = this.config.causalTracker?.nextCausalDeps(collectionName, true) ?? [];
3115
- const operation = await (0, import_core14.createOperation)(
3386
+ const operation = await (0, import_core16.createOperation)(
3116
3387
  {
3117
3388
  nodeId: this.config.nodeId,
3118
3389
  type: "insert",
3119
3390
  collection: collectionName,
3120
3391
  recordId,
3121
- 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),
3122
3395
  previousData: null,
3123
3396
  sequenceNumber,
3124
3397
  causalDeps,
@@ -3165,7 +3438,7 @@ var TransactionContext = class {
3165
3438
  if (!currentRecord) {
3166
3439
  throw new RecordNotFoundError(collectionName, id);
3167
3440
  }
3168
- const validated = (0, import_core14.validateRecord)(collectionName, definition, data, "update");
3441
+ const validated = (0, import_core16.validateRecord)(collectionName, definition, data, "update");
3169
3442
  const now = Date.now();
3170
3443
  const previousData = {};
3171
3444
  const resolvedData = {};
@@ -3173,9 +3446,9 @@ var TransactionContext = class {
3173
3446
  for (const key of Object.keys(validated)) {
3174
3447
  const value = validated[key];
3175
3448
  previousData[key] = currentRecord[key];
3176
- if ((0, import_core14.isAtomicOp)(value)) {
3177
- resolvedData[key] = (0, import_core14.resolveAtomicOp)(currentRecord[key], value);
3178
- 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);
3179
3452
  } else {
3180
3453
  resolvedData[key] = value;
3181
3454
  }
@@ -3183,14 +3456,16 @@ var TransactionContext = class {
3183
3456
  const hasAtomicOps = Object.keys(atomicOps).length > 0;
3184
3457
  const sequenceNumber = await this.config.sequenceAllocator.allocate();
3185
3458
  const causalDeps = this.config.causalTracker?.nextCausalDeps(collectionName, true) ?? [];
3186
- const operation = await (0, import_core14.createOperation)(
3459
+ const operation = await (0, import_core16.createOperation)(
3187
3460
  {
3188
3461
  nodeId: this.config.nodeId,
3189
3462
  type: "update",
3190
3463
  collection: collectionName,
3191
3464
  recordId: id,
3192
- data: { ...resolvedData },
3193
- 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),
3194
3469
  sequenceNumber,
3195
3470
  causalDeps,
3196
3471
  schemaVersion: this.config.schema.version,
@@ -3236,7 +3511,7 @@ var TransactionContext = class {
3236
3511
  const now = Date.now();
3237
3512
  const sequenceNumber = await this.config.sequenceAllocator.allocate();
3238
3513
  const causalDeps = this.config.causalTracker?.nextCausalDeps(collectionName, true) ?? [];
3239
- const operation = await (0, import_core14.createOperation)(
3514
+ const operation = await (0, import_core16.createOperation)(
3240
3515
  {
3241
3516
  nodeId: this.config.nodeId,
3242
3517
  type: "delete",
@@ -3297,14 +3572,16 @@ var TransactionContext = class {
3297
3572
  if (entry.operation.type === "insert" && entry.operation.data) {
3298
3573
  record = {
3299
3574
  id,
3300
- ...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),
3301
3578
  createdAt: entry.operation.timestamp.wallTime,
3302
3579
  updatedAt: entry.operation.timestamp.wallTime
3303
3580
  };
3304
3581
  } else if (entry.operation.type === "update" && entry.operation.data && record) {
3305
3582
  record = {
3306
3583
  ...record,
3307
- ...entry.operation.data,
3584
+ ...decodeRichtextFieldsFromOpData(entry.operation.data, definition.fields),
3308
3585
  updatedAt: entry.operation.timestamp.wallTime
3309
3586
  };
3310
3587
  } else if (entry.operation.type === "delete") {
@@ -3339,7 +3616,7 @@ var TransactionSequenceAllocator = class {
3339
3616
  };
3340
3617
 
3341
3618
  // src/store/tab-node-id.ts
3342
- var import_core15 = require("@korajs/core");
3619
+ var import_core17 = require("@korajs/core");
3343
3620
  function perTabStorageKey(dbName) {
3344
3621
  return `kora-tab-node-${dbName}`;
3345
3622
  }
@@ -3360,7 +3637,7 @@ function resolvePerTabNodeId(dbName) {
3360
3637
  if (existing) {
3361
3638
  return existing;
3362
3639
  }
3363
- const nodeId = (0, import_core15.generateUUIDv7)();
3640
+ const nodeId = (0, import_core17.generateUUIDv7)();
3364
3641
  savePerTabNodeId(dbName, nodeId);
3365
3642
  return nodeId;
3366
3643
  }
@@ -3370,7 +3647,7 @@ var Store = class {
3370
3647
  opened = false;
3371
3648
  nodeId = "";
3372
3649
  sequenceNumber = 0;
3373
- versionVector = (0, import_core17.createVersionVector)();
3650
+ versionVector = (0, import_core19.createVersionVector)();
3374
3651
  clock = null;
3375
3652
  collections = /* @__PURE__ */ new Map();
3376
3653
  subscriptionManager;
@@ -3404,8 +3681,8 @@ var Store = class {
3404
3681
  await this.adapter.open(this.schema);
3405
3682
  await this.runMigrationsIfNeeded();
3406
3683
  this.nodeId = await this.loadOrGenerateNodeId();
3407
- this.clock = new import_core17.HybridLogicalClock(this.nodeId);
3408
- this.causalTracker = new import_core17.CausalTracker();
3684
+ this.clock = new import_core19.HybridLogicalClock(this.nodeId);
3685
+ this.causalTracker = new import_core19.CausalTracker();
3409
3686
  this.sequenceManager = new SequenceManager(this.adapter, this.nodeId);
3410
3687
  this.sequenceNumber = await this.loadSequenceNumber();
3411
3688
  this.versionVector = await this.loadVersionVector();
@@ -3491,6 +3768,14 @@ var Store = class {
3491
3768
  /**
3492
3769
  * Get the node ID for this store instance.
3493
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
+ }
3494
3779
  getNodeId() {
3495
3780
  this.ensureOpen();
3496
3781
  return this.nodeId;
@@ -3517,45 +3802,158 @@ var Store = class {
3517
3802
  if (this.clock) {
3518
3803
  this.clock.receive(op.timestamp);
3519
3804
  }
3520
- const remoteVersion = serializeRowVersion(op.timestamp);
3521
- 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;
3522
3809
  await this.adapter.transaction(async (tx) => {
3523
- 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);
3524
3823
  const existing2 = await tx.query(
3525
- `SELECT _updated_at, _version FROM ${collection} WHERE id = ?`,
3824
+ `SELECT _updated_at, _version, _field_versions, _deleted FROM ${collection} WHERE id = ?`,
3526
3825
  [op.recordId]
3527
3826
  );
3528
3827
  const row = existing2[0];
3529
- if (row && !isIncomingNewerThanRow(op.timestamp, row)) {
3530
- } else {
3531
- const serializedData = serializeRecord(op.data, definition.fields);
3828
+ checkGuard(row);
3829
+ if (!row) {
3532
3830
  const record = {
3533
3831
  id: op.recordId,
3534
3832
  ...serializedData,
3535
3833
  _created_at: wallTime,
3536
3834
  _updated_at: wallTime,
3537
- _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
+ )
3538
3840
  };
3539
3841
  const insertQuery = buildInsertQuery(collection, record);
3540
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);
3541
3896
  }
3542
- } else if (op.type === "update" && op.data) {
3543
- const serializedChanges = serializeRecord(op.data, definition.fields);
3544
- const updatePayload = {
3545
- ...serializedChanges,
3546
- _updated_at: wallTime,
3547
- _version: remoteVersion
3548
- };
3549
- if (options?.reactivateIfDeleted) {
3550
- updatePayload._deleted = 0;
3551
- }
3552
- const updateQuery = buildLwwUpdateQuery(
3553
- collection,
3554
- op.recordId,
3555
- updatePayload,
3556
- 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]
3557
3903
  );
3558
- 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
+ }
3559
3957
  } else if (op.type === "delete") {
3560
3958
  const deleteQuery = buildLwwSoftDeleteQuery(
3561
3959
  collection,
@@ -3583,6 +3981,67 @@ var Store = class {
3583
3981
  this.subscriptionManager.notify(collection, op);
3584
3982
  return "applied";
3585
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
+ }
3586
4045
  /**
3587
4046
  * Get operations from a node within a sequence number range.
3588
4047
  * Implements the OperationLog interface for computeDelta.
@@ -3738,6 +4197,31 @@ var Store = class {
3738
4197
  deleted: row._deleted === 1
3739
4198
  };
3740
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
+ }
3741
4225
  /**
3742
4226
  * Latest operation from this device for a record (used for delete-vs-update merge on sync).
3743
4227
  */
@@ -3780,6 +4264,30 @@ var Store = class {
3780
4264
  (nodeId, fromSeq, toSeq) => this.getOperationRange(nodeId, fromSeq, toSeq)
3781
4265
  );
3782
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
+ }
3783
4291
  /**
3784
4292
  * Count of local operations ahead of the server version vector.
3785
4293
  */
@@ -3794,7 +4302,7 @@ var Store = class {
3794
4302
  async compact(strategy) {
3795
4303
  this.ensureOpen();
3796
4304
  if (strategy.mode === "never") {
3797
- return compactOperationLog(this.adapter, this.schema, strategy, (0, import_core17.createVersionVector)());
4305
+ return compactOperationLog(this.adapter, this.schema, strategy, (0, import_core19.createVersionVector)());
3798
4306
  }
3799
4307
  const serverVector = strategy.serverVector ?? await loadLastAckedServerVector(this.adapter);
3800
4308
  return compactOperationLog(this.adapter, this.schema, strategy, serverVector);
@@ -3829,7 +4337,7 @@ var Store = class {
3829
4337
  let latest = null;
3830
4338
  for (const row of rows) {
3831
4339
  const op = deserializeOperationWithCollection(row, collection);
3832
- if (!latest || import_core17.HybridLogicalClock.compare(op.timestamp, latest.timestamp) > 0) {
4340
+ if (!latest || import_core19.HybridLogicalClock.compare(op.timestamp, latest.timestamp) > 0) {
3833
4341
  latest = op;
3834
4342
  }
3835
4343
  }
@@ -3972,7 +4480,7 @@ var Store = class {
3972
4480
  for (let v = storedVersion + 1; v <= targetVersion; v++) {
3973
4481
  const migration = migrations[v];
3974
4482
  if (!migration) continue;
3975
- const sqlStatements = (0, import_core17.migrationStepsToSQL)(migration.steps);
4483
+ const sqlStatements = (0, import_core19.migrationStepsToSQL)(migration.steps);
3976
4484
  for (const sql of sqlStatements) {
3977
4485
  try {
3978
4486
  await this.adapter.execute(sql);
@@ -4050,7 +4558,7 @@ var Store = class {
4050
4558
  if (rows[0]) {
4051
4559
  return rows[0].value;
4052
4560
  }
4053
- const newNodeId = (0, import_core17.generateUUIDv7)();
4561
+ const newNodeId = (0, import_core19.generateUUIDv7)();
4054
4562
  await this.adapter.execute("INSERT INTO _kora_meta (key, value) VALUES ('node_id', ?)", [
4055
4563
  newNodeId
4056
4564
  ]);
@@ -4067,7 +4575,7 @@ var Store = class {
4067
4575
  const rows = await this.adapter.query(
4068
4576
  "SELECT node_id, sequence_number FROM _kora_version_vector"
4069
4577
  );
4070
- const vector = (0, import_core17.createVersionVector)();
4578
+ const vector = (0, import_core19.createVersionVector)();
4071
4579
  for (const row of rows) {
4072
4580
  vector.set(row.node_id, row.sequence_number);
4073
4581
  }
@@ -4141,12 +4649,12 @@ var QueryStore = class {
4141
4649
  };
4142
4650
 
4143
4651
  // src/reactivity/assert-query-ready.ts
4144
- var import_core18 = require("@korajs/core");
4652
+ var import_core20 = require("@korajs/core");
4145
4653
  var LEGACY_PENDING_COLLECTION = "__pending__";
4146
4654
  function assertQueryReady(query) {
4147
4655
  const descriptor = query.getDescriptor();
4148
4656
  if (descriptor.collection === LEGACY_PENDING_COLLECTION) {
4149
- throw new import_core18.AppNotReadyError(
4657
+ throw new import_core20.AppNotReadyError(
4150
4658
  "Cannot use useQuery() before app.ready. Await app.ready or wrap your UI in <KoraProvider app={app}>."
4151
4659
  );
4152
4660
  }
@@ -4574,6 +5082,7 @@ init_export_audit();
4574
5082
  Collection,
4575
5083
  InvalidStateTransitionError,
4576
5084
  LAST_ACKED_SERVER_VECTOR_META_KEY,
5085
+ OptimisticLockError,
4577
5086
  PersistenceError,
4578
5087
  QueryBuilder,
4579
5088
  QueryError,