@loro-dev/flock-sqlite 0.9.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/dist/index.cjs +2 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +2 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/event-batcher.ts +123 -48
- package/src/index.ts +22 -17
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loro-dev/flock-sqlite",
|
|
3
|
-
"version": "0.9.
|
|
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,7 +63,7 @@
|
|
|
63
63
|
"tsdown": "^0.15.4",
|
|
64
64
|
"typescript": "^5.9.2",
|
|
65
65
|
"vitest": "^3.2.4",
|
|
66
|
-
"@loro-dev/flock": "4.4.
|
|
66
|
+
"@loro-dev/flock": "4.4.1",
|
|
67
67
|
"@loro-dev/unisqlite": "0.6.0"
|
|
68
68
|
},
|
|
69
69
|
"scripts": {
|
package/src/event-batcher.ts
CHANGED
|
@@ -1,22 +1,31 @@
|
|
|
1
|
-
|
|
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
|
+
};
|
|
2
8
|
|
|
3
9
|
export class EventBatcher<TEvent> {
|
|
10
|
+
private outbox: Array<{ source: string; events: TEvent[] }> = [];
|
|
11
|
+
private outboxCursor = 0;
|
|
12
|
+
private draining = false;
|
|
4
13
|
private txnEventSink: TEvent[] | undefined;
|
|
5
14
|
private debounceState:
|
|
6
15
|
| {
|
|
7
16
|
timeout: number;
|
|
8
17
|
maxDebounceTime: number;
|
|
9
18
|
timerId: TimeoutHandle;
|
|
10
|
-
|
|
19
|
+
deadlineAt: number | undefined;
|
|
11
20
|
pendingEvents: TEvent[];
|
|
12
21
|
}
|
|
13
22
|
| undefined;
|
|
14
23
|
|
|
15
|
-
private readonly runtime:
|
|
24
|
+
private readonly runtime: EventBatcherRuntime;
|
|
16
25
|
private readonly emit: (source: string, events: TEvent[]) => void;
|
|
17
26
|
|
|
18
27
|
constructor(options: {
|
|
19
|
-
runtime:
|
|
28
|
+
runtime: EventBatcherRuntime;
|
|
20
29
|
emit: (source: string, events: TEvent[]) => void;
|
|
21
30
|
}) {
|
|
22
31
|
this.runtime = options.runtime;
|
|
@@ -39,11 +48,11 @@ export class EventBatcher<TEvent> {
|
|
|
39
48
|
return;
|
|
40
49
|
}
|
|
41
50
|
if (bufferable && this.debounceState) {
|
|
42
|
-
this.
|
|
43
|
-
this.resetDebounceTimer();
|
|
51
|
+
this.bufferLocalEvents(events);
|
|
44
52
|
return;
|
|
45
53
|
}
|
|
46
|
-
|
|
54
|
+
|
|
55
|
+
this.enqueueBatch({ source, events });
|
|
47
56
|
}
|
|
48
57
|
|
|
49
58
|
beforeImport(): void {
|
|
@@ -68,7 +77,8 @@ export class EventBatcher<TEvent> {
|
|
|
68
77
|
try {
|
|
69
78
|
const result = await callback();
|
|
70
79
|
if (eventSink.length > 0) {
|
|
71
|
-
this.
|
|
80
|
+
this.txnEventSink = undefined;
|
|
81
|
+
this.enqueueBatch({ source: "local", events: eventSink });
|
|
72
82
|
}
|
|
73
83
|
return result;
|
|
74
84
|
} finally {
|
|
@@ -99,7 +109,7 @@ export class EventBatcher<TEvent> {
|
|
|
99
109
|
timeout,
|
|
100
110
|
maxDebounceTime,
|
|
101
111
|
timerId: undefined,
|
|
102
|
-
|
|
112
|
+
deadlineAt: undefined,
|
|
103
113
|
pendingEvents: [],
|
|
104
114
|
};
|
|
105
115
|
}
|
|
@@ -109,17 +119,10 @@ export class EventBatcher<TEvent> {
|
|
|
109
119
|
return;
|
|
110
120
|
}
|
|
111
121
|
|
|
112
|
-
const
|
|
113
|
-
if (timerId !== undefined) {
|
|
114
|
-
this.runtime.clearTimeout(timerId);
|
|
115
|
-
}
|
|
116
|
-
if (maxTimerId !== undefined) {
|
|
117
|
-
this.runtime.clearTimeout(maxTimerId);
|
|
118
|
-
}
|
|
122
|
+
const eventsToEmit = this.takeDebouncedEvents();
|
|
119
123
|
this.debounceState = undefined;
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
this.emit("local", pendingEvents);
|
|
124
|
+
if (eventsToEmit.length > 0) {
|
|
125
|
+
this.enqueueBatch({ source: "local", events: eventsToEmit });
|
|
123
126
|
}
|
|
124
127
|
}
|
|
125
128
|
|
|
@@ -127,21 +130,7 @@ export class EventBatcher<TEvent> {
|
|
|
127
130
|
if (this.debounceState === undefined) {
|
|
128
131
|
return;
|
|
129
132
|
}
|
|
130
|
-
|
|
131
|
-
const { timerId, maxTimerId, pendingEvents } = this.debounceState;
|
|
132
|
-
if (timerId !== undefined) {
|
|
133
|
-
this.runtime.clearTimeout(timerId);
|
|
134
|
-
this.debounceState.timerId = undefined;
|
|
135
|
-
}
|
|
136
|
-
if (maxTimerId !== undefined) {
|
|
137
|
-
this.runtime.clearTimeout(maxTimerId);
|
|
138
|
-
this.debounceState.maxTimerId = undefined;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (pendingEvents.length > 0) {
|
|
142
|
-
this.emit("local", pendingEvents);
|
|
143
|
-
this.debounceState.pendingEvents = [];
|
|
144
|
-
}
|
|
133
|
+
this.flushDebouncedEvents();
|
|
145
134
|
}
|
|
146
135
|
|
|
147
136
|
isAutoDebounceActive(): boolean {
|
|
@@ -159,31 +148,117 @@ export class EventBatcher<TEvent> {
|
|
|
159
148
|
const pending = this.txnEventSink;
|
|
160
149
|
this.txnEventSink = undefined;
|
|
161
150
|
if (pending.length > 0) {
|
|
162
|
-
|
|
151
|
+
const eventsToEmit = pending.slice();
|
|
152
|
+
pending.length = 0;
|
|
153
|
+
this.enqueueBatch({ source: "local", events: eventsToEmit });
|
|
163
154
|
}
|
|
164
155
|
}
|
|
165
156
|
}
|
|
166
157
|
|
|
167
|
-
private
|
|
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 {
|
|
168
190
|
if (this.debounceState === undefined) {
|
|
169
191
|
return;
|
|
170
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
|
+
}
|
|
171
204
|
|
|
172
|
-
|
|
173
|
-
|
|
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;
|
|
174
216
|
}
|
|
175
217
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
+
}
|
|
179
235
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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;
|
|
187
261
|
}
|
|
262
|
+
this.enqueueBatch({ source: "local", events: eventsToEmit });
|
|
188
263
|
}
|
|
189
264
|
}
|
package/src/index.ts
CHANGED
|
@@ -94,7 +94,7 @@ type EncodableVersionVectorEntry = {
|
|
|
94
94
|
counter: number;
|
|
95
95
|
};
|
|
96
96
|
|
|
97
|
-
export interface VersionVector extends VersionVectorType {}
|
|
97
|
+
export interface VersionVector extends VersionVectorType { }
|
|
98
98
|
|
|
99
99
|
const textEncoder = new TextEncoder();
|
|
100
100
|
const textDecoder = new TextDecoder();
|
|
@@ -701,13 +701,13 @@ function createDefaultRoleProvider(db: UniStoreConnection): FlockRoleProvider {
|
|
|
701
701
|
subscribeRoleChange:
|
|
702
702
|
typeof subscribeRoleChange === "function"
|
|
703
703
|
? (listener) =>
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
704
|
+
subscribeRoleChange.call(db, (role) => {
|
|
705
|
+
const normalized = normalizeRole(role);
|
|
706
|
+
if (!normalized) {
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
listener(normalized);
|
|
710
|
+
})
|
|
711
711
|
: undefined,
|
|
712
712
|
};
|
|
713
713
|
}
|
|
@@ -1021,9 +1021,9 @@ export class FlockSQLite {
|
|
|
1021
1021
|
const maxHlc =
|
|
1022
1022
|
first && Number.isFinite(first.physical) && Number.isFinite(first.logical)
|
|
1023
1023
|
? {
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1024
|
+
physicalTime: Number(first.physical),
|
|
1025
|
+
logicalCounter: Number(first.logical),
|
|
1026
|
+
}
|
|
1027
1027
|
: { physicalTime: 0, logicalCounter: 0 };
|
|
1028
1028
|
return { vv, maxHlc };
|
|
1029
1029
|
}
|
|
@@ -1203,9 +1203,14 @@ export class FlockSQLite {
|
|
|
1203
1203
|
}),
|
|
1204
1204
|
),
|
|
1205
1205
|
};
|
|
1206
|
-
this.listeners
|
|
1207
|
-
|
|
1208
|
-
|
|
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
|
+
}
|
|
1209
1214
|
}
|
|
1210
1215
|
|
|
1211
1216
|
async put(key: KeyPart[], value: Value, now?: number): Promise<void> {
|
|
@@ -1555,9 +1560,9 @@ export class FlockSQLite {
|
|
|
1555
1560
|
}
|
|
1556
1561
|
const postFilter = prefixFilter
|
|
1557
1562
|
? (
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1563
|
+
(pf: Uint8Array) => (bytes: Uint8Array) =>
|
|
1564
|
+
keyMatchesPrefix(bytes, pf)
|
|
1565
|
+
)(prefixFilter)
|
|
1561
1566
|
: undefined;
|
|
1562
1567
|
return { where, params, postFilter };
|
|
1563
1568
|
}
|