@loro-dev/flock-sqlite 0.8.0 → 0.9.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 +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 +2 -2
- package/src/event-batcher.ts +189 -0
- package/src/index.ts +375 -203
- 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 = {
|
|
@@ -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
|
-
this.disableAutoDebounceCommit();
|
|
826
|
+
if (this.closed) {
|
|
827
|
+
return;
|
|
688
828
|
}
|
|
829
|
+
this.closed = true;
|
|
830
|
+
this.coordinator.close();
|
|
831
|
+
this.eventBatcher.close();
|
|
689
832
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
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;
|
|
857
|
+
}
|
|
858
|
+
this.seenCommitIds.add(commit.commitId);
|
|
859
|
+
|
|
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(
|
|
@@ -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)
|
|
@@ -1011,12 +1209,13 @@ export class FlockSQLite {
|
|
|
1011
1209
|
}
|
|
1012
1210
|
|
|
1013
1211
|
async put(key: KeyPart[], value: Value, now?: number): Promise<void> {
|
|
1014
|
-
await this.
|
|
1212
|
+
await this.dispatchWriteRequest<void>({
|
|
1213
|
+
kind: "apply",
|
|
1214
|
+
source: "local",
|
|
1015
1215
|
key,
|
|
1016
1216
|
payload: { data: cloneJson(value) },
|
|
1017
1217
|
now,
|
|
1018
1218
|
skipSameValue: true,
|
|
1019
|
-
source: "local",
|
|
1020
1219
|
});
|
|
1021
1220
|
}
|
|
1022
1221
|
|
|
@@ -1040,31 +1239,34 @@ export class FlockSQLite {
|
|
|
1040
1239
|
if (finalPayload.data === undefined) {
|
|
1041
1240
|
throw new TypeError("putWithMeta requires a data value");
|
|
1042
1241
|
}
|
|
1043
|
-
await this.
|
|
1242
|
+
await this.dispatchWriteRequest<void>({
|
|
1243
|
+
kind: "apply",
|
|
1244
|
+
source: "local",
|
|
1044
1245
|
key,
|
|
1045
1246
|
payload: finalPayload,
|
|
1046
1247
|
now: options.now,
|
|
1047
1248
|
skipSameValue: true,
|
|
1048
|
-
source: "local",
|
|
1049
1249
|
});
|
|
1050
1250
|
return;
|
|
1051
1251
|
}
|
|
1052
|
-
await this.
|
|
1252
|
+
await this.dispatchWriteRequest<void>({
|
|
1253
|
+
kind: "apply",
|
|
1254
|
+
source: "local",
|
|
1053
1255
|
key,
|
|
1054
1256
|
payload: basePayload,
|
|
1055
1257
|
now: options.now,
|
|
1056
1258
|
skipSameValue: true,
|
|
1057
|
-
source: "local",
|
|
1058
1259
|
});
|
|
1059
1260
|
}
|
|
1060
1261
|
|
|
1061
1262
|
async delete(key: KeyPart[], now?: number): Promise<void> {
|
|
1062
|
-
await this.
|
|
1263
|
+
await this.dispatchWriteRequest<void>({
|
|
1264
|
+
kind: "apply",
|
|
1265
|
+
source: "local",
|
|
1063
1266
|
key,
|
|
1064
1267
|
payload: {},
|
|
1065
1268
|
now,
|
|
1066
1269
|
skipSameValue: true,
|
|
1067
|
-
source: "local",
|
|
1068
1270
|
});
|
|
1069
1271
|
}
|
|
1070
1272
|
|
|
@@ -1073,12 +1275,13 @@ export class FlockSQLite {
|
|
|
1073
1275
|
* This will refresh the timestamp.
|
|
1074
1276
|
*/
|
|
1075
1277
|
async forcePut(key: KeyPart[], value: Value, now?: number): Promise<void> {
|
|
1076
|
-
await this.
|
|
1278
|
+
await this.dispatchWriteRequest<void>({
|
|
1279
|
+
kind: "apply",
|
|
1280
|
+
source: "local",
|
|
1077
1281
|
key,
|
|
1078
1282
|
payload: { data: cloneJson(value) },
|
|
1079
1283
|
now,
|
|
1080
1284
|
skipSameValue: false,
|
|
1081
|
-
source: "local",
|
|
1082
1285
|
});
|
|
1083
1286
|
}
|
|
1084
1287
|
|
|
@@ -1106,21 +1309,23 @@ export class FlockSQLite {
|
|
|
1106
1309
|
if (finalPayload.data === undefined) {
|
|
1107
1310
|
throw new TypeError("forcePutWithMeta requires a data value");
|
|
1108
1311
|
}
|
|
1109
|
-
await this.
|
|
1312
|
+
await this.dispatchWriteRequest<void>({
|
|
1313
|
+
kind: "apply",
|
|
1314
|
+
source: "local",
|
|
1110
1315
|
key,
|
|
1111
1316
|
payload: finalPayload,
|
|
1112
1317
|
now: options.now,
|
|
1113
1318
|
skipSameValue: false,
|
|
1114
|
-
source: "local",
|
|
1115
1319
|
});
|
|
1116
1320
|
return;
|
|
1117
1321
|
}
|
|
1118
|
-
await this.
|
|
1322
|
+
await this.dispatchWriteRequest<void>({
|
|
1323
|
+
kind: "apply",
|
|
1324
|
+
source: "local",
|
|
1119
1325
|
key,
|
|
1120
1326
|
payload: basePayload,
|
|
1121
1327
|
now: options.now,
|
|
1122
1328
|
skipSameValue: false,
|
|
1123
|
-
source: "local",
|
|
1124
1329
|
});
|
|
1125
1330
|
}
|
|
1126
1331
|
|
|
@@ -1129,12 +1334,13 @@ export class FlockSQLite {
|
|
|
1129
1334
|
* This will refresh the timestamp.
|
|
1130
1335
|
*/
|
|
1131
1336
|
async forceDelete(key: KeyPart[], now?: number): Promise<void> {
|
|
1132
|
-
await this.
|
|
1337
|
+
await this.dispatchWriteRequest<void>({
|
|
1338
|
+
kind: "apply",
|
|
1339
|
+
source: "local",
|
|
1133
1340
|
key,
|
|
1134
1341
|
payload: {},
|
|
1135
1342
|
now,
|
|
1136
1343
|
skipSameValue: false,
|
|
1137
|
-
source: "local",
|
|
1138
1344
|
});
|
|
1139
1345
|
}
|
|
1140
1346
|
|
|
@@ -1143,6 +1349,15 @@ export class FlockSQLite {
|
|
|
1143
1349
|
}
|
|
1144
1350
|
|
|
1145
1351
|
async setPeerId(peerId: string): Promise<void> {
|
|
1352
|
+
const normalized = normalizePeerId(peerId);
|
|
1353
|
+
await this.dispatchWriteRequest<void>({
|
|
1354
|
+
kind: "setPeerId",
|
|
1355
|
+
source: "meta",
|
|
1356
|
+
peerId: normalized,
|
|
1357
|
+
});
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
private async setPeerIdInternal(peerId: string): Promise<void> {
|
|
1146
1361
|
const normalized = normalizePeerId(peerId);
|
|
1147
1362
|
await this.db.exec(`DELETE FROM ${this.tables.meta}`);
|
|
1148
1363
|
await this.db.run(`INSERT INTO ${this.tables.meta}(peer_id) VALUES (?)`, [
|
|
@@ -1209,15 +1424,49 @@ export class FlockSQLite {
|
|
|
1209
1424
|
if (value === null || typeof value === "object") {
|
|
1210
1425
|
throw new TypeError("putMvr only accepts scalar values");
|
|
1211
1426
|
}
|
|
1427
|
+
await this.dispatchWriteRequest<void>({
|
|
1428
|
+
kind: "putMvr",
|
|
1429
|
+
source: "local",
|
|
1430
|
+
key,
|
|
1431
|
+
value,
|
|
1432
|
+
now,
|
|
1433
|
+
});
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
private async putMvrInternal(
|
|
1437
|
+
key: KeyPart[],
|
|
1438
|
+
value: Value,
|
|
1439
|
+
now: number | undefined,
|
|
1440
|
+
eventSink: PendingEvent[],
|
|
1441
|
+
): Promise<void> {
|
|
1442
|
+
if (value === null || typeof value === "object") {
|
|
1443
|
+
throw new TypeError("putMvr only accepts scalar values");
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1212
1446
|
const existing = await this.scan({ prefix: key });
|
|
1213
1447
|
for (const row of existing) {
|
|
1214
1448
|
if (row.raw.d === true) {
|
|
1215
|
-
await this.
|
|
1449
|
+
await this.applyOperation({
|
|
1450
|
+
key: row.key,
|
|
1451
|
+
payload: {},
|
|
1452
|
+
now,
|
|
1453
|
+
skipSameValue: true,
|
|
1454
|
+
source: "local",
|
|
1455
|
+
eventSink,
|
|
1456
|
+
});
|
|
1216
1457
|
}
|
|
1217
1458
|
}
|
|
1459
|
+
|
|
1218
1460
|
const composite = key.slice();
|
|
1219
1461
|
composite.push(value);
|
|
1220
|
-
await this.
|
|
1462
|
+
await this.applyOperation({
|
|
1463
|
+
key: composite,
|
|
1464
|
+
payload: { data: true },
|
|
1465
|
+
now,
|
|
1466
|
+
skipSameValue: true,
|
|
1467
|
+
source: "local",
|
|
1468
|
+
eventSink,
|
|
1469
|
+
});
|
|
1221
1470
|
}
|
|
1222
1471
|
|
|
1223
1472
|
private buildScanBounds(options: ScanOptions): {
|
|
@@ -1540,34 +1789,15 @@ export class FlockSQLite {
|
|
|
1540
1789
|
return this.exportInternal(arg, pruneTombstonesBefore);
|
|
1541
1790
|
}
|
|
1542
1791
|
|
|
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
|
-
|
|
1792
|
+
private async importBundleInternal(
|
|
1793
|
+
bundle: ExportBundle,
|
|
1794
|
+
eventSink: PendingEvent[],
|
|
1795
|
+
): Promise<ImportReport> {
|
|
1561
1796
|
if (bundle.version !== 0) {
|
|
1562
1797
|
throw new TypeError("Unsupported bundle version");
|
|
1563
1798
|
}
|
|
1564
1799
|
let accepted = 0;
|
|
1565
1800
|
const skipped: Array<{ key: KeyPart[]; reason: string }> = [];
|
|
1566
|
-
const appliedEvents: Array<{
|
|
1567
|
-
key: KeyPart[];
|
|
1568
|
-
payload: ExportPayload;
|
|
1569
|
-
source: string;
|
|
1570
|
-
}> = [];
|
|
1571
1801
|
for (const [keyString, record] of Object.entries(bundle.entries)) {
|
|
1572
1802
|
let keyParts: KeyPart[];
|
|
1573
1803
|
try {
|
|
@@ -1589,12 +1819,9 @@ export class FlockSQLite {
|
|
|
1589
1819
|
clock,
|
|
1590
1820
|
skipSameValue: false,
|
|
1591
1821
|
source: "import",
|
|
1592
|
-
eventSink
|
|
1822
|
+
eventSink,
|
|
1593
1823
|
});
|
|
1594
1824
|
}
|
|
1595
|
-
if (appliedEvents.length > 0) {
|
|
1596
|
-
this.emitEvents("import", appliedEvents);
|
|
1597
|
-
}
|
|
1598
1825
|
return { accepted, skipped };
|
|
1599
1826
|
}
|
|
1600
1827
|
|
|
@@ -1633,13 +1860,24 @@ export class FlockSQLite {
|
|
|
1633
1860
|
}
|
|
1634
1861
|
}
|
|
1635
1862
|
}
|
|
1636
|
-
|
|
1863
|
+
this.eventBatcher.beforeImport();
|
|
1864
|
+
const { result: baseReport } = await this.dispatchWriteRequest<ImportReport>({
|
|
1865
|
+
kind: "import",
|
|
1866
|
+
source: "import",
|
|
1867
|
+
bundle: working,
|
|
1868
|
+
});
|
|
1637
1869
|
return {
|
|
1638
1870
|
accepted: baseReport.accepted,
|
|
1639
1871
|
skipped: skipped.concat(baseReport.skipped),
|
|
1640
1872
|
};
|
|
1641
1873
|
}
|
|
1642
|
-
|
|
1874
|
+
this.eventBatcher.beforeImport();
|
|
1875
|
+
const { result } = await this.dispatchWriteRequest<ImportReport>({
|
|
1876
|
+
kind: "import",
|
|
1877
|
+
source: "import",
|
|
1878
|
+
bundle: arg,
|
|
1879
|
+
});
|
|
1880
|
+
return result;
|
|
1643
1881
|
}
|
|
1644
1882
|
|
|
1645
1883
|
async importJsonStr(json: string): Promise<ImportReport> {
|
|
@@ -1708,39 +1946,14 @@ export class FlockSQLite {
|
|
|
1708
1946
|
* ```
|
|
1709
1947
|
*/
|
|
1710
1948
|
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
|
-
}
|
|
1949
|
+
return this.eventBatcher.txn(callback);
|
|
1737
1950
|
}
|
|
1738
1951
|
|
|
1739
1952
|
/**
|
|
1740
1953
|
* Check if a transaction is currently active.
|
|
1741
1954
|
*/
|
|
1742
1955
|
isInTxn(): boolean {
|
|
1743
|
-
return this.
|
|
1956
|
+
return this.eventBatcher.isInTxn();
|
|
1744
1957
|
}
|
|
1745
1958
|
|
|
1746
1959
|
/**
|
|
@@ -1771,24 +1984,7 @@ export class FlockSQLite {
|
|
|
1771
1984
|
timeout: number,
|
|
1772
1985
|
options?: { maxDebounceTime?: number },
|
|
1773
1986
|
): 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
|
-
};
|
|
1987
|
+
this.eventBatcher.autoDebounceCommit(timeout, options);
|
|
1792
1988
|
}
|
|
1793
1989
|
|
|
1794
1990
|
/**
|
|
@@ -1796,22 +1992,7 @@ export class FlockSQLite {
|
|
|
1796
1992
|
* No-op if autoDebounceCommit is not active.
|
|
1797
1993
|
*/
|
|
1798
1994
|
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
|
-
}
|
|
1995
|
+
this.eventBatcher.disableAutoDebounceCommit();
|
|
1815
1996
|
}
|
|
1816
1997
|
|
|
1817
1998
|
/**
|
|
@@ -1820,37 +2001,21 @@ export class FlockSQLite {
|
|
|
1820
2001
|
* No-op if autoDebounceCommit is not active or no events are pending.
|
|
1821
2002
|
*/
|
|
1822
2003
|
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
|
-
}
|
|
2004
|
+
this.eventBatcher.commit();
|
|
1841
2005
|
}
|
|
1842
2006
|
|
|
1843
2007
|
/**
|
|
1844
2008
|
* Check if auto-debounce mode is currently active.
|
|
1845
2009
|
*/
|
|
1846
2010
|
isAutoDebounceActive(): boolean {
|
|
1847
|
-
return this.
|
|
2011
|
+
return this.eventBatcher.isAutoDebounceActive();
|
|
1848
2012
|
}
|
|
1849
2013
|
}
|
|
1850
2014
|
|
|
1851
2015
|
export type {
|
|
1852
2016
|
Event,
|
|
1853
2017
|
EventBatch,
|
|
2018
|
+
FlockSQLiteRole,
|
|
1854
2019
|
ExportBundle,
|
|
1855
2020
|
ExportHooks,
|
|
1856
2021
|
ExportOptions,
|
|
@@ -1872,4 +2037,11 @@ export type {
|
|
|
1872
2037
|
};
|
|
1873
2038
|
|
|
1874
2039
|
export { FlockSQLite as Flock };
|
|
1875
|
-
export type { EventListener };
|
|
2040
|
+
export type { EventListener, RoleChangeListener };
|
|
2041
|
+
export type {
|
|
2042
|
+
FlockRoleProvider,
|
|
2043
|
+
FlockRuntime,
|
|
2044
|
+
FlockTransport,
|
|
2045
|
+
FlockTransportFactory,
|
|
2046
|
+
TimeoutHandle,
|
|
2047
|
+
} from "./multi-tab-env";
|