@korajs/store 0.6.0 → 1.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/LICENSE +21 -0
  2. package/dist/adapters/better-sqlite3.cjs +94 -40
  3. package/dist/adapters/better-sqlite3.cjs.map +1 -1
  4. package/dist/adapters/better-sqlite3.d.cts +9 -1
  5. package/dist/adapters/better-sqlite3.d.ts +9 -1
  6. package/dist/adapters/better-sqlite3.js +62 -41
  7. package/dist/adapters/better-sqlite3.js.map +1 -1
  8. package/dist/adapters/indexeddb.cjs +24 -13
  9. package/dist/adapters/indexeddb.cjs.map +1 -1
  10. package/dist/adapters/indexeddb.d.cts +2 -2
  11. package/dist/adapters/indexeddb.d.ts +2 -2
  12. package/dist/adapters/indexeddb.js +4 -3
  13. package/dist/adapters/indexeddb.js.map +1 -1
  14. package/dist/adapters/sqlite-wasm.cjs +24 -13
  15. package/dist/adapters/sqlite-wasm.cjs.map +1 -1
  16. package/dist/adapters/sqlite-wasm.d.cts +27 -4
  17. package/dist/adapters/sqlite-wasm.d.ts +27 -4
  18. package/dist/adapters/sqlite-wasm.js +6 -4
  19. package/dist/{backup-JN3LB4YT.js → backup-ZV4Y424B.js} +3 -3
  20. package/dist/blob-fs.cjs +166 -0
  21. package/dist/blob-fs.cjs.map +1 -0
  22. package/dist/blob-fs.d.cts +29 -0
  23. package/dist/blob-fs.d.ts +29 -0
  24. package/dist/blob-fs.js +128 -0
  25. package/dist/blob-fs.js.map +1 -0
  26. package/dist/{chunk-56LL4XP6.js → chunk-6NKLDBTY.js} +52 -1
  27. package/dist/chunk-6NKLDBTY.js.map +1 -0
  28. package/dist/{chunk-AGN3CIPB.js → chunk-FFYA7J5H.js} +2 -2
  29. package/dist/{chunk-XO4LMPAH.js → chunk-IOEFNHHP.js} +2 -35
  30. package/dist/chunk-IOEFNHHP.js.map +1 -0
  31. package/dist/{chunk-LAWV6CFH.js → chunk-QL2XVZRN.js} +12 -1
  32. package/dist/chunk-QL2XVZRN.js.map +1 -0
  33. package/dist/{chunk-6AHYGQQT.js → chunk-RBPVEAVM.js} +4 -4
  34. package/dist/chunk-TVJEC5S2.js +40 -0
  35. package/dist/chunk-TVJEC5S2.js.map +1 -0
  36. package/dist/chunk-W4MTTNEZ.js +55 -0
  37. package/dist/chunk-W4MTTNEZ.js.map +1 -0
  38. package/dist/{chunk-EZSTD2XC.js → chunk-X57MWW35.js} +2 -2
  39. package/dist/{chunk-7BPETY5Y.js → chunk-ZLWAYQEQ.js} +128 -15
  40. package/dist/chunk-ZLWAYQEQ.js.map +1 -0
  41. package/dist/content-addressed-blob-store-Dcj3SB9r.d.cts +60 -0
  42. package/dist/content-addressed-blob-store-Dcj3SB9r.d.ts +60 -0
  43. package/dist/{export-audit-5GV6VRDZ.js → export-audit-AJ7RORNK.js} +3 -3
  44. package/dist/index.cjs +1245 -113
  45. package/dist/index.cjs.map +1 -1
  46. package/dist/index.d.cts +468 -7
  47. package/dist/index.d.ts +468 -7
  48. package/dist/index.js +975 -61
  49. package/dist/index.js.map +1 -1
  50. package/dist/internal.cjs +123 -33
  51. package/dist/internal.cjs.map +1 -1
  52. package/dist/internal.d.cts +3 -3
  53. package/dist/internal.d.ts +3 -3
  54. package/dist/internal.js +3 -3
  55. package/dist/{sqlite-wasm-channel-I9y_jObg.d.cts → sqlite-wasm-channel-DUDPclTE.d.cts} +2 -15
  56. package/dist/{sqlite-wasm-channel-I9y_jObg.d.ts → sqlite-wasm-channel-DUDPclTE.d.ts} +2 -15
  57. package/dist/sqlite-wasm-channel-H43KEZLM.js +12 -0
  58. package/dist/{types-CTr00g_d.d.cts → types-5h4Hqcwc.d.ts} +10 -3
  59. package/dist/{types-BMyHmwvn.d.ts → types-DfPiPwKr.d.cts} +10 -3
  60. package/dist/{types-DepnRDk_.d.cts → types-k6AZgiH4.d.cts} +48 -2
  61. package/dist/{types-DepnRDk_.d.ts → types-k6AZgiH4.d.ts} +48 -2
  62. package/package.json +15 -2
  63. package/dist/chunk-56LL4XP6.js.map +0 -1
  64. package/dist/chunk-7BPETY5Y.js.map +0 -1
  65. package/dist/chunk-LAWV6CFH.js.map +0 -1
  66. package/dist/chunk-XO4LMPAH.js.map +0 -1
  67. package/dist/sqlite-wasm-channel-HDDW6UBH.js +0 -10
  68. /package/dist/{backup-JN3LB4YT.js.map → backup-ZV4Y424B.js.map} +0 -0
  69. /package/dist/{chunk-AGN3CIPB.js.map → chunk-FFYA7J5H.js.map} +0 -0
  70. /package/dist/{chunk-6AHYGQQT.js.map → chunk-RBPVEAVM.js.map} +0 -0
  71. /package/dist/{chunk-EZSTD2XC.js.map → chunk-X57MWW35.js.map} +0 -0
  72. /package/dist/{export-audit-5GV6VRDZ.js.map → export-audit-AJ7RORNK.js.map} +0 -0
  73. /package/dist/{sqlite-wasm-channel-HDDW6UBH.js.map → sqlite-wasm-channel-H43KEZLM.js.map} +0 -0
package/dist/internal.cjs CHANGED
@@ -54,10 +54,28 @@ __export(internal_exports, {
54
54
  module.exports = __toCommonJS(internal_exports);
55
55
 
56
56
  // src/serialization/serializer.ts
57
- var import_core = require("@korajs/core");
57
+ var import_core2 = require("@korajs/core");
58
58
 
59
59
  // src/serialization/richtext-serializer.ts
60
60
  var Y = __toESM(require("yjs"), 1);
61
+
62
+ // src/serialization/op-data-encoding.ts
63
+ var import_core = require("@korajs/core");
64
+ var decodeRichtextOpDataValue = import_core.decodeBytesFromOpData;
65
+ function encodeRichtextFieldsForOpData(data, fields) {
66
+ const result = { ...data };
67
+ for (const [key, value] of Object.entries(data)) {
68
+ if (fields[key]?.kind !== "richtext") {
69
+ continue;
70
+ }
71
+ if (typeof value === "string" || value instanceof Uint8Array || value instanceof ArrayBuffer) {
72
+ result[key] = (0, import_core.encodeBytesForOpData)(value);
73
+ }
74
+ }
75
+ return result;
76
+ }
77
+
78
+ // src/serialization/richtext-serializer.ts
61
79
  var TEXT_KEY = "content";
62
80
  function encodeRichtext(value) {
63
81
  if (value === null || value === void 0) {
@@ -74,6 +92,10 @@ function encodeRichtext(value) {
74
92
  if (value instanceof ArrayBuffer) {
75
93
  return new Uint8Array(value);
76
94
  }
95
+ if ((0, import_core.isKoraBytesValue)(value) || (0, import_core.isLegacyNumericByteObject)(value)) {
96
+ const decoded = decodeRichtextOpDataValue(value);
97
+ return decoded instanceof Uint8Array ? decoded : null;
98
+ }
77
99
  throw new Error("Richtext value must be a string, Uint8Array, ArrayBuffer, null, or undefined.");
78
100
  }
79
101
  function decodeRichtext(value) {
@@ -156,7 +178,7 @@ function serializeOperation(op) {
156
178
  record_id: op.recordId,
157
179
  data: dataPayload ? JSON.stringify(dataPayload) : null,
158
180
  previous_data: op.previousData ? JSON.stringify(op.previousData) : null,
159
- timestamp: import_core.HybridLogicalClock.serialize(op.timestamp),
181
+ timestamp: import_core2.HybridLogicalClock.serialize(op.timestamp),
160
182
  sequence_number: op.sequenceNumber,
161
183
  causal_deps: JSON.stringify(op.causalDeps),
162
184
  schema_version: op.schemaVersion
@@ -190,7 +212,7 @@ function deserializeOperation(row) {
190
212
  recordId: row.record_id,
191
213
  data,
192
214
  previousData: row.previous_data ? JSON.parse(row.previous_data) : null,
193
- timestamp: import_core.HybridLogicalClock.deserialize(row.timestamp),
215
+ timestamp: import_core2.HybridLogicalClock.deserialize(row.timestamp),
194
216
  sequenceNumber: row.sequence_number,
195
217
  causalDeps: JSON.parse(row.causal_deps),
196
218
  schemaVersion: row.schema_version,
@@ -211,6 +233,9 @@ function serializeValue(value, descriptor) {
211
233
  case "boolean":
212
234
  return value ? 1 : 0;
213
235
  case "array":
236
+ case "object":
237
+ case "json":
238
+ case "blob":
214
239
  return JSON.stringify(value);
215
240
  case "richtext":
216
241
  return encodeRichtext(value);
@@ -227,6 +252,13 @@ function deserializeValue(value, descriptor) {
227
252
  return JSON.parse(value);
228
253
  }
229
254
  return value;
255
+ case "object":
256
+ case "json":
257
+ case "blob":
258
+ if (typeof value === "string") {
259
+ return JSON.parse(value);
260
+ }
261
+ return value;
230
262
  case "richtext":
231
263
  return decodeRichtext(value);
232
264
  default:
@@ -235,14 +267,14 @@ function deserializeValue(value, descriptor) {
235
267
  }
236
268
 
237
269
  // src/errors.ts
238
- var import_core2 = require("@korajs/core");
239
- var QueryError = class extends import_core2.KoraError {
270
+ var import_core3 = require("@korajs/core");
271
+ var QueryError = class extends import_core3.KoraError {
240
272
  constructor(message, context) {
241
273
  super(message, "QUERY_ERROR", context);
242
274
  this.name = "QueryError";
243
275
  }
244
276
  };
245
- var RecordNotFoundError = class extends import_core2.KoraError {
277
+ var RecordNotFoundError = class extends import_core3.KoraError {
246
278
  constructor(collection, recordId) {
247
279
  super(`Record "${recordId}" not found in collection "${collection}"`, "RECORD_NOT_FOUND", {
248
280
  collection,
@@ -253,20 +285,20 @@ var RecordNotFoundError = class extends import_core2.KoraError {
253
285
  };
254
286
 
255
287
  // src/lww/row-version.ts
256
- var import_core3 = require("@korajs/core");
288
+ var import_core4 = require("@korajs/core");
257
289
  var EMPTY_ROW_VERSION = "";
258
290
  function serializeRowVersion(timestamp) {
259
- return import_core3.HybridLogicalClock.serialize(timestamp);
291
+ return import_core4.HybridLogicalClock.serialize(timestamp);
260
292
  }
261
293
  function rowVersionFromRecord(row) {
262
294
  const version = row._version;
263
295
  if (typeof version === "string" && version.length > 0) {
264
- return import_core3.HybridLogicalClock.deserialize(version);
296
+ return import_core4.HybridLogicalClock.deserialize(version);
265
297
  }
266
298
  return { wallTime: row._updated_at, logical: 0, nodeId: "" };
267
299
  }
268
300
  function isIncomingNewerThanRow(incoming, row) {
269
- return import_core3.HybridLogicalClock.compare(incoming, rowVersionFromRecord(row)) > 0;
301
+ return import_core4.HybridLogicalClock.compare(incoming, rowVersionFromRecord(row)) > 0;
270
302
  }
271
303
  function lwwVersionWhereClause(remoteVersion) {
272
304
  return {
@@ -447,7 +479,39 @@ function validateFieldName(fieldName, fields) {
447
479
  }
448
480
 
449
481
  // src/mutations/execute-insert.ts
450
- var import_core4 = require("@korajs/core");
482
+ var import_core5 = require("@korajs/core");
483
+
484
+ // src/lww/field-versions.ts
485
+ function parseFieldVersions(raw) {
486
+ if (typeof raw !== "string" || raw.length === 0) {
487
+ return {};
488
+ }
489
+ try {
490
+ const parsed = JSON.parse(raw);
491
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
492
+ return {};
493
+ }
494
+ const result = {};
495
+ for (const [key, value] of Object.entries(parsed)) {
496
+ if (typeof value === "string") {
497
+ result[key] = value;
498
+ }
499
+ }
500
+ return result;
501
+ } catch {
502
+ return {};
503
+ }
504
+ }
505
+ function serializeFieldVersions(versions) {
506
+ return JSON.stringify(versions);
507
+ }
508
+ function fieldVersionsForFields(fieldNames, version) {
509
+ const versions = {};
510
+ for (const field of fieldNames) {
511
+ versions[field] = version;
512
+ }
513
+ return versions;
514
+ }
451
515
 
452
516
  // src/store/sequence-allocator.ts
453
517
  async function allocateNextSequenceInTransaction(tx, nodeId) {
@@ -482,25 +546,33 @@ function resolveCausalDeps(ctx) {
482
546
 
483
547
  // src/mutations/execute-insert.ts
484
548
  async function executeInsert(ctx, data) {
485
- const validated = (0, import_core4.validateRecord)(ctx.collection, ctx.definition, data, "insert");
486
- const recordId = (0, import_core4.generateUUIDv7)();
549
+ const validated = (0, import_core5.validateRecord)(ctx.collection, ctx.definition, data, "insert");
550
+ const recordId = (0, import_core5.generateUUIDv7)();
487
551
  for (const [fieldName, descriptor] of Object.entries(ctx.definition.fields)) {
488
552
  if (descriptor.auto && descriptor.kind === "timestamp") {
489
553
  validated[fieldName] = Date.now();
490
554
  }
491
555
  }
556
+ const writeData = await (0, import_core5.transformSecretFieldsForWrite)(
557
+ validated,
558
+ ctx.definition,
559
+ ctx.secretKeyProvider
560
+ );
492
561
  const causalDeps = resolveCausalDeps(ctx);
493
562
  let operation;
494
563
  let record;
495
564
  await ctx.adapter.transaction(async (tx) => {
496
565
  const sequenceNumber = await allocateNextSequenceInTransaction(tx, ctx.nodeId);
497
- operation = await (0, import_core4.createOperation)(
566
+ operation = await (0, import_core5.createOperation)(
498
567
  {
499
568
  nodeId: ctx.nodeId,
500
569
  type: "insert",
501
570
  collection: ctx.collection,
502
571
  recordId,
503
- data: { ...validated },
572
+ // Binary richtext values are tagged as canonical JSON BEFORE the
573
+ // operation is content-hashed, so the hash input, persisted JSON,
574
+ // and wire payload are the identical value.
575
+ data: encodeRichtextFieldsForOpData(writeData, ctx.definition.fields),
504
576
  previousData: null,
505
577
  sequenceNumber,
506
578
  causalDeps,
@@ -509,14 +581,19 @@ async function executeInsert(ctx, data) {
509
581
  ctx.clock
510
582
  );
511
583
  ctx.causalTracker?.afterOperation(ctx.collection, operation.id, ctx.inTransaction);
512
- const serializedData = serializeRecord(validated, ctx.definition.fields);
584
+ const serializedData = serializeRecord(writeData, ctx.definition.fields);
513
585
  const version = serializeRowVersion(operation.timestamp);
514
586
  record = {
515
587
  id: recordId,
516
588
  ...serializedData,
517
589
  _created_at: operation.timestamp.wallTime,
518
590
  _updated_at: operation.timestamp.wallTime,
519
- _version: version
591
+ _version: version,
592
+ // Stamp every inserted field with this operation's version so later
593
+ // per-field LWW compares against a real writer, not the fallback.
594
+ _field_versions: serializeFieldVersions(
595
+ fieldVersionsForFields(Object.keys(serializedData), version)
596
+ )
520
597
  };
521
598
  const builtInsert = buildInsertQuery(ctx.collection, record);
522
599
  const opInsert = buildInsertQuery(
@@ -529,18 +606,18 @@ async function executeInsert(ctx, data) {
529
606
  ctx.onMutation(ctx.collection, operation);
530
607
  return {
531
608
  id: recordId,
532
- ...validated,
609
+ ...writeData,
533
610
  createdAt: operation.timestamp.wallTime,
534
611
  updatedAt: operation.timestamp.wallTime
535
612
  };
536
613
  }
537
614
 
538
615
  // src/mutations/execute-update.ts
539
- var import_core6 = require("@korajs/core");
616
+ var import_core7 = require("@korajs/core");
540
617
 
541
618
  // src/state-machine/state-validator.ts
542
- var import_core5 = require("@korajs/core");
543
- var InvalidStateTransitionError = class extends import_core5.KoraError {
619
+ var import_core6 = require("@korajs/core");
620
+ var InvalidStateTransitionError = class extends import_core6.KoraError {
544
621
  constructor(collection, recordId, field, fromState, toState, allowedStates) {
545
622
  super(
546
623
  `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)"}`,
@@ -574,7 +651,7 @@ function validateStateTransition(collectionName, recordId, stateMachine, current
574
651
  collection: collectionName,
575
652
  transitions: stateMachine.transitions
576
653
  };
577
- const transitionResult = (0, import_core5.validateTransition)(constraint, currentState, newState);
654
+ const transitionResult = (0, import_core6.validateTransition)(constraint, currentState, newState);
578
655
  if (transitionResult.valid) {
579
656
  return { valid: true, allowedStates: transitionResult.allowedTargets };
580
657
  }
@@ -630,7 +707,7 @@ async function executeUpdate(ctx, id, data) {
630
707
  if (!currentRow) {
631
708
  throw new RecordNotFoundError(ctx.collection, id);
632
709
  }
633
- let validated = (0, import_core6.validateRecord)(ctx.collection, ctx.definition, data, "update");
710
+ let validated = (0, import_core7.validateRecord)(ctx.collection, ctx.definition, data, "update");
634
711
  const currentRecord = deserializeRecord(currentRow, ctx.definition.fields);
635
712
  validated = validateUpdateStateMachine(
636
713
  ctx.collection,
@@ -648,26 +725,34 @@ async function executeUpdate(ctx, id, data) {
648
725
  for (const key of Object.keys(validated)) {
649
726
  const value = validated[key];
650
727
  previousData[key] = currentRecord[key];
651
- if ((0, import_core6.isAtomicOp)(value)) {
652
- resolvedData[key] = (0, import_core6.resolveAtomicOp)(currentRecord[key], value);
653
- atomicOps[key] = (0, import_core6.toAtomicOp)(value);
728
+ if ((0, import_core7.isAtomicOp)(value)) {
729
+ resolvedData[key] = (0, import_core7.resolveAtomicOp)(currentRecord[key], value);
730
+ atomicOps[key] = (0, import_core7.toAtomicOp)(value);
654
731
  } else {
655
732
  resolvedData[key] = value;
656
733
  }
657
734
  }
735
+ const writeData = await (0, import_core7.transformSecretFieldsForWrite)(
736
+ resolvedData,
737
+ ctx.definition,
738
+ ctx.secretKeyProvider
739
+ );
658
740
  const hasAtomicOps = Object.keys(atomicOps).length > 0;
659
741
  const causalDeps = resolveCausalDeps(ctx);
660
742
  let operation;
661
743
  await ctx.adapter.transaction(async (tx) => {
662
744
  const sequenceNumber = await allocateNextSequenceInTransaction(tx, ctx.nodeId);
663
- operation = await (0, import_core6.createOperation)(
745
+ operation = await (0, import_core7.createOperation)(
664
746
  {
665
747
  nodeId: ctx.nodeId,
666
748
  type: "update",
667
749
  collection: ctx.collection,
668
750
  recordId: id,
669
- data: { ...resolvedData },
670
- previousData,
751
+ // Binary richtext values (new and previous) are tagged as canonical
752
+ // JSON BEFORE the operation is content-hashed, so the hash input,
753
+ // persisted JSON, and wire payload are the identical value.
754
+ data: encodeRichtextFieldsForOpData(writeData, ctx.definition.fields),
755
+ previousData: encodeRichtextFieldsForOpData(previousData, ctx.definition.fields),
671
756
  sequenceNumber,
672
757
  causalDeps,
673
758
  schemaVersion: ctx.schema.version,
@@ -676,12 +761,17 @@ async function executeUpdate(ctx, id, data) {
676
761
  ctx.clock
677
762
  );
678
763
  ctx.causalTracker?.afterOperation(ctx.collection, operation.id, ctx.inTransaction);
679
- const serializedChanges = serializeRecord(resolvedData, ctx.definition.fields);
764
+ const serializedChanges = serializeRecord(writeData, ctx.definition.fields);
680
765
  const version = serializeRowVersion(operation.timestamp);
766
+ const mergedFieldVersions = parseFieldVersions(currentRow._field_versions);
767
+ for (const changedField of Object.keys(serializedChanges)) {
768
+ mergedFieldVersions[changedField] = version;
769
+ }
681
770
  const updateQuery = buildUpdateQuery(ctx.collection, id, {
682
771
  ...serializedChanges,
683
772
  _updated_at: operation.timestamp.wallTime,
684
- _version: version
773
+ _version: version,
774
+ _field_versions: serializeFieldVersions(mergedFieldVersions)
685
775
  });
686
776
  const opInsert = buildInsertQuery(
687
777
  `_kora_ops_${ctx.collection}`,
@@ -703,7 +793,7 @@ async function executeUpdate(ctx, id, data) {
703
793
  }
704
794
 
705
795
  // src/mutations/execute-delete.ts
706
- var import_core7 = require("@korajs/core");
796
+ var import_core8 = require("@korajs/core");
707
797
  async function executeDelete(ctx, id, options) {
708
798
  const currentRows = await ctx.adapter.query(
709
799
  `SELECT * FROM ${ctx.collection} WHERE id = ? AND _deleted = 0`,
@@ -718,7 +808,7 @@ async function executeDelete(ctx, id, options) {
718
808
  await ctx.adapter.transaction(async (tx) => {
719
809
  if (!operation) {
720
810
  const sequenceNumber = await allocateNextSequenceInTransaction(tx, ctx.nodeId);
721
- operation = await (0, import_core7.createOperation)(
811
+ operation = await (0, import_core8.createOperation)(
722
812
  {
723
813
  nodeId: ctx.nodeId,
724
814
  type: "delete",