@loro-dev/flock 4.4.0 → 4.4.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/src/index.ts CHANGED
@@ -29,6 +29,11 @@ import {
29
29
  is_in_txn_ffi,
30
30
  } from "./_moon_flock";
31
31
 
32
+ import {
33
+ EventBatcher,
34
+ type EventBatcherRuntime,
35
+ } from "../../packages/flock-sqlite/src/event-batcher";
36
+
32
37
  type RawVersionVector = Record<string, [number, number]>;
33
38
  type RawScanRow = { key: KeyPart[]; raw: ExportRecord; value?: Value };
34
39
  type RawEventPayload = { data?: Value; metadata?: MetadataMap };
@@ -842,23 +847,26 @@ function isImportOptions(value: unknown): value is ImportOptions {
842
847
  );
843
848
  }
844
849
 
850
+ const defaultEventBatcherRuntime: EventBatcherRuntime = {
851
+ now: () => Date.now(),
852
+ setTimeout: (fn, ms) => setTimeout(fn, ms) as unknown,
853
+ clearTimeout: (handle) => clearTimeout(handle as any),
854
+ };
855
+
845
856
  export class Flock {
846
857
  private inner: ReturnType<typeof newFlock>;
847
858
  private listeners: Set<(batch: EventBatch) => void> = new Set();
848
859
  private nativeUnsubscribe: (() => void) | undefined;
849
- /** Debounce state for autoDebounceCommit */
850
- private debounceState:
851
- | {
852
- timeout: number;
853
- maxDebounceTime: number;
854
- timerId: ReturnType<typeof setTimeout> | undefined;
855
- maxTimerId: ReturnType<typeof setTimeout> | undefined;
856
- pendingEvents: Event[];
857
- }
858
- | undefined;
860
+ private readonly eventBatcher: EventBatcher<Event>;
859
861
 
860
862
  constructor(peerId?: string) {
861
863
  this.inner = newFlock(normalizePeerId(peerId));
864
+ this.eventBatcher = new EventBatcher<Event>({
865
+ runtime: defaultEventBatcherRuntime,
866
+ emit: (source, events) => {
867
+ this.deliverBatch({ source, events });
868
+ },
869
+ });
862
870
  }
863
871
 
864
872
  private static fromInner(inner: ReturnType<typeof newFlock>): Flock {
@@ -1190,41 +1198,21 @@ export class Flock {
1190
1198
  }
1191
1199
 
1192
1200
  private handleBatch(batch: EventBatch): void {
1193
- if (this.debounceState !== undefined) {
1194
- // Debounce active: accumulate events and reset timer
1195
- const wasEmpty = this.debounceState.pendingEvents.length === 0;
1196
- this.debounceState.pendingEvents.push(...batch.events);
1197
- this.resetDebounceTimer(wasEmpty);
1198
- } else {
1199
- // Normal mode: emit immediately
1200
- this.emitBatch(batch);
1201
- }
1201
+ const bufferable = batch.source === "local";
1202
+ this.eventBatcher.handleCommitEvents(batch.source, batch.events, bufferable);
1202
1203
  }
1203
1204
 
1204
- private emitBatch(batch: EventBatch): void {
1205
- for (const listener of this.listeners) {
1206
- listener(batch);
1207
- }
1208
- }
1209
-
1210
- private resetDebounceTimer(isFirstEvent: boolean): void {
1211
- if (this.debounceState === undefined) {
1205
+ private deliverBatch(batch: EventBatch): void {
1206
+ if (this.listeners.size === 0) {
1212
1207
  return;
1213
1208
  }
1214
-
1215
- if (this.debounceState.timerId !== undefined) {
1216
- clearTimeout(this.debounceState.timerId);
1217
- }
1218
-
1219
- this.debounceState.timerId = setTimeout(() => {
1220
- this.commit();
1221
- }, this.debounceState.timeout);
1222
-
1223
- // Start max debounce timer on first pending event
1224
- if (this.debounceState.maxTimerId === undefined && isFirstEvent) {
1225
- this.debounceState.maxTimerId = setTimeout(() => {
1226
- this.commit();
1227
- }, this.debounceState.maxDebounceTime);
1209
+ const listeners = Array.from(this.listeners);
1210
+ for (const listener of listeners) {
1211
+ try {
1212
+ listener(batch);
1213
+ } catch (error) {
1214
+ void error;
1215
+ }
1228
1216
  }
1229
1217
  }
1230
1218
 
@@ -1273,19 +1261,7 @@ export class Flock {
1273
1261
  "Cannot enable autoDebounceCommit while transaction is active",
1274
1262
  );
1275
1263
  }
1276
- if (this.debounceState !== undefined) {
1277
- throw new Error("autoDebounceCommit is already active");
1278
- }
1279
-
1280
- const maxDebounceTime = options?.maxDebounceTime ?? 10000;
1281
-
1282
- this.debounceState = {
1283
- timeout,
1284
- maxDebounceTime,
1285
- timerId: undefined,
1286
- maxTimerId: undefined,
1287
- pendingEvents: [],
1288
- };
1264
+ this.eventBatcher.autoDebounceCommit(timeout, options);
1289
1265
  }
1290
1266
 
1291
1267
  /**
@@ -1293,22 +1269,7 @@ export class Flock {
1293
1269
  * No-op if autoDebounceCommit is not active.
1294
1270
  */
1295
1271
  disableAutoDebounceCommit(): void {
1296
- if (this.debounceState === undefined) {
1297
- return;
1298
- }
1299
-
1300
- const { timerId, maxTimerId, pendingEvents } = this.debounceState;
1301
- if (timerId !== undefined) {
1302
- clearTimeout(timerId);
1303
- }
1304
- if (maxTimerId !== undefined) {
1305
- clearTimeout(maxTimerId);
1306
- }
1307
- this.debounceState = undefined;
1308
-
1309
- if (pendingEvents.length > 0) {
1310
- this.emitBatch({ source: "local", events: pendingEvents });
1311
- }
1272
+ this.eventBatcher.disableAutoDebounceCommit();
1312
1273
  }
1313
1274
 
1314
1275
  /**
@@ -1317,31 +1278,14 @@ export class Flock {
1317
1278
  * No-op if autoDebounceCommit is not active or no events are pending.
1318
1279
  */
1319
1280
  commit(): void {
1320
- if (this.debounceState === undefined) {
1321
- return;
1322
- }
1323
-
1324
- const { timerId, maxTimerId, pendingEvents } = this.debounceState;
1325
- if (timerId !== undefined) {
1326
- clearTimeout(timerId);
1327
- this.debounceState.timerId = undefined;
1328
- }
1329
- if (maxTimerId !== undefined) {
1330
- clearTimeout(maxTimerId);
1331
- this.debounceState.maxTimerId = undefined;
1332
- }
1333
-
1334
- if (pendingEvents.length > 0) {
1335
- this.emitBatch({ source: "local", events: pendingEvents });
1336
- this.debounceState.pendingEvents = [];
1337
- }
1281
+ this.eventBatcher.commit();
1338
1282
  }
1339
1283
 
1340
1284
  /**
1341
1285
  * Check if auto-debounce mode is currently active.
1342
1286
  */
1343
1287
  isAutoDebounceActive(): boolean {
1344
- return this.debounceState !== undefined;
1288
+ return this.eventBatcher.isAutoDebounceActive();
1345
1289
  }
1346
1290
 
1347
1291
  /**
@@ -1359,6 +1303,7 @@ export class Flock {
1359
1303
  * @returns The return value of the callback
1360
1304
  * @throws Error if nested transaction attempted
1361
1305
  * @throws Error if import is called during the transaction (auto-commits first)
1306
+ * @throws Error if called while autoDebounceCommit is active
1362
1307
  *
1363
1308
  * @example
1364
1309
  * ```ts
@@ -1371,6 +1316,11 @@ export class Flock {
1371
1316
  * ```
1372
1317
  */
1373
1318
  txn<T>(callback: () => T): T {
1319
+ if (this.eventBatcher.isAutoDebounceActive()) {
1320
+ throw new Error(
1321
+ "Cannot start transaction while autoDebounceCommit is active",
1322
+ );
1323
+ }
1374
1324
  txn_begin_ffi(this.inner);
1375
1325
  try {
1376
1326
  const result = callback();