@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loro-dev/flock-sqlite",
3
- "version": "0.9.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,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.0",
66
+ "@loro-dev/flock": "4.4.1",
67
67
  "@loro-dev/unisqlite": "0.6.0"
68
68
  },
69
69
  "scripts": {
@@ -1,22 +1,31 @@
1
- import type { FlockRuntime, TimeoutHandle } from "./multi-tab-env";
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
- maxTimerId: TimeoutHandle;
19
+ deadlineAt: number | undefined;
11
20
  pendingEvents: TEvent[];
12
21
  }
13
22
  | undefined;
14
23
 
15
- private readonly runtime: FlockRuntime;
24
+ private readonly runtime: EventBatcherRuntime;
16
25
  private readonly emit: (source: string, events: TEvent[]) => void;
17
26
 
18
27
  constructor(options: {
19
- runtime: FlockRuntime;
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.debounceState.pendingEvents.push(...events);
43
- this.resetDebounceTimer();
51
+ this.bufferLocalEvents(events);
44
52
  return;
45
53
  }
46
- this.emit(source, events);
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.emit("local", eventSink);
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
- maxTimerId: undefined,
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 { timerId, maxTimerId, pendingEvents } = this.debounceState;
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
- if (pendingEvents.length > 0) {
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
- this.emit("local", pending);
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 resetDebounceTimer(): void {
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
- if (this.debounceState.timerId !== undefined) {
173
- this.runtime.clearTimeout(this.debounceState.timerId);
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
- this.debounceState.timerId = this.runtime.setTimeout(() => {
177
- this.commit();
178
- }, this.debounceState.timeout);
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
- if (
181
- this.debounceState.maxTimerId === undefined &&
182
- this.debounceState.pendingEvents.length === 1
183
- ) {
184
- this.debounceState.maxTimerId = this.runtime.setTimeout(() => {
185
- this.commit();
186
- }, this.debounceState.maxDebounceTime);
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
- subscribeRoleChange.call(db, (role) => {
705
- const normalized = normalizeRole(role);
706
- if (!normalized) {
707
- return;
708
- }
709
- listener(normalized);
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
- physicalTime: Number(first.physical),
1025
- logicalCounter: Number(first.logical),
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.forEach((listener) => {
1207
- listener(batch);
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
- (pf: Uint8Array) => (bytes: Uint8Array) =>
1559
- keyMatchesPrefix(bytes, pf)
1560
- )(prefixFilter)
1563
+ (pf: Uint8Array) => (bytes: Uint8Array) =>
1564
+ keyMatchesPrefix(bytes, pf)
1565
+ )(prefixFilter)
1561
1566
  : undefined;
1562
1567
  return { where, params, postFilter };
1563
1568
  }