@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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loro-dev/flock-sqlite",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "SQLite-backed Flock CRDT replica for Node, browsers, and Cloudflare Workers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -63,8 +63,8 @@
|
|
|
63
63
|
"tsdown": "^0.15.4",
|
|
64
64
|
"typescript": "^5.9.2",
|
|
65
65
|
"vitest": "^3.2.4",
|
|
66
|
-
"@loro-dev/flock": "4.4.
|
|
67
|
-
"@loro-dev/unisqlite": "0.
|
|
66
|
+
"@loro-dev/flock": "4.4.1",
|
|
67
|
+
"@loro-dev/unisqlite": "0.6.0"
|
|
68
68
|
},
|
|
69
69
|
"scripts": {
|
|
70
70
|
"build": "tsdown",
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
export type TimeoutHandle = unknown;
|
|
2
|
+
|
|
3
|
+
export type EventBatcherRuntime = {
|
|
4
|
+
now(): number;
|
|
5
|
+
setTimeout(fn: () => void, ms: number): TimeoutHandle;
|
|
6
|
+
clearTimeout(handle: TimeoutHandle): void;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export class EventBatcher<TEvent> {
|
|
10
|
+
private outbox: Array<{ source: string; events: TEvent[] }> = [];
|
|
11
|
+
private outboxCursor = 0;
|
|
12
|
+
private draining = false;
|
|
13
|
+
private txnEventSink: TEvent[] | undefined;
|
|
14
|
+
private debounceState:
|
|
15
|
+
| {
|
|
16
|
+
timeout: number;
|
|
17
|
+
maxDebounceTime: number;
|
|
18
|
+
timerId: TimeoutHandle;
|
|
19
|
+
deadlineAt: number | undefined;
|
|
20
|
+
pendingEvents: TEvent[];
|
|
21
|
+
}
|
|
22
|
+
| undefined;
|
|
23
|
+
|
|
24
|
+
private readonly runtime: EventBatcherRuntime;
|
|
25
|
+
private readonly emit: (source: string, events: TEvent[]) => void;
|
|
26
|
+
|
|
27
|
+
constructor(options: {
|
|
28
|
+
runtime: EventBatcherRuntime;
|
|
29
|
+
emit: (source: string, events: TEvent[]) => void;
|
|
30
|
+
}) {
|
|
31
|
+
this.runtime = options.runtime;
|
|
32
|
+
this.emit = options.emit;
|
|
33
|
+
this.txnEventSink = undefined;
|
|
34
|
+
this.debounceState = undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
handleCommitEvents(
|
|
38
|
+
source: string,
|
|
39
|
+
events: TEvent[],
|
|
40
|
+
bufferable: boolean,
|
|
41
|
+
): void {
|
|
42
|
+
if (events.length === 0) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (bufferable && this.txnEventSink) {
|
|
47
|
+
this.txnEventSink.push(...events);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (bufferable && this.debounceState) {
|
|
51
|
+
this.bufferLocalEvents(events);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
this.enqueueBatch({ source, events });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
beforeImport(): void {
|
|
59
|
+
if (this.debounceState !== undefined) {
|
|
60
|
+
this.commit();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async txn<T>(callback: () => Promise<T>): Promise<T> {
|
|
65
|
+
if (this.txnEventSink !== undefined) {
|
|
66
|
+
throw new Error("Nested transactions are not supported");
|
|
67
|
+
}
|
|
68
|
+
if (this.debounceState !== undefined) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
"Cannot start transaction while autoDebounceCommit is active",
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const eventSink: TEvent[] = [];
|
|
75
|
+
this.txnEventSink = eventSink;
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const result = await callback();
|
|
79
|
+
if (eventSink.length > 0) {
|
|
80
|
+
this.txnEventSink = undefined;
|
|
81
|
+
this.enqueueBatch({ source: "local", events: eventSink });
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
} finally {
|
|
85
|
+
this.txnEventSink = undefined;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
isInTxn(): boolean {
|
|
90
|
+
return this.txnEventSink !== undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
autoDebounceCommit(
|
|
94
|
+
timeout: number,
|
|
95
|
+
options?: { maxDebounceTime?: number },
|
|
96
|
+
): void {
|
|
97
|
+
if (this.txnEventSink !== undefined) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
"Cannot enable autoDebounceCommit while transaction is active",
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
if (this.debounceState !== undefined) {
|
|
103
|
+
throw new Error("autoDebounceCommit is already active");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const maxDebounceTime = options?.maxDebounceTime ?? 10000;
|
|
107
|
+
|
|
108
|
+
this.debounceState = {
|
|
109
|
+
timeout,
|
|
110
|
+
maxDebounceTime,
|
|
111
|
+
timerId: undefined,
|
|
112
|
+
deadlineAt: undefined,
|
|
113
|
+
pendingEvents: [],
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
disableAutoDebounceCommit(): void {
|
|
118
|
+
if (this.debounceState === undefined) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const eventsToEmit = this.takeDebouncedEvents();
|
|
123
|
+
this.debounceState = undefined;
|
|
124
|
+
if (eventsToEmit.length > 0) {
|
|
125
|
+
this.enqueueBatch({ source: "local", events: eventsToEmit });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
commit(): void {
|
|
130
|
+
if (this.debounceState === undefined) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
this.flushDebouncedEvents();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
isAutoDebounceActive(): boolean {
|
|
137
|
+
return this.debounceState !== undefined;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
close(): void {
|
|
141
|
+
// Commit any pending debounced events
|
|
142
|
+
if (this.debounceState !== undefined) {
|
|
143
|
+
this.disableAutoDebounceCommit();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Commit any transaction events (edge case: close during txn)
|
|
147
|
+
if (this.txnEventSink !== undefined) {
|
|
148
|
+
const pending = this.txnEventSink;
|
|
149
|
+
this.txnEventSink = undefined;
|
|
150
|
+
if (pending.length > 0) {
|
|
151
|
+
const eventsToEmit = pending.slice();
|
|
152
|
+
pending.length = 0;
|
|
153
|
+
this.enqueueBatch({ source: "local", events: eventsToEmit });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private enqueueBatch(batch: { source: string; events: TEvent[] }): void {
|
|
159
|
+
this.outbox.push(batch);
|
|
160
|
+
this.drainOutbox();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private drainOutbox(): void {
|
|
164
|
+
if (this.draining) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
this.draining = true;
|
|
168
|
+
try {
|
|
169
|
+
while (this.outboxCursor < this.outbox.length) {
|
|
170
|
+
const batch = this.outbox[this.outboxCursor];
|
|
171
|
+
this.outboxCursor += 1;
|
|
172
|
+
this.deliverBatch(batch);
|
|
173
|
+
}
|
|
174
|
+
} finally {
|
|
175
|
+
this.outbox = [];
|
|
176
|
+
this.outboxCursor = 0;
|
|
177
|
+
this.draining = false;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private deliverBatch(batch: { source: string; events: TEvent[] }): void {
|
|
182
|
+
try {
|
|
183
|
+
this.emit(batch.source, batch.events);
|
|
184
|
+
} catch (error) {
|
|
185
|
+
void error;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private bufferLocalEvents(events: TEvent[]): void {
|
|
190
|
+
if (this.debounceState === undefined) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (events.length === 0) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const state = this.debounceState;
|
|
197
|
+
const now = this.runtime.now();
|
|
198
|
+
if (state.pendingEvents.length === 0) {
|
|
199
|
+
state.deadlineAt = now + state.maxDebounceTime;
|
|
200
|
+
}
|
|
201
|
+
state.pendingEvents.push(...events);
|
|
202
|
+
this.scheduleDebounceFlush(now);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private scheduleDebounceFlush(now: number): void {
|
|
206
|
+
if (this.debounceState === undefined) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const state = this.debounceState;
|
|
210
|
+
if (state.pendingEvents.length === 0) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (state.timeout <= 0) {
|
|
214
|
+
this.flushDebouncedEvents();
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const deadlineAt = state.deadlineAt ?? now + state.maxDebounceTime;
|
|
219
|
+
state.deadlineAt = deadlineAt;
|
|
220
|
+
const dueAt = Math.min(now + state.timeout, deadlineAt);
|
|
221
|
+
|
|
222
|
+
if (dueAt <= now) {
|
|
223
|
+
this.flushDebouncedEvents();
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (state.timerId !== undefined) {
|
|
228
|
+
this.runtime.clearTimeout(state.timerId);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
state.timerId = this.runtime.setTimeout(() => {
|
|
232
|
+
this.flushDebouncedEvents();
|
|
233
|
+
}, Math.max(0, dueAt - now));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private takeDebouncedEvents(): TEvent[] {
|
|
237
|
+
if (this.debounceState === undefined) {
|
|
238
|
+
return [];
|
|
239
|
+
}
|
|
240
|
+
const state = this.debounceState;
|
|
241
|
+
|
|
242
|
+
if (state.timerId !== undefined) {
|
|
243
|
+
this.runtime.clearTimeout(state.timerId);
|
|
244
|
+
state.timerId = undefined;
|
|
245
|
+
}
|
|
246
|
+
state.deadlineAt = undefined;
|
|
247
|
+
|
|
248
|
+
if (state.pendingEvents.length === 0) {
|
|
249
|
+
return [];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const eventsToEmit = state.pendingEvents;
|
|
253
|
+
state.pendingEvents = [];
|
|
254
|
+
return eventsToEmit;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
private flushDebouncedEvents(): void {
|
|
258
|
+
const eventsToEmit = this.takeDebouncedEvents();
|
|
259
|
+
if (eventsToEmit.length === 0) {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
this.enqueueBatch({ source: "local", events: eventsToEmit });
|
|
263
|
+
}
|
|
264
|
+
}
|