@loro-dev/flock-sqlite 0.7.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 +4 -4
- 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/lru.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export class LruMap<K, V> {
|
|
2
|
+
private readonly map = new Map<K, V>();
|
|
3
|
+
|
|
4
|
+
constructor(private readonly maxSize: number) {
|
|
5
|
+
if (!Number.isFinite(maxSize) || maxSize <= 0) {
|
|
6
|
+
throw new Error(`LruMap maxSize must be a positive number, got ${maxSize}`);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
get size(): number {
|
|
11
|
+
return this.map.size;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
has(key: K): boolean {
|
|
15
|
+
return this.map.has(key);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
get(key: K): V | undefined {
|
|
19
|
+
if (!this.map.has(key)) {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
const value = this.map.get(key) as V;
|
|
23
|
+
// Refresh recency.
|
|
24
|
+
this.map.delete(key);
|
|
25
|
+
this.map.set(key, value);
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
set(key: K, value: V): void {
|
|
30
|
+
if (this.map.has(key)) {
|
|
31
|
+
this.map.delete(key);
|
|
32
|
+
}
|
|
33
|
+
this.map.set(key, value);
|
|
34
|
+
this.evictIfNeeded();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
delete(key: K): boolean {
|
|
38
|
+
return this.map.delete(key);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
clear(): void {
|
|
42
|
+
this.map.clear();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private evictIfNeeded(): void {
|
|
46
|
+
while (this.map.size > this.maxSize) {
|
|
47
|
+
const oldest = this.map.keys().next().value as K | undefined;
|
|
48
|
+
if (oldest === undefined) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
this.map.delete(oldest);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export class LruSet<T> {
|
|
57
|
+
private readonly map: LruMap<T, true>;
|
|
58
|
+
|
|
59
|
+
constructor(maxSize: number) {
|
|
60
|
+
this.map = new LruMap<T, true>(maxSize);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
has(value: T): boolean {
|
|
64
|
+
return this.map.get(value) !== undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
add(value: T): void {
|
|
68
|
+
this.map.set(value, true);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
clear(): void {
|
|
72
|
+
this.map.clear();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { FlockSQLiteRole } from "./types";
|
|
2
|
+
|
|
3
|
+
export type FlockTransport = {
|
|
4
|
+
postMessage(message: unknown): void;
|
|
5
|
+
subscribe(onMessage: (message: unknown) => void): () => void;
|
|
6
|
+
close?: () => void;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type FlockTransportFactory = (name: string) => FlockTransport | undefined;
|
|
10
|
+
|
|
11
|
+
export type FlockRoleProvider = {
|
|
12
|
+
getRole(): FlockSQLiteRole;
|
|
13
|
+
subscribeRoleChange?: (listener: (role: FlockSQLiteRole) => void) => () => void;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type TimeoutHandle = unknown;
|
|
17
|
+
|
|
18
|
+
export type FlockRuntime = {
|
|
19
|
+
now(): number;
|
|
20
|
+
setTimeout(fn: () => void, ms: number): TimeoutHandle;
|
|
21
|
+
clearTimeout(handle: TimeoutHandle): void;
|
|
22
|
+
randomUUID(): string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function createDefaultRuntime(): FlockRuntime {
|
|
26
|
+
return {
|
|
27
|
+
now: () => Date.now(),
|
|
28
|
+
setTimeout: (fn, ms) => setTimeout(fn, ms) as unknown,
|
|
29
|
+
clearTimeout: (handle) => clearTimeout(handle as any),
|
|
30
|
+
randomUUID: () => {
|
|
31
|
+
try {
|
|
32
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
33
|
+
return crypto.randomUUID();
|
|
34
|
+
}
|
|
35
|
+
} catch {
|
|
36
|
+
// ignore
|
|
37
|
+
}
|
|
38
|
+
return `${Date.now()}_${Math.random().toString(16).slice(2)}`;
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function createBroadcastChannelTransport(name: string): FlockTransport | undefined {
|
|
44
|
+
if (typeof BroadcastChannel === "undefined") {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const channel = new BroadcastChannel(name);
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
postMessage: (message) => {
|
|
53
|
+
try {
|
|
54
|
+
channel.postMessage(message);
|
|
55
|
+
} catch {
|
|
56
|
+
// Swallow transport errors to avoid breaking the CRDT/sync pipeline.
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
subscribe: (onMessage) => {
|
|
60
|
+
const handler = (event: MessageEvent) => {
|
|
61
|
+
onMessage(event.data);
|
|
62
|
+
};
|
|
63
|
+
channel.addEventListener("message", handler);
|
|
64
|
+
return () => {
|
|
65
|
+
channel.removeEventListener("message", handler);
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
close: () => {
|
|
69
|
+
try {
|
|
70
|
+
channel.close();
|
|
71
|
+
} catch {
|
|
72
|
+
// ignore
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
} catch {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
}
|
package/src/multi-tab.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { EntryClock, ExportBundle, ExportPayload, KeyPart, Value } from "./types";
|
|
2
|
+
|
|
3
|
+
export type TabId = string;
|
|
4
|
+
export type RequestId = string;
|
|
5
|
+
|
|
6
|
+
export type FlockCommit = {
|
|
7
|
+
commitId: string;
|
|
8
|
+
origin: TabId;
|
|
9
|
+
source: string;
|
|
10
|
+
events: Array<{
|
|
11
|
+
key: KeyPart[];
|
|
12
|
+
clock: EntryClock;
|
|
13
|
+
payload: ExportPayload;
|
|
14
|
+
}>;
|
|
15
|
+
/** Optional meta updates that consumers should apply. */
|
|
16
|
+
meta?: {
|
|
17
|
+
peerId?: string;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type FlockWriteRequest =
|
|
22
|
+
| {
|
|
23
|
+
kind: "apply";
|
|
24
|
+
source: "local";
|
|
25
|
+
key: KeyPart[];
|
|
26
|
+
payload: ExportPayload;
|
|
27
|
+
now?: number;
|
|
28
|
+
skipSameValue: boolean;
|
|
29
|
+
}
|
|
30
|
+
| {
|
|
31
|
+
kind: "putMvr";
|
|
32
|
+
source: "local";
|
|
33
|
+
key: KeyPart[];
|
|
34
|
+
value: Value;
|
|
35
|
+
now?: number;
|
|
36
|
+
}
|
|
37
|
+
| {
|
|
38
|
+
kind: "import";
|
|
39
|
+
source: "import";
|
|
40
|
+
bundle: ExportBundle;
|
|
41
|
+
}
|
|
42
|
+
| {
|
|
43
|
+
kind: "setPeerId";
|
|
44
|
+
source: "meta";
|
|
45
|
+
peerId: string;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type FlockRpcRequest = {
|
|
49
|
+
t: "req";
|
|
50
|
+
from: TabId;
|
|
51
|
+
id: RequestId;
|
|
52
|
+
payload: FlockWriteRequest;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export type FlockRpcResponse = {
|
|
56
|
+
t: "res";
|
|
57
|
+
to: TabId;
|
|
58
|
+
id: RequestId;
|
|
59
|
+
ok: boolean;
|
|
60
|
+
commit?: FlockCommit;
|
|
61
|
+
result?: unknown;
|
|
62
|
+
error?: { name: string; message: string; stack?: string };
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export type FlockCommitMessage = {
|
|
66
|
+
t: "commit";
|
|
67
|
+
commit: FlockCommit;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export type FlockChannelMessage = FlockRpcRequest | FlockRpcResponse | FlockCommitMessage;
|
|
71
|
+
|
|
72
|
+
export function isFlockChannelMessage(message: unknown): message is FlockChannelMessage {
|
|
73
|
+
if (!message || typeof message !== "object") {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
const candidate = message as { t?: unknown };
|
|
77
|
+
return candidate.t === "req" || candidate.t === "res" || candidate.t === "commit";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function createRequestId(): RequestId {
|
|
81
|
+
try {
|
|
82
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
83
|
+
return crypto.randomUUID();
|
|
84
|
+
}
|
|
85
|
+
} catch {
|
|
86
|
+
// ignore
|
|
87
|
+
}
|
|
88
|
+
return `req_${Date.now()}_${Math.random().toString(16).slice(2)}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function createTabId(): TabId {
|
|
92
|
+
try {
|
|
93
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
94
|
+
return crypto.randomUUID();
|
|
95
|
+
}
|
|
96
|
+
} catch {
|
|
97
|
+
// ignore
|
|
98
|
+
}
|
|
99
|
+
return `tab_${Date.now()}_${Math.random().toString(16).slice(2)}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function serializeError(error: unknown): { name: string; message: string; stack?: string } {
|
|
103
|
+
if (error instanceof Error) {
|
|
104
|
+
return { name: error.name, message: error.message, stack: error.stack };
|
|
105
|
+
}
|
|
106
|
+
return { name: "Error", message: String(error) };
|
|
107
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -101,6 +101,7 @@ export type EventPayload = ExportPayload;
|
|
|
101
101
|
|
|
102
102
|
export type Event = {
|
|
103
103
|
key: KeyPart[];
|
|
104
|
+
clock: EntryClock;
|
|
104
105
|
value?: Value;
|
|
105
106
|
metadata?: MetadataMap;
|
|
106
107
|
payload: EventPayload;
|
|
@@ -111,6 +112,8 @@ export type EventBatch = {
|
|
|
111
112
|
events: Event[];
|
|
112
113
|
};
|
|
113
114
|
|
|
115
|
+
export type FlockSQLiteRole = "host" | "participant" | "unknown";
|
|
116
|
+
|
|
114
117
|
export type ExportOptions = {
|
|
115
118
|
from?: VersionVector;
|
|
116
119
|
hooks?: ExportHooks;
|
|
@@ -144,3 +147,5 @@ export type PutWithMetaOptions = {
|
|
|
144
147
|
};
|
|
145
148
|
|
|
146
149
|
export type EventListener = (batch: EventBatch) => void;
|
|
150
|
+
|
|
151
|
+
export type RoleChangeListener = (role: FlockSQLiteRole) => void;
|