@loro-dev/flock-sqlite 0.8.0 → 0.9.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/README.md +60 -0
- package/dist/index.cjs +4 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +47 -7
- package/dist/index.d.ts +47 -7
- package/dist/index.mjs +4 -4
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/event-batcher.ts +264 -0
- package/src/index.ts +390 -213
- package/src/lru.ts +74 -0
- package/src/multi-tab-env.ts +79 -0
- package/src/multi-tab.ts +107 -0
- package/src/types.ts +5 -0
- package/src/write-coordinator.ts +468 -0
package/src/index.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { openStore, type UniStoreConnection } from "@loro-dev/unisqlite";
|
|
2
2
|
import { computeDigest, type DigestRow } from "./digest";
|
|
3
|
+
import { EventBatcher } from "./event-batcher";
|
|
4
|
+
import { LruSet } from "./lru";
|
|
3
5
|
import {
|
|
4
6
|
compareBytes,
|
|
5
7
|
decodeKeyParts,
|
|
@@ -7,6 +9,21 @@ import {
|
|
|
7
9
|
keyToString,
|
|
8
10
|
prefixUpperBound,
|
|
9
11
|
} from "./key-encoding";
|
|
12
|
+
import type {
|
|
13
|
+
FlockCommit,
|
|
14
|
+
FlockWriteRequest,
|
|
15
|
+
RequestId,
|
|
16
|
+
TabId,
|
|
17
|
+
} from "./multi-tab";
|
|
18
|
+
import {
|
|
19
|
+
createBroadcastChannelTransport,
|
|
20
|
+
createDefaultRuntime,
|
|
21
|
+
type FlockRoleProvider,
|
|
22
|
+
type FlockRuntime,
|
|
23
|
+
type FlockTransport,
|
|
24
|
+
type FlockTransportFactory,
|
|
25
|
+
} from "./multi-tab-env";
|
|
26
|
+
import { createWriteCoordinator, type WriteCoordinator } from "./write-coordinator";
|
|
10
27
|
import type {
|
|
11
28
|
EntryClock,
|
|
12
29
|
Event,
|
|
@@ -25,6 +42,8 @@ import type {
|
|
|
25
42
|
PutHooks,
|
|
26
43
|
PutWithMetaOptions,
|
|
27
44
|
EventListener,
|
|
45
|
+
FlockSQLiteRole,
|
|
46
|
+
RoleChangeListener,
|
|
28
47
|
ScanBound,
|
|
29
48
|
ScanOptions,
|
|
30
49
|
ScanRow,
|
|
@@ -51,6 +70,13 @@ type KvRow = {
|
|
|
51
70
|
|
|
52
71
|
type ExportQueryRow = KvRow;
|
|
53
72
|
|
|
73
|
+
type PendingEvent = {
|
|
74
|
+
key: KeyPart[];
|
|
75
|
+
payload: ExportPayload;
|
|
76
|
+
source: string;
|
|
77
|
+
clock: EntryClock;
|
|
78
|
+
};
|
|
79
|
+
|
|
54
80
|
type PutOperation = {
|
|
55
81
|
key: KeyPart[];
|
|
56
82
|
payload: ExportPayload;
|
|
@@ -58,7 +84,7 @@ type PutOperation = {
|
|
|
58
84
|
skipSameValue: boolean;
|
|
59
85
|
source: string;
|
|
60
86
|
clock?: EntryClock;
|
|
61
|
-
eventSink
|
|
87
|
+
eventSink: PendingEvent[];
|
|
62
88
|
};
|
|
63
89
|
|
|
64
90
|
type EncodableVersionVectorEntry = {
|
|
@@ -68,7 +94,7 @@ type EncodableVersionVectorEntry = {
|
|
|
68
94
|
counter: number;
|
|
69
95
|
};
|
|
70
96
|
|
|
71
|
-
export interface VersionVector extends VersionVectorType {}
|
|
97
|
+
export interface VersionVector extends VersionVectorType { }
|
|
72
98
|
|
|
73
99
|
const textEncoder = new TextEncoder();
|
|
74
100
|
const textDecoder = new TextDecoder();
|
|
@@ -585,6 +611,16 @@ export type FlockSQLiteOptions = {
|
|
|
585
611
|
peerId?: string;
|
|
586
612
|
connection?: UniStoreConnection;
|
|
587
613
|
tablePrefix?: string;
|
|
614
|
+
/**
|
|
615
|
+
* Advanced: Inject multi-tab environment dependencies.
|
|
616
|
+
* Defaults to BroadcastChannel + UniSQLite role + real timers when available.
|
|
617
|
+
*/
|
|
618
|
+
multiTab?: {
|
|
619
|
+
transportFactory?: FlockTransportFactory;
|
|
620
|
+
roleProvider?: FlockRoleProvider;
|
|
621
|
+
runtime?: FlockRuntime;
|
|
622
|
+
tabId?: string;
|
|
623
|
+
};
|
|
588
624
|
};
|
|
589
625
|
|
|
590
626
|
type TableNames = {
|
|
@@ -620,6 +656,89 @@ function buildTableNames(prefix: string): TableNames {
|
|
|
620
656
|
};
|
|
621
657
|
}
|
|
622
658
|
|
|
659
|
+
function normalizeRole(role: unknown): FlockSQLiteRole | undefined {
|
|
660
|
+
if (role === "host" || role === "participant" || role === "unknown") {
|
|
661
|
+
return role;
|
|
662
|
+
}
|
|
663
|
+
return undefined;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function resolveRole(db: UniStoreConnection): FlockSQLiteRole {
|
|
667
|
+
const role = (db as unknown as { getRole?: () => unknown }).getRole?.();
|
|
668
|
+
const normalized = normalizeRole(role);
|
|
669
|
+
if (normalized) {
|
|
670
|
+
return normalized;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// Back-compat with older UniSQLite versions that only expose getSQLiteInfo().
|
|
674
|
+
const info = (db as unknown as { getSQLiteInfo?: () => unknown }).getSQLiteInfo?.();
|
|
675
|
+
if (info && typeof info === "object") {
|
|
676
|
+
const { isHost, isReady } = info as { isHost?: unknown; isReady?: unknown };
|
|
677
|
+
if (isHost === true && isReady === true) {
|
|
678
|
+
return "host";
|
|
679
|
+
}
|
|
680
|
+
if (isReady === false) {
|
|
681
|
+
return "unknown";
|
|
682
|
+
}
|
|
683
|
+
if (isReady === true) {
|
|
684
|
+
return "participant";
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// Non-browser adapters typically behave like a stable host.
|
|
689
|
+
return "host";
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function createDefaultRoleProvider(db: UniStoreConnection): FlockRoleProvider {
|
|
693
|
+
const subscribeRoleChange = (
|
|
694
|
+
db as unknown as {
|
|
695
|
+
subscribeRoleChange?: (listener: (role: unknown) => void) => () => void;
|
|
696
|
+
}
|
|
697
|
+
).subscribeRoleChange;
|
|
698
|
+
|
|
699
|
+
return {
|
|
700
|
+
getRole: () => resolveRole(db),
|
|
701
|
+
subscribeRoleChange:
|
|
702
|
+
typeof subscribeRoleChange === "function"
|
|
703
|
+
? (listener) =>
|
|
704
|
+
subscribeRoleChange.call(db, (role) => {
|
|
705
|
+
const normalized = normalizeRole(role);
|
|
706
|
+
if (!normalized) {
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
listener(normalized);
|
|
710
|
+
})
|
|
711
|
+
: undefined,
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
const MAX_SEEN_COMMITS = 2048;
|
|
716
|
+
|
|
717
|
+
function buildFlockChannelName(path: string, tablePrefix: string): string {
|
|
718
|
+
if (tablePrefix) {
|
|
719
|
+
return `flock:rpc:${path}:${tablePrefix}`;
|
|
720
|
+
}
|
|
721
|
+
return `flock:rpc:${path}`;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function resolveTabIdWithRuntime(
|
|
725
|
+
db: UniStoreConnection,
|
|
726
|
+
runtime: FlockRuntime,
|
|
727
|
+
override?: string,
|
|
728
|
+
): TabId {
|
|
729
|
+
if (typeof override === "string" && override.length > 0) {
|
|
730
|
+
return override;
|
|
731
|
+
}
|
|
732
|
+
const info = (db as unknown as { getSQLiteInfo?: () => unknown }).getSQLiteInfo?.();
|
|
733
|
+
if (info && typeof info === "object") {
|
|
734
|
+
const tabId = (info as { tabId?: unknown }).tabId;
|
|
735
|
+
if (typeof tabId === "string" && tabId.length > 0) {
|
|
736
|
+
return tabId;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
return runtime.randomUUID();
|
|
740
|
+
}
|
|
741
|
+
|
|
623
742
|
export class FlockSQLite {
|
|
624
743
|
private db: UniStoreConnection;
|
|
625
744
|
private peerIdValue: string;
|
|
@@ -627,24 +746,11 @@ export class FlockSQLite {
|
|
|
627
746
|
private maxHlc: { physicalTime: number; logicalCounter: number };
|
|
628
747
|
private listeners: Set<EventListener>;
|
|
629
748
|
private tables: TableNames;
|
|
630
|
-
|
|
631
|
-
private
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
private debounceState:
|
|
636
|
-
| {
|
|
637
|
-
timeout: number;
|
|
638
|
-
maxDebounceTime: number;
|
|
639
|
-
timerId: ReturnType<typeof setTimeout> | undefined;
|
|
640
|
-
maxTimerId: ReturnType<typeof setTimeout> | undefined;
|
|
641
|
-
pendingEvents: Array<{
|
|
642
|
-
key: KeyPart[];
|
|
643
|
-
payload: ExportPayload;
|
|
644
|
-
source: string;
|
|
645
|
-
}>;
|
|
646
|
-
}
|
|
647
|
-
| undefined;
|
|
749
|
+
private readonly runtime: FlockRuntime;
|
|
750
|
+
private readonly eventBatcher: EventBatcher<PendingEvent>;
|
|
751
|
+
private readonly coordinator: WriteCoordinator;
|
|
752
|
+
private readonly seenCommitIds: LruSet<string>;
|
|
753
|
+
private closed: boolean;
|
|
648
754
|
|
|
649
755
|
private constructor(
|
|
650
756
|
db: UniStoreConnection,
|
|
@@ -652,6 +758,10 @@ export class FlockSQLite {
|
|
|
652
758
|
vv: Map<string, VersionVectorEntry>,
|
|
653
759
|
maxHlc: { physicalTime: number; logicalCounter: number },
|
|
654
760
|
tables: TableNames,
|
|
761
|
+
tabId: TabId,
|
|
762
|
+
runtime: FlockRuntime,
|
|
763
|
+
transport: FlockTransport | undefined,
|
|
764
|
+
roleProvider: FlockRoleProvider,
|
|
655
765
|
) {
|
|
656
766
|
this.db = db;
|
|
657
767
|
this.peerIdValue = peerId;
|
|
@@ -659,8 +769,22 @@ export class FlockSQLite {
|
|
|
659
769
|
this.maxHlc = maxHlc;
|
|
660
770
|
this.listeners = new Set();
|
|
661
771
|
this.tables = tables;
|
|
662
|
-
this.
|
|
663
|
-
this.
|
|
772
|
+
this.runtime = runtime;
|
|
773
|
+
this.eventBatcher = new EventBatcher({
|
|
774
|
+
runtime,
|
|
775
|
+
emit: (source, events) => this.emitEvents(source, events),
|
|
776
|
+
});
|
|
777
|
+
this.seenCommitIds = new LruSet(MAX_SEEN_COMMITS);
|
|
778
|
+
this.coordinator = createWriteCoordinator({
|
|
779
|
+
runtime,
|
|
780
|
+
tabId,
|
|
781
|
+
transport,
|
|
782
|
+
roleProvider,
|
|
783
|
+
ingestCommit: (commit, bufferable) => this.applyCommit(commit, bufferable),
|
|
784
|
+
executeWriteRequest: (payload, origin, requestId) =>
|
|
785
|
+
this.executeWriteRequest(payload, origin, requestId),
|
|
786
|
+
});
|
|
787
|
+
this.closed = false;
|
|
664
788
|
}
|
|
665
789
|
|
|
666
790
|
static async open(options: FlockSQLiteOptions): Promise<FlockSQLite> {
|
|
@@ -670,7 +794,24 @@ export class FlockSQLite {
|
|
|
670
794
|
await FlockSQLite.ensureSchema(db, tables);
|
|
671
795
|
const peerId = await FlockSQLite.resolvePeerId(db, tables, options.peerId);
|
|
672
796
|
const { vv, maxHlc } = await FlockSQLite.loadVersionState(db, tables);
|
|
673
|
-
|
|
797
|
+
const runtime = options.multiTab?.runtime ?? createDefaultRuntime();
|
|
798
|
+
const roleProvider = options.multiTab?.roleProvider ?? createDefaultRoleProvider(db);
|
|
799
|
+
const channelName = buildFlockChannelName(options.path, prefix);
|
|
800
|
+
const transportFactory: FlockTransportFactory =
|
|
801
|
+
options.multiTab?.transportFactory ?? createBroadcastChannelTransport;
|
|
802
|
+
const transport = transportFactory(channelName);
|
|
803
|
+
const tabId = resolveTabIdWithRuntime(db, runtime, options.multiTab?.tabId);
|
|
804
|
+
return new FlockSQLite(
|
|
805
|
+
db,
|
|
806
|
+
peerId,
|
|
807
|
+
vv,
|
|
808
|
+
maxHlc,
|
|
809
|
+
tables,
|
|
810
|
+
tabId,
|
|
811
|
+
runtime,
|
|
812
|
+
transport,
|
|
813
|
+
roleProvider,
|
|
814
|
+
);
|
|
674
815
|
}
|
|
675
816
|
|
|
676
817
|
static async fromJson(
|
|
@@ -682,21 +823,114 @@ export class FlockSQLite {
|
|
|
682
823
|
}
|
|
683
824
|
|
|
684
825
|
async close(): Promise<void> {
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
826
|
+
if (this.closed) {
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
this.closed = true;
|
|
830
|
+
this.coordinator.close();
|
|
831
|
+
this.eventBatcher.close();
|
|
832
|
+
|
|
833
|
+
await this.db.close();
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
getRole(): FlockSQLiteRole {
|
|
837
|
+
return this.coordinator.getRole();
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
isHost(): boolean {
|
|
841
|
+
return this.coordinator.isHost();
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
subscribeRoleChange(listener: RoleChangeListener): () => void {
|
|
845
|
+
return this.coordinator.subscribeRoleChange(listener);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
private dispatchWriteRequest<T>(
|
|
849
|
+
payload: FlockWriteRequest,
|
|
850
|
+
): Promise<{ commit: FlockCommit; result: T }> {
|
|
851
|
+
return this.coordinator.dispatchWriteRequest(payload);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
private applyCommit(commit: FlockCommit, bufferable: boolean): void {
|
|
855
|
+
if (this.seenCommitIds.has(commit.commitId)) {
|
|
856
|
+
return;
|
|
688
857
|
}
|
|
858
|
+
this.seenCommitIds.add(commit.commitId);
|
|
689
859
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
this.emitEvents("local", pending);
|
|
860
|
+
if (commit.meta?.peerId) {
|
|
861
|
+
try {
|
|
862
|
+
this.peerIdValue = normalizePeerId(commit.meta.peerId);
|
|
863
|
+
} catch {
|
|
864
|
+
// Ignore invalid peer ids; they should only come from user input.
|
|
696
865
|
}
|
|
697
866
|
}
|
|
698
867
|
|
|
699
|
-
|
|
868
|
+
const pendingEvents: PendingEvent[] = commit.events.map((event) => ({
|
|
869
|
+
key: cloneJson(event.key),
|
|
870
|
+
payload: clonePayload(event.payload),
|
|
871
|
+
source: commit.source,
|
|
872
|
+
clock: { ...event.clock },
|
|
873
|
+
}));
|
|
874
|
+
|
|
875
|
+
for (const event of pendingEvents) {
|
|
876
|
+
this.bumpVersion(event.clock);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
if (pendingEvents.length === 0) {
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
this.eventBatcher.handleCommitEvents(
|
|
884
|
+
commit.source,
|
|
885
|
+
pendingEvents,
|
|
886
|
+
bufferable,
|
|
887
|
+
);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
private async executeWriteRequest(
|
|
891
|
+
payload: FlockWriteRequest,
|
|
892
|
+
origin: TabId,
|
|
893
|
+
requestId: RequestId,
|
|
894
|
+
): Promise<{ commit: FlockCommit; result: unknown }> {
|
|
895
|
+
const events: PendingEvent[] = [];
|
|
896
|
+
let result: unknown = undefined;
|
|
897
|
+
let meta: FlockCommit["meta"] | undefined;
|
|
898
|
+
|
|
899
|
+
if (payload.kind === "apply") {
|
|
900
|
+
await this.applyOperation({
|
|
901
|
+
key: payload.key,
|
|
902
|
+
payload: payload.payload,
|
|
903
|
+
now: payload.now,
|
|
904
|
+
skipSameValue: payload.skipSameValue,
|
|
905
|
+
source: payload.source,
|
|
906
|
+
eventSink: events,
|
|
907
|
+
});
|
|
908
|
+
} else if (payload.kind === "putMvr") {
|
|
909
|
+
await this.putMvrInternal(payload.key, payload.value, payload.now, events);
|
|
910
|
+
} else if (payload.kind === "import") {
|
|
911
|
+
result = await this.importBundleInternal(payload.bundle, events);
|
|
912
|
+
} else if (payload.kind === "setPeerId") {
|
|
913
|
+
await this.setPeerIdInternal(payload.peerId);
|
|
914
|
+
meta = { peerId: this.peerIdValue };
|
|
915
|
+
} else {
|
|
916
|
+
const neverPayload: never = payload;
|
|
917
|
+
throw new Error(`Unsupported write payload ${String(neverPayload)}`);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
const commitId = `c:${origin}:${requestId}`;
|
|
921
|
+
const commit: FlockCommit = {
|
|
922
|
+
commitId,
|
|
923
|
+
origin,
|
|
924
|
+
source: payload.source,
|
|
925
|
+
events: events.map((event) => ({
|
|
926
|
+
key: event.key.slice(),
|
|
927
|
+
clock: { ...event.clock },
|
|
928
|
+
payload: clonePayload(event.payload),
|
|
929
|
+
})),
|
|
930
|
+
...(meta ? { meta } : {}),
|
|
931
|
+
};
|
|
932
|
+
|
|
933
|
+
return { commit, result };
|
|
700
934
|
}
|
|
701
935
|
|
|
702
936
|
private static async ensureSchema(
|
|
@@ -787,9 +1021,9 @@ export class FlockSQLite {
|
|
|
787
1021
|
const maxHlc =
|
|
788
1022
|
first && Number.isFinite(first.physical) && Number.isFinite(first.logical)
|
|
789
1023
|
? {
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
1024
|
+
physicalTime: Number(first.physical),
|
|
1025
|
+
logicalCounter: Number(first.logical),
|
|
1026
|
+
}
|
|
793
1027
|
: { physicalTime: 0, logicalCounter: 0 };
|
|
794
1028
|
return { vv, maxHlc };
|
|
795
1029
|
}
|
|
@@ -818,7 +1052,7 @@ export class FlockSQLite {
|
|
|
818
1052
|
}
|
|
819
1053
|
|
|
820
1054
|
private allocateClock(now?: number): EntryClock {
|
|
821
|
-
const timestamp = now ??
|
|
1055
|
+
const timestamp = now ?? this.runtime.now();
|
|
822
1056
|
let physical = this.maxHlc.physicalTime;
|
|
823
1057
|
let logical = this.maxHlc.logicalCounter;
|
|
824
1058
|
if (timestamp > physical) {
|
|
@@ -935,58 +1169,21 @@ export class FlockSQLite {
|
|
|
935
1169
|
if (usedClock) {
|
|
936
1170
|
this.bumpVersion(usedClock);
|
|
937
1171
|
}
|
|
938
|
-
if (applied) {
|
|
939
|
-
const eventPayload = {
|
|
1172
|
+
if (applied && usedClock) {
|
|
1173
|
+
const eventPayload: PendingEvent = {
|
|
940
1174
|
key: operation.key.slice(),
|
|
941
1175
|
payload,
|
|
942
1176
|
source: operation.source,
|
|
1177
|
+
clock: usedClock,
|
|
943
1178
|
};
|
|
944
|
-
|
|
945
|
-
// Explicit event sink provided (e.g., import)
|
|
946
|
-
operation.eventSink.push(eventPayload);
|
|
947
|
-
} else if (this.txnEventSink) {
|
|
948
|
-
// In transaction: accumulate events
|
|
949
|
-
this.txnEventSink.push(eventPayload);
|
|
950
|
-
} else if (this.debounceState) {
|
|
951
|
-
// Debounce active: accumulate and reset timer
|
|
952
|
-
this.debounceState.pendingEvents.push(eventPayload);
|
|
953
|
-
this.resetDebounceTimer();
|
|
954
|
-
} else {
|
|
955
|
-
// Normal: emit immediately
|
|
956
|
-
this.emitEvents(operation.source, [eventPayload]);
|
|
957
|
-
}
|
|
1179
|
+
operation.eventSink.push(eventPayload);
|
|
958
1180
|
}
|
|
959
1181
|
return applied;
|
|
960
1182
|
}
|
|
961
1183
|
|
|
962
|
-
private resetDebounceTimer(): void {
|
|
963
|
-
if (this.debounceState === undefined) {
|
|
964
|
-
return;
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
if (this.debounceState.timerId !== undefined) {
|
|
968
|
-
clearTimeout(this.debounceState.timerId);
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
this.debounceState.timerId = setTimeout(() => {
|
|
972
|
-
this.commit();
|
|
973
|
-
}, this.debounceState.timeout);
|
|
974
|
-
|
|
975
|
-
// Start max debounce timer on first pending event
|
|
976
|
-
// Note: this is called after the event is pushed, so length === 1 means first event
|
|
977
|
-
if (
|
|
978
|
-
this.debounceState.maxTimerId === undefined &&
|
|
979
|
-
this.debounceState.pendingEvents.length === 1
|
|
980
|
-
) {
|
|
981
|
-
this.debounceState.maxTimerId = setTimeout(() => {
|
|
982
|
-
this.commit();
|
|
983
|
-
}, this.debounceState.maxDebounceTime);
|
|
984
|
-
}
|
|
985
|
-
}
|
|
986
|
-
|
|
987
1184
|
private emitEvents(
|
|
988
1185
|
source: string,
|
|
989
|
-
events:
|
|
1186
|
+
events: PendingEvent[],
|
|
990
1187
|
): void {
|
|
991
1188
|
if (this.listeners.size === 0 || events.length === 0) {
|
|
992
1189
|
return;
|
|
@@ -996,6 +1193,7 @@ export class FlockSQLite {
|
|
|
996
1193
|
events: events.map(
|
|
997
1194
|
(event): Event => ({
|
|
998
1195
|
key: cloneJson(event.key),
|
|
1196
|
+
clock: { ...event.clock },
|
|
999
1197
|
value:
|
|
1000
1198
|
event.payload.data !== undefined
|
|
1001
1199
|
? cloneJson(event.payload.data)
|
|
@@ -1005,18 +1203,24 @@ export class FlockSQLite {
|
|
|
1005
1203
|
}),
|
|
1006
1204
|
),
|
|
1007
1205
|
};
|
|
1008
|
-
this.listeners
|
|
1009
|
-
|
|
1010
|
-
|
|
1206
|
+
const listeners = Array.from(this.listeners);
|
|
1207
|
+
for (const listener of listeners) {
|
|
1208
|
+
try {
|
|
1209
|
+
listener(batch);
|
|
1210
|
+
} catch (error) {
|
|
1211
|
+
console.error(error);
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1011
1214
|
}
|
|
1012
1215
|
|
|
1013
1216
|
async put(key: KeyPart[], value: Value, now?: number): Promise<void> {
|
|
1014
|
-
await this.
|
|
1217
|
+
await this.dispatchWriteRequest<void>({
|
|
1218
|
+
kind: "apply",
|
|
1219
|
+
source: "local",
|
|
1015
1220
|
key,
|
|
1016
1221
|
payload: { data: cloneJson(value) },
|
|
1017
1222
|
now,
|
|
1018
1223
|
skipSameValue: true,
|
|
1019
|
-
source: "local",
|
|
1020
1224
|
});
|
|
1021
1225
|
}
|
|
1022
1226
|
|
|
@@ -1040,31 +1244,34 @@ export class FlockSQLite {
|
|
|
1040
1244
|
if (finalPayload.data === undefined) {
|
|
1041
1245
|
throw new TypeError("putWithMeta requires a data value");
|
|
1042
1246
|
}
|
|
1043
|
-
await this.
|
|
1247
|
+
await this.dispatchWriteRequest<void>({
|
|
1248
|
+
kind: "apply",
|
|
1249
|
+
source: "local",
|
|
1044
1250
|
key,
|
|
1045
1251
|
payload: finalPayload,
|
|
1046
1252
|
now: options.now,
|
|
1047
1253
|
skipSameValue: true,
|
|
1048
|
-
source: "local",
|
|
1049
1254
|
});
|
|
1050
1255
|
return;
|
|
1051
1256
|
}
|
|
1052
|
-
await this.
|
|
1257
|
+
await this.dispatchWriteRequest<void>({
|
|
1258
|
+
kind: "apply",
|
|
1259
|
+
source: "local",
|
|
1053
1260
|
key,
|
|
1054
1261
|
payload: basePayload,
|
|
1055
1262
|
now: options.now,
|
|
1056
1263
|
skipSameValue: true,
|
|
1057
|
-
source: "local",
|
|
1058
1264
|
});
|
|
1059
1265
|
}
|
|
1060
1266
|
|
|
1061
1267
|
async delete(key: KeyPart[], now?: number): Promise<void> {
|
|
1062
|
-
await this.
|
|
1268
|
+
await this.dispatchWriteRequest<void>({
|
|
1269
|
+
kind: "apply",
|
|
1270
|
+
source: "local",
|
|
1063
1271
|
key,
|
|
1064
1272
|
payload: {},
|
|
1065
1273
|
now,
|
|
1066
1274
|
skipSameValue: true,
|
|
1067
|
-
source: "local",
|
|
1068
1275
|
});
|
|
1069
1276
|
}
|
|
1070
1277
|
|
|
@@ -1073,12 +1280,13 @@ export class FlockSQLite {
|
|
|
1073
1280
|
* This will refresh the timestamp.
|
|
1074
1281
|
*/
|
|
1075
1282
|
async forcePut(key: KeyPart[], value: Value, now?: number): Promise<void> {
|
|
1076
|
-
await this.
|
|
1283
|
+
await this.dispatchWriteRequest<void>({
|
|
1284
|
+
kind: "apply",
|
|
1285
|
+
source: "local",
|
|
1077
1286
|
key,
|
|
1078
1287
|
payload: { data: cloneJson(value) },
|
|
1079
1288
|
now,
|
|
1080
1289
|
skipSameValue: false,
|
|
1081
|
-
source: "local",
|
|
1082
1290
|
});
|
|
1083
1291
|
}
|
|
1084
1292
|
|
|
@@ -1106,21 +1314,23 @@ export class FlockSQLite {
|
|
|
1106
1314
|
if (finalPayload.data === undefined) {
|
|
1107
1315
|
throw new TypeError("forcePutWithMeta requires a data value");
|
|
1108
1316
|
}
|
|
1109
|
-
await this.
|
|
1317
|
+
await this.dispatchWriteRequest<void>({
|
|
1318
|
+
kind: "apply",
|
|
1319
|
+
source: "local",
|
|
1110
1320
|
key,
|
|
1111
1321
|
payload: finalPayload,
|
|
1112
1322
|
now: options.now,
|
|
1113
1323
|
skipSameValue: false,
|
|
1114
|
-
source: "local",
|
|
1115
1324
|
});
|
|
1116
1325
|
return;
|
|
1117
1326
|
}
|
|
1118
|
-
await this.
|
|
1327
|
+
await this.dispatchWriteRequest<void>({
|
|
1328
|
+
kind: "apply",
|
|
1329
|
+
source: "local",
|
|
1119
1330
|
key,
|
|
1120
1331
|
payload: basePayload,
|
|
1121
1332
|
now: options.now,
|
|
1122
1333
|
skipSameValue: false,
|
|
1123
|
-
source: "local",
|
|
1124
1334
|
});
|
|
1125
1335
|
}
|
|
1126
1336
|
|
|
@@ -1129,12 +1339,13 @@ export class FlockSQLite {
|
|
|
1129
1339
|
* This will refresh the timestamp.
|
|
1130
1340
|
*/
|
|
1131
1341
|
async forceDelete(key: KeyPart[], now?: number): Promise<void> {
|
|
1132
|
-
await this.
|
|
1342
|
+
await this.dispatchWriteRequest<void>({
|
|
1343
|
+
kind: "apply",
|
|
1344
|
+
source: "local",
|
|
1133
1345
|
key,
|
|
1134
1346
|
payload: {},
|
|
1135
1347
|
now,
|
|
1136
1348
|
skipSameValue: false,
|
|
1137
|
-
source: "local",
|
|
1138
1349
|
});
|
|
1139
1350
|
}
|
|
1140
1351
|
|
|
@@ -1143,6 +1354,15 @@ export class FlockSQLite {
|
|
|
1143
1354
|
}
|
|
1144
1355
|
|
|
1145
1356
|
async setPeerId(peerId: string): Promise<void> {
|
|
1357
|
+
const normalized = normalizePeerId(peerId);
|
|
1358
|
+
await this.dispatchWriteRequest<void>({
|
|
1359
|
+
kind: "setPeerId",
|
|
1360
|
+
source: "meta",
|
|
1361
|
+
peerId: normalized,
|
|
1362
|
+
});
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
private async setPeerIdInternal(peerId: string): Promise<void> {
|
|
1146
1366
|
const normalized = normalizePeerId(peerId);
|
|
1147
1367
|
await this.db.exec(`DELETE FROM ${this.tables.meta}`);
|
|
1148
1368
|
await this.db.run(`INSERT INTO ${this.tables.meta}(peer_id) VALUES (?)`, [
|
|
@@ -1209,15 +1429,49 @@ export class FlockSQLite {
|
|
|
1209
1429
|
if (value === null || typeof value === "object") {
|
|
1210
1430
|
throw new TypeError("putMvr only accepts scalar values");
|
|
1211
1431
|
}
|
|
1432
|
+
await this.dispatchWriteRequest<void>({
|
|
1433
|
+
kind: "putMvr",
|
|
1434
|
+
source: "local",
|
|
1435
|
+
key,
|
|
1436
|
+
value,
|
|
1437
|
+
now,
|
|
1438
|
+
});
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
private async putMvrInternal(
|
|
1442
|
+
key: KeyPart[],
|
|
1443
|
+
value: Value,
|
|
1444
|
+
now: number | undefined,
|
|
1445
|
+
eventSink: PendingEvent[],
|
|
1446
|
+
): Promise<void> {
|
|
1447
|
+
if (value === null || typeof value === "object") {
|
|
1448
|
+
throw new TypeError("putMvr only accepts scalar values");
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1212
1451
|
const existing = await this.scan({ prefix: key });
|
|
1213
1452
|
for (const row of existing) {
|
|
1214
1453
|
if (row.raw.d === true) {
|
|
1215
|
-
await this.
|
|
1454
|
+
await this.applyOperation({
|
|
1455
|
+
key: row.key,
|
|
1456
|
+
payload: {},
|
|
1457
|
+
now,
|
|
1458
|
+
skipSameValue: true,
|
|
1459
|
+
source: "local",
|
|
1460
|
+
eventSink,
|
|
1461
|
+
});
|
|
1216
1462
|
}
|
|
1217
1463
|
}
|
|
1464
|
+
|
|
1218
1465
|
const composite = key.slice();
|
|
1219
1466
|
composite.push(value);
|
|
1220
|
-
await this.
|
|
1467
|
+
await this.applyOperation({
|
|
1468
|
+
key: composite,
|
|
1469
|
+
payload: { data: true },
|
|
1470
|
+
now,
|
|
1471
|
+
skipSameValue: true,
|
|
1472
|
+
source: "local",
|
|
1473
|
+
eventSink,
|
|
1474
|
+
});
|
|
1221
1475
|
}
|
|
1222
1476
|
|
|
1223
1477
|
private buildScanBounds(options: ScanOptions): {
|
|
@@ -1306,9 +1560,9 @@ export class FlockSQLite {
|
|
|
1306
1560
|
}
|
|
1307
1561
|
const postFilter = prefixFilter
|
|
1308
1562
|
? (
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1563
|
+
(pf: Uint8Array) => (bytes: Uint8Array) =>
|
|
1564
|
+
keyMatchesPrefix(bytes, pf)
|
|
1565
|
+
)(prefixFilter)
|
|
1312
1566
|
: undefined;
|
|
1313
1567
|
return { where, params, postFilter };
|
|
1314
1568
|
}
|
|
@@ -1540,34 +1794,15 @@ export class FlockSQLite {
|
|
|
1540
1794
|
return this.exportInternal(arg, pruneTombstonesBefore);
|
|
1541
1795
|
}
|
|
1542
1796
|
|
|
1543
|
-
private async
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
this.txnEventSink = undefined;
|
|
1548
|
-
if (pending.length > 0) {
|
|
1549
|
-
this.emitEvents("local", pending);
|
|
1550
|
-
}
|
|
1551
|
-
throw new Error(
|
|
1552
|
-
"import called during transaction - transaction was auto-committed",
|
|
1553
|
-
);
|
|
1554
|
-
}
|
|
1555
|
-
|
|
1556
|
-
// Force commit if in debounce mode
|
|
1557
|
-
if (this.debounceState !== undefined) {
|
|
1558
|
-
this.commit();
|
|
1559
|
-
}
|
|
1560
|
-
|
|
1797
|
+
private async importBundleInternal(
|
|
1798
|
+
bundle: ExportBundle,
|
|
1799
|
+
eventSink: PendingEvent[],
|
|
1800
|
+
): Promise<ImportReport> {
|
|
1561
1801
|
if (bundle.version !== 0) {
|
|
1562
1802
|
throw new TypeError("Unsupported bundle version");
|
|
1563
1803
|
}
|
|
1564
1804
|
let accepted = 0;
|
|
1565
1805
|
const skipped: Array<{ key: KeyPart[]; reason: string }> = [];
|
|
1566
|
-
const appliedEvents: Array<{
|
|
1567
|
-
key: KeyPart[];
|
|
1568
|
-
payload: ExportPayload;
|
|
1569
|
-
source: string;
|
|
1570
|
-
}> = [];
|
|
1571
1806
|
for (const [keyString, record] of Object.entries(bundle.entries)) {
|
|
1572
1807
|
let keyParts: KeyPart[];
|
|
1573
1808
|
try {
|
|
@@ -1589,12 +1824,9 @@ export class FlockSQLite {
|
|
|
1589
1824
|
clock,
|
|
1590
1825
|
skipSameValue: false,
|
|
1591
1826
|
source: "import",
|
|
1592
|
-
eventSink
|
|
1827
|
+
eventSink,
|
|
1593
1828
|
});
|
|
1594
1829
|
}
|
|
1595
|
-
if (appliedEvents.length > 0) {
|
|
1596
|
-
this.emitEvents("import", appliedEvents);
|
|
1597
|
-
}
|
|
1598
1830
|
return { accepted, skipped };
|
|
1599
1831
|
}
|
|
1600
1832
|
|
|
@@ -1633,13 +1865,24 @@ export class FlockSQLite {
|
|
|
1633
1865
|
}
|
|
1634
1866
|
}
|
|
1635
1867
|
}
|
|
1636
|
-
|
|
1868
|
+
this.eventBatcher.beforeImport();
|
|
1869
|
+
const { result: baseReport } = await this.dispatchWriteRequest<ImportReport>({
|
|
1870
|
+
kind: "import",
|
|
1871
|
+
source: "import",
|
|
1872
|
+
bundle: working,
|
|
1873
|
+
});
|
|
1637
1874
|
return {
|
|
1638
1875
|
accepted: baseReport.accepted,
|
|
1639
1876
|
skipped: skipped.concat(baseReport.skipped),
|
|
1640
1877
|
};
|
|
1641
1878
|
}
|
|
1642
|
-
|
|
1879
|
+
this.eventBatcher.beforeImport();
|
|
1880
|
+
const { result } = await this.dispatchWriteRequest<ImportReport>({
|
|
1881
|
+
kind: "import",
|
|
1882
|
+
source: "import",
|
|
1883
|
+
bundle: arg,
|
|
1884
|
+
});
|
|
1885
|
+
return result;
|
|
1643
1886
|
}
|
|
1644
1887
|
|
|
1645
1888
|
async importJsonStr(json: string): Promise<ImportReport> {
|
|
@@ -1708,39 +1951,14 @@ export class FlockSQLite {
|
|
|
1708
1951
|
* ```
|
|
1709
1952
|
*/
|
|
1710
1953
|
async txn<T>(callback: () => Promise<T>): Promise<T> {
|
|
1711
|
-
|
|
1712
|
-
throw new Error("Nested transactions are not supported");
|
|
1713
|
-
}
|
|
1714
|
-
if (this.debounceState !== undefined) {
|
|
1715
|
-
throw new Error(
|
|
1716
|
-
"Cannot start transaction while autoDebounceCommit is active",
|
|
1717
|
-
);
|
|
1718
|
-
}
|
|
1719
|
-
|
|
1720
|
-
const eventSink: Array<{
|
|
1721
|
-
key: KeyPart[];
|
|
1722
|
-
payload: ExportPayload;
|
|
1723
|
-
source: string;
|
|
1724
|
-
}> = [];
|
|
1725
|
-
this.txnEventSink = eventSink;
|
|
1726
|
-
|
|
1727
|
-
try {
|
|
1728
|
-
const result = await callback();
|
|
1729
|
-
// Commit: emit all accumulated events as single batch
|
|
1730
|
-
if (eventSink.length > 0) {
|
|
1731
|
-
this.emitEvents("local", eventSink);
|
|
1732
|
-
}
|
|
1733
|
-
return result;
|
|
1734
|
-
} finally {
|
|
1735
|
-
this.txnEventSink = undefined;
|
|
1736
|
-
}
|
|
1954
|
+
return this.eventBatcher.txn(callback);
|
|
1737
1955
|
}
|
|
1738
1956
|
|
|
1739
1957
|
/**
|
|
1740
1958
|
* Check if a transaction is currently active.
|
|
1741
1959
|
*/
|
|
1742
1960
|
isInTxn(): boolean {
|
|
1743
|
-
return this.
|
|
1961
|
+
return this.eventBatcher.isInTxn();
|
|
1744
1962
|
}
|
|
1745
1963
|
|
|
1746
1964
|
/**
|
|
@@ -1771,24 +1989,7 @@ export class FlockSQLite {
|
|
|
1771
1989
|
timeout: number,
|
|
1772
1990
|
options?: { maxDebounceTime?: number },
|
|
1773
1991
|
): void {
|
|
1774
|
-
|
|
1775
|
-
throw new Error(
|
|
1776
|
-
"Cannot enable autoDebounceCommit while transaction is active",
|
|
1777
|
-
);
|
|
1778
|
-
}
|
|
1779
|
-
if (this.debounceState !== undefined) {
|
|
1780
|
-
throw new Error("autoDebounceCommit is already active");
|
|
1781
|
-
}
|
|
1782
|
-
|
|
1783
|
-
const maxDebounceTime = options?.maxDebounceTime ?? 10000;
|
|
1784
|
-
|
|
1785
|
-
this.debounceState = {
|
|
1786
|
-
timeout,
|
|
1787
|
-
maxDebounceTime,
|
|
1788
|
-
timerId: undefined,
|
|
1789
|
-
maxTimerId: undefined,
|
|
1790
|
-
pendingEvents: [],
|
|
1791
|
-
};
|
|
1992
|
+
this.eventBatcher.autoDebounceCommit(timeout, options);
|
|
1792
1993
|
}
|
|
1793
1994
|
|
|
1794
1995
|
/**
|
|
@@ -1796,22 +1997,7 @@ export class FlockSQLite {
|
|
|
1796
1997
|
* No-op if autoDebounceCommit is not active.
|
|
1797
1998
|
*/
|
|
1798
1999
|
disableAutoDebounceCommit(): void {
|
|
1799
|
-
|
|
1800
|
-
return;
|
|
1801
|
-
}
|
|
1802
|
-
|
|
1803
|
-
const { timerId, maxTimerId, pendingEvents } = this.debounceState;
|
|
1804
|
-
if (timerId !== undefined) {
|
|
1805
|
-
clearTimeout(timerId);
|
|
1806
|
-
}
|
|
1807
|
-
if (maxTimerId !== undefined) {
|
|
1808
|
-
clearTimeout(maxTimerId);
|
|
1809
|
-
}
|
|
1810
|
-
this.debounceState = undefined;
|
|
1811
|
-
|
|
1812
|
-
if (pendingEvents.length > 0) {
|
|
1813
|
-
this.emitEvents("local", pendingEvents);
|
|
1814
|
-
}
|
|
2000
|
+
this.eventBatcher.disableAutoDebounceCommit();
|
|
1815
2001
|
}
|
|
1816
2002
|
|
|
1817
2003
|
/**
|
|
@@ -1820,37 +2006,21 @@ export class FlockSQLite {
|
|
|
1820
2006
|
* No-op if autoDebounceCommit is not active or no events are pending.
|
|
1821
2007
|
*/
|
|
1822
2008
|
commit(): void {
|
|
1823
|
-
|
|
1824
|
-
return;
|
|
1825
|
-
}
|
|
1826
|
-
|
|
1827
|
-
const { timerId, maxTimerId, pendingEvents } = this.debounceState;
|
|
1828
|
-
if (timerId !== undefined) {
|
|
1829
|
-
clearTimeout(timerId);
|
|
1830
|
-
this.debounceState.timerId = undefined;
|
|
1831
|
-
}
|
|
1832
|
-
if (maxTimerId !== undefined) {
|
|
1833
|
-
clearTimeout(maxTimerId);
|
|
1834
|
-
this.debounceState.maxTimerId = undefined;
|
|
1835
|
-
}
|
|
1836
|
-
|
|
1837
|
-
if (pendingEvents.length > 0) {
|
|
1838
|
-
this.emitEvents("local", pendingEvents);
|
|
1839
|
-
this.debounceState.pendingEvents = [];
|
|
1840
|
-
}
|
|
2009
|
+
this.eventBatcher.commit();
|
|
1841
2010
|
}
|
|
1842
2011
|
|
|
1843
2012
|
/**
|
|
1844
2013
|
* Check if auto-debounce mode is currently active.
|
|
1845
2014
|
*/
|
|
1846
2015
|
isAutoDebounceActive(): boolean {
|
|
1847
|
-
return this.
|
|
2016
|
+
return this.eventBatcher.isAutoDebounceActive();
|
|
1848
2017
|
}
|
|
1849
2018
|
}
|
|
1850
2019
|
|
|
1851
2020
|
export type {
|
|
1852
2021
|
Event,
|
|
1853
2022
|
EventBatch,
|
|
2023
|
+
FlockSQLiteRole,
|
|
1854
2024
|
ExportBundle,
|
|
1855
2025
|
ExportHooks,
|
|
1856
2026
|
ExportOptions,
|
|
@@ -1872,4 +2042,11 @@ export type {
|
|
|
1872
2042
|
};
|
|
1873
2043
|
|
|
1874
2044
|
export { FlockSQLite as Flock };
|
|
1875
|
-
export type { EventListener };
|
|
2045
|
+
export type { EventListener, RoleChangeListener };
|
|
2046
|
+
export type {
|
|
2047
|
+
FlockRoleProvider,
|
|
2048
|
+
FlockRuntime,
|
|
2049
|
+
FlockTransport,
|
|
2050
|
+
FlockTransportFactory,
|
|
2051
|
+
TimeoutHandle,
|
|
2052
|
+
} from "./multi-tab-env";
|