@loro-dev/flock 4.4.4 → 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/README.md +9 -1
- package/dist/index.cjs +2 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +4 -3
- package/dist/index.d.ts +4 -3
- package/dist/index.mjs +2 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -2
- package/src/_moon_flock.ts +4773 -6231
- package/src/index.ts +190 -29
- package/src/moonbit.d.ts +21 -10
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 (
|
|
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
|
-
|
|
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
|
|
638
|
-
|
|
639
|
-
)
|
|
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
|
-
|
|
646
|
-
|
|
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
|
|
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(
|
|
729
|
-
|
|
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
|
-
|
|
905
|
-
|
|
1037
|
+
cleanKey,
|
|
1038
|
+
stringifyStoredJson(value),
|
|
906
1039
|
metadataClone,
|
|
907
|
-
|
|
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.
|
|
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:
|
|
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.
|
|
936
|
-
|
|
1070
|
+
this.putWithMetaPrepared(
|
|
1071
|
+
cleanKey,
|
|
937
1072
|
finalValue,
|
|
938
1073
|
finalPayload.metadata,
|
|
939
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
@@ -34,9 +34,24 @@ export type Float = number;
|
|
|
34
34
|
|
|
35
35
|
export type Double = number;
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Backed by `bigint`, but not equal to the backing value:
|
|
39
|
+
* runtime operations interpret it with signed 64-bit wraparound semantics.
|
|
40
|
+
* Use `BigInt.asIntN(64, x)` to get the numerically equivalent `bigint`.
|
|
41
|
+
*/
|
|
42
|
+
export type Int64 = bigint & { [__brand]: "Int64" };
|
|
38
43
|
|
|
39
|
-
|
|
44
|
+
/**
|
|
45
|
+
* Backed directly by `bigint`.
|
|
46
|
+
* Valid `UInt64` values are in the unsigned 64-bit range `[0, 2^64)`.
|
|
47
|
+
*/
|
|
48
|
+
export type UInt64 = bigint & { [__brand]: "UInt64" };
|
|
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" };
|
|
40
55
|
|
|
41
56
|
export type String = string;
|
|
42
57
|
|
|
@@ -44,14 +59,10 @@ export type Bytes = Uint8Array;
|
|
|
44
59
|
|
|
45
60
|
export type FixedArray<T> = T[];
|
|
46
61
|
|
|
47
|
-
export type UnboxedOption<T> =
|
|
48
|
-
| /* Some */ T
|
|
49
|
-
| /* None */ undefined;
|
|
62
|
+
export type UnboxedOption<T> = /* Some */ T | /* None */ undefined;
|
|
50
63
|
|
|
51
|
-
export type UnboxedOptionAsInt<T> =
|
|
52
|
-
| /* Some */ T
|
|
53
|
-
| /* None */ -1;
|
|
64
|
+
export type UnboxedOptionAsInt<T> = /* Some */ T | /* None */ -1;
|
|
54
65
|
|
|
55
66
|
export type Result<T, E> =
|
|
56
|
-
| /* Ok */ { $tag: 1
|
|
57
|
-
| /* Err */ { $tag: 0
|
|
67
|
+
| /* Ok */ { $tag: 1; _0: T }
|
|
68
|
+
| /* Err */ { $tag: 0; _0: E };
|