@loro-dev/flock 4.4.5 → 4.5.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.
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,163 @@ 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
+ const JSON_SERIALIZATION_WARNING_PREFIX = "[flock] JSON value normalized:";
684
+
685
+ function warnJsonNormalization(message: string): void {
686
+ try {
687
+ console.warn(`${JSON_SERIALIZATION_WARNING_PREFIX} ${message}`);
688
+ } catch {
689
+ // Ignore console failures so value normalization never becomes a write error.
690
+ }
691
+ }
692
+
693
+ function normalizeInvalidJsonValue(message: string): null {
694
+ warnJsonNormalization(message);
695
+ return null;
696
+ }
697
+
698
+ function normalizeJsonValue(
699
+ value: unknown,
700
+ seen: WeakSet<object>,
701
+ inObjectEntry = false,
702
+ ): unknown {
703
+ if (typeof value === "number" && !Number.isFinite(value)) {
704
+ return normalizeInvalidJsonValue("non-finite number stored as null");
705
+ }
706
+ if (value === undefined) {
707
+ return inObjectEntry ? undefined : null;
708
+ }
709
+ if (typeof value === "function" || typeof value === "symbol") {
710
+ warnJsonNormalization(`${typeof value} value omitted`);
711
+ return inObjectEntry ? undefined : null;
712
+ }
713
+ if (typeof value === "bigint") {
714
+ if (
715
+ value <= BigInt(Number.MAX_SAFE_INTEGER) &&
716
+ value >= BigInt(Number.MIN_SAFE_INTEGER)
717
+ ) {
718
+ return Number(value);
719
+ }
720
+ return normalizeInvalidJsonValue(
721
+ "bigint exceeds JavaScript safe integer range and was stored as null",
722
+ );
723
+ }
724
+ if (!value || typeof value !== "object") {
725
+ return value;
726
+ }
727
+
728
+ const toJson = (value as { toJSON?: unknown }).toJSON;
729
+ if (typeof toJson === "function") {
730
+ try {
731
+ return normalizeJsonValue(toJson.call(value), seen, inObjectEntry);
732
+ } catch {
733
+ return normalizeInvalidJsonValue("toJSON threw and value was stored as null");
734
+ }
735
+ }
736
+
737
+ if (seen.has(value)) {
738
+ return normalizeInvalidJsonValue("circular reference stored as null");
739
+ }
740
+ seen.add(value);
741
+ if (Array.isArray(value)) {
742
+ const result = value.map((item) => {
743
+ const normalized = normalizeJsonValue(item, seen);
744
+ return normalized === undefined ? null : normalized;
745
+ });
746
+ seen.delete(value);
747
+ return result;
748
+ }
749
+
750
+ let keys: string[];
751
+ try {
752
+ keys = Object.keys(value);
753
+ } catch {
754
+ seen.delete(value);
755
+ return normalizeInvalidJsonValue("object keys could not be read and value was stored as null");
756
+ }
757
+
758
+ const result: Record<string, unknown> = {};
759
+ for (const key of keys) {
760
+ let rawValue: unknown;
761
+ try {
762
+ rawValue = (value as Record<string, unknown>)[key];
763
+ } catch {
764
+ result[key] = normalizeInvalidJsonValue(
765
+ "object property could not be read and was stored as null",
766
+ );
767
+ continue;
768
+ }
769
+ const normalized = normalizeJsonValue(
770
+ rawValue,
771
+ seen,
772
+ true,
773
+ );
774
+ if (normalized !== undefined) {
775
+ result[key] = normalized;
776
+ }
777
+ }
778
+ seen.delete(value);
779
+ return result;
780
+ }
781
+
782
+ function validateJsonPart(value: unknown): unknown {
783
+ if (typeof value === "number" && !Number.isFinite(value)) {
784
+ throw new TypeError(JSON_SERIALIZATION_ERROR);
785
+ }
786
+ if (
787
+ value === undefined ||
788
+ typeof value === "function" ||
789
+ typeof value === "symbol" ||
790
+ typeof value === "bigint"
791
+ ) {
792
+ throw new TypeError(JSON_SERIALIZATION_ERROR);
793
+ }
794
+ return value;
795
+ }
796
+
797
+ function stringifyJson(value: unknown): string {
798
+ const normalized = normalizeJsonValue(value, new WeakSet<object>());
799
+ return JSON.stringify(normalized) ?? "null";
800
+ }
801
+
802
+ function stringifyStrictJson(value: unknown): string {
803
+ const encoded = JSON.stringify(value, (_key, part) =>
804
+ validateJsonPart(part),
805
+ );
806
+ if (encoded === undefined) {
807
+ throw new TypeError(JSON_SERIALIZATION_ERROR);
808
+ }
809
+ return encoded;
810
+ }
811
+
812
+ function stringifyStoredJson(value: Value | undefined): string {
813
+ return value === undefined ? "null" : stringifyJson(value);
814
+ }
640
815
 
641
816
  function cloneJson<T>(value: T): T {
642
817
  if (value === undefined) {
643
818
  return value;
644
819
  }
645
- if (structuredCloneFn) {
646
- return structuredCloneFn(value);
820
+ return JSON.parse(stringifyJson(value)) as T;
821
+ }
822
+
823
+ function cloneStrictJson<T>(value: T): T {
824
+ if (value === undefined) {
825
+ return value;
647
826
  }
648
- return JSON.parse(JSON.stringify(value)) as T;
827
+ return JSON.parse(stringifyStrictJson(value)) as T;
828
+ }
829
+
830
+ function cloneStoredJson(value: Value | undefined): Value {
831
+ return value === undefined ? null : cloneJson(value);
832
+ }
833
+
834
+ function cloneKey(key: KeyPart[]): KeyPart[] {
835
+ if (!Array.isArray(key)) {
836
+ throw new TypeError("key must be an array");
837
+ }
838
+ return cloneStrictJson(key) as KeyPart[];
649
839
  }
650
840
 
651
841
  function parseKeyString(key: string): KeyPart[] {
@@ -725,8 +915,11 @@ function createExportPayload(record: ExportRecord): ExportPayload {
725
915
  return payload;
726
916
  }
727
917
 
728
- function createPutPayload(value: Value, metadata?: MetadataMap): ExportPayload {
729
- const payload: ExportPayload = { data: cloneJson(value) };
918
+ function createPutPayload(
919
+ value: Value | undefined,
920
+ metadata?: MetadataMap,
921
+ ): ExportPayload {
922
+ const payload: ExportPayload = { data: cloneStoredJson(value) };
730
923
  const cleanMetadata = cloneMetadata(metadata);
731
924
  if (cleanMetadata !== undefined) {
732
925
  payload.metadata = cleanMetadata;
@@ -804,6 +997,21 @@ function normalizeImportDecision(
804
997
  return { accept: true };
805
998
  }
806
999
 
1000
+ function payloadFromImportDecision(
1001
+ decision: ImportDecision,
1002
+ ): ImportPayload | undefined {
1003
+ if (!decision || typeof decision !== "object") {
1004
+ return undefined;
1005
+ }
1006
+ if ("accept" in decision) {
1007
+ return undefined;
1008
+ }
1009
+ if ("data" in decision || "metadata" in decision) {
1010
+ return decision as ImportPayload;
1011
+ }
1012
+ return undefined;
1013
+ }
1014
+
807
1015
  function decodeImportReport(raw: unknown): ImportReport {
808
1016
  if (!raw || typeof raw !== "object") {
809
1017
  return { accepted: 0, skipped: [] };
@@ -828,10 +1036,31 @@ function cloneBundle(bundle: ExportBundle): ExportBundle {
828
1036
  return next;
829
1037
  }
830
1038
 
1039
+ function isVersionVectorEntry(value: unknown): value is VersionVectorEntry {
1040
+ return (
1041
+ typeof value === "object" &&
1042
+ value !== null &&
1043
+ typeof (value as VersionVectorEntry).physicalTime === "number" &&
1044
+ Number.isFinite((value as VersionVectorEntry).physicalTime) &&
1045
+ typeof (value as VersionVectorEntry).logicalCounter === "number" &&
1046
+ Number.isFinite((value as VersionVectorEntry).logicalCounter)
1047
+ );
1048
+ }
1049
+
1050
+ function isVersionVectorLike(value: unknown): value is VersionVector {
1051
+ return (
1052
+ typeof value === "object" &&
1053
+ value !== null &&
1054
+ Object.keys(value).length > 0 &&
1055
+ Object.values(value).every(isVersionVectorEntry)
1056
+ );
1057
+ }
1058
+
831
1059
  function isExportOptions(value: unknown): value is ExportOptions {
832
1060
  return (
833
1061
  typeof value === "object" &&
834
1062
  value !== null &&
1063
+ !isVersionVectorLike(value) &&
835
1064
  (Object.prototype.hasOwnProperty.call(value, "hooks") ||
836
1065
  Object.prototype.hasOwnProperty.call(value, "from") ||
837
1066
  Object.prototype.hasOwnProperty.call(value, "pruneTombstonesBefore") ||
@@ -894,34 +1123,50 @@ export class Flock {
894
1123
 
895
1124
  private putWithMetaInternal(
896
1125
  key: KeyPart[],
897
- value: Value,
1126
+ value: Value | undefined,
898
1127
  metadata?: MetadataMap,
899
1128
  now?: number,
1129
+ ): void {
1130
+ this.putWithMetaPrepared(
1131
+ cloneKey(key),
1132
+ value,
1133
+ metadata,
1134
+ normalizeMutationNow(now),
1135
+ );
1136
+ }
1137
+
1138
+ private putWithMetaPrepared(
1139
+ cleanKey: KeyPart[],
1140
+ value: Value | undefined,
1141
+ metadata: MetadataMap | undefined,
1142
+ cleanNow: number | undefined,
900
1143
  ): void {
901
1144
  const metadataClone = cloneMetadata(metadata);
902
1145
  put_with_meta_ffi(
903
1146
  this.inner,
904
- key,
905
- JSON.stringify(value),
1147
+ cleanKey,
1148
+ stringifyStoredJson(value),
906
1149
  metadataClone,
907
- now,
1150
+ cleanNow,
908
1151
  );
909
1152
  }
910
1153
 
911
1154
  private async putWithMetaWithHooks(
912
1155
  key: KeyPart[],
913
- value: Value,
1156
+ value: Value | undefined,
914
1157
  options: PutWithMetaOptions,
915
1158
  ): Promise<void> {
1159
+ const cleanKey = cloneKey(key);
1160
+ const cleanNow = normalizeMutationNow(options.now);
916
1161
  const basePayload = createPutPayload(value, options.metadata);
917
1162
  const transform = options.hooks?.transform;
918
1163
  if (!transform) {
919
- this.putWithMetaInternal(key, value, options.metadata, options.now);
1164
+ this.putWithMetaPrepared(cleanKey, value, options.metadata, cleanNow);
920
1165
  return;
921
1166
  }
922
1167
  const workingPayload = clonePayload(basePayload);
923
1168
  const transformed = await transform(
924
- { key: key.slice(), now: options.now },
1169
+ { key: cleanKey.slice(), now: cleanNow },
925
1170
  workingPayload,
926
1171
  );
927
1172
  const finalPayload = mergePayload(
@@ -932,11 +1177,11 @@ export class Flock {
932
1177
  if (finalValue === undefined) {
933
1178
  throw new TypeError("putWithMeta requires a data value");
934
1179
  }
935
- this.putWithMetaInternal(
936
- key,
1180
+ this.putWithMetaPrepared(
1181
+ cleanKey,
937
1182
  finalValue,
938
1183
  finalPayload.metadata,
939
- options.now,
1184
+ cleanNow,
940
1185
  );
941
1186
  }
942
1187
 
@@ -946,13 +1191,18 @@ export class Flock {
946
1191
  * @param value
947
1192
  * @param now
948
1193
  */
949
- put(key: KeyPart[], value: Value, now?: number): void {
950
- put_json_ffi(this.inner, key, JSON.stringify(value), now);
1194
+ put(key: KeyPart[], value: Value | undefined, now?: number): void {
1195
+ put_json_ffi(
1196
+ this.inner,
1197
+ cloneKey(key),
1198
+ stringifyStoredJson(value),
1199
+ normalizeMutationNow(now),
1200
+ );
951
1201
  }
952
1202
 
953
1203
  putWithMeta(
954
1204
  key: KeyPart[],
955
- value: Value,
1205
+ value: Value | undefined,
956
1206
  options?: PutWithMetaOptions,
957
1207
  ): void | Promise<void> {
958
1208
  const opts = options ?? {};
@@ -962,7 +1212,7 @@ export class Flock {
962
1212
  this.putWithMetaInternal(key, value, opts.metadata, opts.now);
963
1213
  }
964
1214
 
965
- set(key: KeyPart[], value: Value, now?: number): void {
1215
+ set(key: KeyPart[], value: Value | undefined, now?: number): void {
966
1216
  this.put(key, value, now);
967
1217
  }
968
1218
 
@@ -972,7 +1222,7 @@ export class Flock {
972
1222
  * @param now
973
1223
  */
974
1224
  delete(key: KeyPart[], now?: number): void {
975
- delete_ffi(this.inner, key, now);
1225
+ delete_ffi(this.inner, cloneKey(key), normalizeMutationNow(now));
976
1226
  }
977
1227
 
978
1228
  get(key: KeyPart[]): Value | undefined {
@@ -1119,7 +1369,10 @@ export class Flock {
1119
1369
  delete working.entries[key];
1120
1370
  continue;
1121
1371
  }
1122
- working.entries[key] = buildRecord(record.c, basePayload);
1372
+ working.entries[key] = buildRecord(
1373
+ record.c,
1374
+ mergePayload(basePayload, payloadFromImportDecision(decision)),
1375
+ );
1123
1376
  }
1124
1377
  }
1125
1378
  const coreReport = this.importJsonInternal(working);
@@ -1176,18 +1429,26 @@ export class Flock {
1176
1429
  }
1177
1430
 
1178
1431
  putMvr(key: KeyPart[], value: Value, now?: number): void {
1179
- put_mvr_ffi(this.inner, key, JSON.stringify(value), now);
1432
+ put_mvr_ffi(
1433
+ this.inner,
1434
+ cloneKey(key),
1435
+ stringifyJson(value),
1436
+ normalizeMutationNow(now),
1437
+ );
1180
1438
  }
1181
1439
 
1182
1440
  getMvr(key: KeyPart[]): Value[] {
1183
- const raw = get_mvr_ffi(this.inner, key);
1441
+ const raw = get_mvr_ffi(this.inner, cloneKey(key));
1184
1442
  return Array.isArray(raw) ? (raw as Value[]) : [];
1185
1443
  }
1186
1444
 
1187
1445
  scan(options: ScanOptions = {}): ScanRow[] {
1446
+ if (!options || typeof options !== "object") {
1447
+ throw new TypeError("scan options must be an object");
1448
+ }
1188
1449
  const start = encodeBound(options.start);
1189
1450
  const end = encodeBound(options.end);
1190
- const prefix = options.prefix ? options.prefix.slice() : undefined;
1451
+ const prefix = normalizeScanPrefix(options.prefix);
1191
1452
  const rows = scan_ffi(this.inner, start, end, prefix) as
1192
1453
  | RawScanRow[]
1193
1454
  | undefined;
@@ -1337,9 +1598,19 @@ export class Flock {
1337
1598
  "Cannot start transaction while autoDebounceCommit is active",
1338
1599
  );
1339
1600
  }
1601
+ if (isAsyncCallback(callback)) {
1602
+ throw new TypeError(SYNC_TXN_CALLBACK_ERROR);
1603
+ }
1340
1604
  txn_begin_ffi(this.inner);
1341
1605
  try {
1342
1606
  const result = callback();
1607
+ if (isThenable(result)) {
1608
+ void Promise.resolve(result).catch(() => {});
1609
+ if (is_in_txn_ffi(this.inner)) {
1610
+ txn_rollback_ffi(this.inner);
1611
+ }
1612
+ throw new TypeError(SYNC_TXN_CALLBACK_ERROR);
1613
+ }
1343
1614
  txn_commit_ffi(this.inner);
1344
1615
  return result;
1345
1616
  } 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;