@loro-dev/flock 4.4.5 → 4.5.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.
package/src/index.ts CHANGED
@@ -258,6 +258,20 @@ function normalizePeerId(peerId?: string): string {
258
258
  return peerId;
259
259
  }
260
260
 
261
+ const SYNC_TXN_CALLBACK_ERROR = "Flock.txn callback must be synchronous";
262
+
263
+ function isAsyncCallback(callback: Function): boolean {
264
+ return callback.constructor?.name === "AsyncFunction";
265
+ }
266
+
267
+ function isThenable(value: unknown): value is PromiseLike<unknown> {
268
+ return (
269
+ (typeof value === "object" || typeof value === "function") &&
270
+ value !== null &&
271
+ typeof (value as PromiseLike<unknown>).then === "function"
272
+ );
273
+ }
274
+
261
275
  type EncodableVersionVectorEntry = {
262
276
  peer: string;
263
277
  peerBytes: Uint8Array;
@@ -523,6 +537,16 @@ function normalizePruneBefore(
523
537
  return pruneTombstonesBefore;
524
538
  }
525
539
 
540
+ function normalizeMutationNow(now?: number | null): number | undefined {
541
+ if (now === undefined || now === null) {
542
+ return undefined;
543
+ }
544
+ if (typeof now !== "number" || !Number.isFinite(now)) {
545
+ throw new TypeError("now must be a finite number");
546
+ }
547
+ return now;
548
+ }
549
+
526
550
  function decodeVersionVectorFromRaw(raw: unknown): VersionVector {
527
551
  if (raw === null || typeof raw !== "object") {
528
552
  return {};
@@ -554,13 +578,34 @@ function decodeVersionVectorFromRaw(raw: unknown): VersionVector {
554
578
  }
555
579
 
556
580
  function encodeBound(bound?: ScanBound): Record<string, unknown> | undefined {
557
- if (!bound) {
581
+ if (bound === undefined) {
558
582
  return undefined;
559
583
  }
584
+ if (!bound || typeof bound !== "object") {
585
+ throw new TypeError("scan bound must be an object");
586
+ }
560
587
  if (bound.kind === "unbounded") {
561
588
  return { kind: "unbounded" };
562
589
  }
563
- return { kind: bound.kind, key: bound.key.slice() };
590
+ if (bound.kind !== "inclusive" && bound.kind !== "exclusive") {
591
+ throw new TypeError(
592
+ "scan bound kind must be inclusive, exclusive, or unbounded",
593
+ );
594
+ }
595
+ if (!Array.isArray(bound.key)) {
596
+ throw new TypeError("scan bound key must be a key array");
597
+ }
598
+ return { kind: bound.kind, key: cloneKey(bound.key) };
599
+ }
600
+
601
+ function normalizeScanPrefix(prefix: unknown): KeyPart[] | undefined {
602
+ if (prefix === undefined) {
603
+ return undefined;
604
+ }
605
+ if (!Array.isArray(prefix)) {
606
+ throw new TypeError("scan prefix must be a key array");
607
+ }
608
+ return cloneKey(prefix as KeyPart[]);
564
609
  }
565
610
 
566
611
  function decodeEntryInfo(raw: unknown): EntryInfo | undefined {
@@ -634,18 +679,53 @@ function normalizeRawEventPayload(
634
679
  return result;
635
680
  }
636
681
 
637
- const structuredCloneFn: (<T>(value: T) => T) | undefined = (
638
- globalThis as typeof globalThis & { structuredClone?: <T>(value: T) => T }
639
- ).structuredClone;
682
+ const JSON_SERIALIZATION_ERROR = "Value is not JSON serializable";
683
+
684
+ function validateJsonPart(value: unknown): unknown {
685
+ if (typeof value === "number" && !Number.isFinite(value)) {
686
+ throw new TypeError(JSON_SERIALIZATION_ERROR);
687
+ }
688
+ if (
689
+ value === undefined ||
690
+ typeof value === "function" ||
691
+ typeof value === "symbol" ||
692
+ typeof value === "bigint"
693
+ ) {
694
+ throw new TypeError(JSON_SERIALIZATION_ERROR);
695
+ }
696
+ return value;
697
+ }
698
+
699
+ function stringifyJson(value: unknown): string {
700
+ const encoded = JSON.stringify(value, (_key, part) =>
701
+ validateJsonPart(part),
702
+ );
703
+ if (encoded === undefined) {
704
+ throw new TypeError(JSON_SERIALIZATION_ERROR);
705
+ }
706
+ return encoded;
707
+ }
708
+
709
+ function stringifyStoredJson(value: Value | undefined): string {
710
+ return value === undefined ? "null" : stringifyJson(value);
711
+ }
640
712
 
641
713
  function cloneJson<T>(value: T): T {
642
714
  if (value === undefined) {
643
715
  return value;
644
716
  }
645
- if (structuredCloneFn) {
646
- return structuredCloneFn(value);
717
+ return JSON.parse(stringifyJson(value)) as T;
718
+ }
719
+
720
+ function cloneStoredJson(value: Value | undefined): Value {
721
+ return value === undefined ? null : cloneJson(value);
722
+ }
723
+
724
+ function cloneKey(key: KeyPart[]): KeyPart[] {
725
+ if (!Array.isArray(key)) {
726
+ throw new TypeError("key must be an array");
647
727
  }
648
- return JSON.parse(JSON.stringify(value)) as T;
728
+ return cloneJson(key) as KeyPart[];
649
729
  }
650
730
 
651
731
  function parseKeyString(key: string): KeyPart[] {
@@ -725,8 +805,11 @@ function createExportPayload(record: ExportRecord): ExportPayload {
725
805
  return payload;
726
806
  }
727
807
 
728
- function createPutPayload(value: Value, metadata?: MetadataMap): ExportPayload {
729
- const payload: ExportPayload = { data: cloneJson(value) };
808
+ function createPutPayload(
809
+ value: Value | undefined,
810
+ metadata?: MetadataMap,
811
+ ): ExportPayload {
812
+ const payload: ExportPayload = { data: cloneStoredJson(value) };
730
813
  const cleanMetadata = cloneMetadata(metadata);
731
814
  if (cleanMetadata !== undefined) {
732
815
  payload.metadata = cleanMetadata;
@@ -804,6 +887,21 @@ function normalizeImportDecision(
804
887
  return { accept: true };
805
888
  }
806
889
 
890
+ function payloadFromImportDecision(
891
+ decision: ImportDecision,
892
+ ): ImportPayload | undefined {
893
+ if (!decision || typeof decision !== "object") {
894
+ return undefined;
895
+ }
896
+ if ("accept" in decision) {
897
+ return undefined;
898
+ }
899
+ if ("data" in decision || "metadata" in decision) {
900
+ return decision as ImportPayload;
901
+ }
902
+ return undefined;
903
+ }
904
+
807
905
  function decodeImportReport(raw: unknown): ImportReport {
808
906
  if (!raw || typeof raw !== "object") {
809
907
  return { accepted: 0, skipped: [] };
@@ -828,10 +926,31 @@ function cloneBundle(bundle: ExportBundle): ExportBundle {
828
926
  return next;
829
927
  }
830
928
 
929
+ function isVersionVectorEntry(value: unknown): value is VersionVectorEntry {
930
+ return (
931
+ typeof value === "object" &&
932
+ value !== null &&
933
+ typeof (value as VersionVectorEntry).physicalTime === "number" &&
934
+ Number.isFinite((value as VersionVectorEntry).physicalTime) &&
935
+ typeof (value as VersionVectorEntry).logicalCounter === "number" &&
936
+ Number.isFinite((value as VersionVectorEntry).logicalCounter)
937
+ );
938
+ }
939
+
940
+ function isVersionVectorLike(value: unknown): value is VersionVector {
941
+ return (
942
+ typeof value === "object" &&
943
+ value !== null &&
944
+ Object.keys(value).length > 0 &&
945
+ Object.values(value).every(isVersionVectorEntry)
946
+ );
947
+ }
948
+
831
949
  function isExportOptions(value: unknown): value is ExportOptions {
832
950
  return (
833
951
  typeof value === "object" &&
834
952
  value !== null &&
953
+ !isVersionVectorLike(value) &&
835
954
  (Object.prototype.hasOwnProperty.call(value, "hooks") ||
836
955
  Object.prototype.hasOwnProperty.call(value, "from") ||
837
956
  Object.prototype.hasOwnProperty.call(value, "pruneTombstonesBefore") ||
@@ -894,34 +1013,50 @@ export class Flock {
894
1013
 
895
1014
  private putWithMetaInternal(
896
1015
  key: KeyPart[],
897
- value: Value,
1016
+ value: Value | undefined,
898
1017
  metadata?: MetadataMap,
899
1018
  now?: number,
1019
+ ): void {
1020
+ this.putWithMetaPrepared(
1021
+ cloneKey(key),
1022
+ value,
1023
+ metadata,
1024
+ normalizeMutationNow(now),
1025
+ );
1026
+ }
1027
+
1028
+ private putWithMetaPrepared(
1029
+ cleanKey: KeyPart[],
1030
+ value: Value | undefined,
1031
+ metadata: MetadataMap | undefined,
1032
+ cleanNow: number | undefined,
900
1033
  ): void {
901
1034
  const metadataClone = cloneMetadata(metadata);
902
1035
  put_with_meta_ffi(
903
1036
  this.inner,
904
- key,
905
- JSON.stringify(value),
1037
+ cleanKey,
1038
+ stringifyStoredJson(value),
906
1039
  metadataClone,
907
- now,
1040
+ cleanNow,
908
1041
  );
909
1042
  }
910
1043
 
911
1044
  private async putWithMetaWithHooks(
912
1045
  key: KeyPart[],
913
- value: Value,
1046
+ value: Value | undefined,
914
1047
  options: PutWithMetaOptions,
915
1048
  ): Promise<void> {
1049
+ const cleanKey = cloneKey(key);
1050
+ const cleanNow = normalizeMutationNow(options.now);
916
1051
  const basePayload = createPutPayload(value, options.metadata);
917
1052
  const transform = options.hooks?.transform;
918
1053
  if (!transform) {
919
- this.putWithMetaInternal(key, value, options.metadata, options.now);
1054
+ this.putWithMetaPrepared(cleanKey, value, options.metadata, cleanNow);
920
1055
  return;
921
1056
  }
922
1057
  const workingPayload = clonePayload(basePayload);
923
1058
  const transformed = await transform(
924
- { key: key.slice(), now: options.now },
1059
+ { key: cleanKey.slice(), now: cleanNow },
925
1060
  workingPayload,
926
1061
  );
927
1062
  const finalPayload = mergePayload(
@@ -932,11 +1067,11 @@ export class Flock {
932
1067
  if (finalValue === undefined) {
933
1068
  throw new TypeError("putWithMeta requires a data value");
934
1069
  }
935
- this.putWithMetaInternal(
936
- key,
1070
+ this.putWithMetaPrepared(
1071
+ cleanKey,
937
1072
  finalValue,
938
1073
  finalPayload.metadata,
939
- options.now,
1074
+ cleanNow,
940
1075
  );
941
1076
  }
942
1077
 
@@ -946,13 +1081,18 @@ export class Flock {
946
1081
  * @param value
947
1082
  * @param now
948
1083
  */
949
- put(key: KeyPart[], value: Value, now?: number): void {
950
- put_json_ffi(this.inner, key, JSON.stringify(value), now);
1084
+ put(key: KeyPart[], value: Value | undefined, now?: number): void {
1085
+ put_json_ffi(
1086
+ this.inner,
1087
+ cloneKey(key),
1088
+ stringifyStoredJson(value),
1089
+ normalizeMutationNow(now),
1090
+ );
951
1091
  }
952
1092
 
953
1093
  putWithMeta(
954
1094
  key: KeyPart[],
955
- value: Value,
1095
+ value: Value | undefined,
956
1096
  options?: PutWithMetaOptions,
957
1097
  ): void | Promise<void> {
958
1098
  const opts = options ?? {};
@@ -962,7 +1102,7 @@ export class Flock {
962
1102
  this.putWithMetaInternal(key, value, opts.metadata, opts.now);
963
1103
  }
964
1104
 
965
- set(key: KeyPart[], value: Value, now?: number): void {
1105
+ set(key: KeyPart[], value: Value | undefined, now?: number): void {
966
1106
  this.put(key, value, now);
967
1107
  }
968
1108
 
@@ -972,7 +1112,7 @@ export class Flock {
972
1112
  * @param now
973
1113
  */
974
1114
  delete(key: KeyPart[], now?: number): void {
975
- delete_ffi(this.inner, key, now);
1115
+ delete_ffi(this.inner, cloneKey(key), normalizeMutationNow(now));
976
1116
  }
977
1117
 
978
1118
  get(key: KeyPart[]): Value | undefined {
@@ -1119,7 +1259,10 @@ export class Flock {
1119
1259
  delete working.entries[key];
1120
1260
  continue;
1121
1261
  }
1122
- working.entries[key] = buildRecord(record.c, basePayload);
1262
+ working.entries[key] = buildRecord(
1263
+ record.c,
1264
+ mergePayload(basePayload, payloadFromImportDecision(decision)),
1265
+ );
1123
1266
  }
1124
1267
  }
1125
1268
  const coreReport = this.importJsonInternal(working);
@@ -1176,18 +1319,26 @@ export class Flock {
1176
1319
  }
1177
1320
 
1178
1321
  putMvr(key: KeyPart[], value: Value, now?: number): void {
1179
- put_mvr_ffi(this.inner, key, JSON.stringify(value), now);
1322
+ put_mvr_ffi(
1323
+ this.inner,
1324
+ cloneKey(key),
1325
+ stringifyJson(value),
1326
+ normalizeMutationNow(now),
1327
+ );
1180
1328
  }
1181
1329
 
1182
1330
  getMvr(key: KeyPart[]): Value[] {
1183
- const raw = get_mvr_ffi(this.inner, key);
1331
+ const raw = get_mvr_ffi(this.inner, cloneKey(key));
1184
1332
  return Array.isArray(raw) ? (raw as Value[]) : [];
1185
1333
  }
1186
1334
 
1187
1335
  scan(options: ScanOptions = {}): ScanRow[] {
1336
+ if (!options || typeof options !== "object") {
1337
+ throw new TypeError("scan options must be an object");
1338
+ }
1188
1339
  const start = encodeBound(options.start);
1189
1340
  const end = encodeBound(options.end);
1190
- const prefix = options.prefix ? options.prefix.slice() : undefined;
1341
+ const prefix = normalizeScanPrefix(options.prefix);
1191
1342
  const rows = scan_ffi(this.inner, start, end, prefix) as
1192
1343
  | RawScanRow[]
1193
1344
  | undefined;
@@ -1337,9 +1488,19 @@ export class Flock {
1337
1488
  "Cannot start transaction while autoDebounceCommit is active",
1338
1489
  );
1339
1490
  }
1491
+ if (isAsyncCallback(callback)) {
1492
+ throw new TypeError(SYNC_TXN_CALLBACK_ERROR);
1493
+ }
1340
1494
  txn_begin_ffi(this.inner);
1341
1495
  try {
1342
1496
  const result = callback();
1497
+ if (isThenable(result)) {
1498
+ void Promise.resolve(result).catch(() => {});
1499
+ if (is_in_txn_ffi(this.inner)) {
1500
+ txn_rollback_ffi(this.inner);
1501
+ }
1502
+ throw new TypeError(SYNC_TXN_CALLBACK_ERROR);
1503
+ }
1343
1504
  txn_commit_ffi(this.inner);
1344
1505
  return result;
1345
1506
  } catch (e) {
package/src/moonbit.d.ts CHANGED
@@ -47,6 +47,12 @@ export type Int64 = bigint & { [__brand]: "Int64" };
47
47
  */
48
48
  export type UInt64 = bigint & { [__brand]: "UInt64" };
49
49
 
50
+ /**
51
+ * Backed directly by `bigint`.
52
+ * Valid `V128` values are in the unsigned 128-bit range `[0, 2^128)`.
53
+ */
54
+ export type V128 = bigint & { [__brand]: "V128" };
55
+
50
56
  export type String = string;
51
57
 
52
58
  export type Bytes = Uint8Array;