@loro-dev/flock 4.4.0 → 4.4.2

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 {
@@ -984,13 +992,21 @@ export class Flock {
984
992
  }
985
993
 
986
994
  merge(other: Flock): void {
995
+ this.eventBatcher.beforeImport();
987
996
  merge(this.inner, other.inner);
988
997
  }
989
998
 
990
999
  /**
991
1000
  * Returns the exclusive version vector, which only includes peers that have
992
- * at least one entry in the current state. This is consistent with the state
993
- * after export and re-import.
1001
+ * at least one entry in the current state.
1002
+ *
1003
+ * Complexity: O(M + V log M + R (log L + log S)).
1004
+ * - M = memtablePeerCount
1005
+ * - V = vvPeerCount
1006
+ * - L = memtableLen
1007
+ * - R = scanned candidate rows in KV_BY_PEER_CLOCK
1008
+ * - S = storage key count in KV_BY_KEY
1009
+ * No full O(memtableSize) pre-scan is performed.
994
1010
  *
995
1011
  * Use this version when sending to other peers for incremental sync.
996
1012
  */
@@ -1069,6 +1085,7 @@ export class Flock {
1069
1085
  }
1070
1086
 
1071
1087
  private importJsonInternal(bundle: ExportBundle): ImportReport {
1088
+ this.eventBatcher.beforeImport();
1072
1089
  const report = import_json_ffi(this.inner, bundle) as
1073
1090
  | RawImportReport
1074
1091
  | undefined;
@@ -1118,6 +1135,7 @@ export class Flock {
1118
1135
  }
1119
1136
 
1120
1137
  importJsonStr(bundle: string): ImportReport {
1138
+ this.eventBatcher.beforeImport();
1121
1139
  const report = import_json_str_ffi(this.inner, bundle) as
1122
1140
  | RawImportReport
1123
1141
  | undefined;
@@ -1190,41 +1208,21 @@ export class Flock {
1190
1208
  }
1191
1209
 
1192
1210
  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
- }
1211
+ const bufferable = batch.source === "local";
1212
+ this.eventBatcher.handleCommitEvents(batch.source, batch.events, bufferable);
1202
1213
  }
1203
1214
 
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) {
1215
+ private deliverBatch(batch: EventBatch): void {
1216
+ if (this.listeners.size === 0) {
1212
1217
  return;
1213
1218
  }
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);
1219
+ const listeners = Array.from(this.listeners);
1220
+ for (const listener of listeners) {
1221
+ try {
1222
+ listener(batch);
1223
+ } catch (error) {
1224
+ void error;
1225
+ }
1228
1226
  }
1229
1227
  }
1230
1228
 
@@ -1273,19 +1271,7 @@ export class Flock {
1273
1271
  "Cannot enable autoDebounceCommit while transaction is active",
1274
1272
  );
1275
1273
  }
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
- };
1274
+ this.eventBatcher.autoDebounceCommit(timeout, options);
1289
1275
  }
1290
1276
 
1291
1277
  /**
@@ -1293,22 +1279,7 @@ export class Flock {
1293
1279
  * No-op if autoDebounceCommit is not active.
1294
1280
  */
1295
1281
  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
- }
1282
+ this.eventBatcher.disableAutoDebounceCommit();
1312
1283
  }
1313
1284
 
1314
1285
  /**
@@ -1317,31 +1288,14 @@ export class Flock {
1317
1288
  * No-op if autoDebounceCommit is not active or no events are pending.
1318
1289
  */
1319
1290
  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
- }
1291
+ this.eventBatcher.commit();
1338
1292
  }
1339
1293
 
1340
1294
  /**
1341
1295
  * Check if auto-debounce mode is currently active.
1342
1296
  */
1343
1297
  isAutoDebounceActive(): boolean {
1344
- return this.debounceState !== undefined;
1298
+ return this.eventBatcher.isAutoDebounceActive();
1345
1299
  }
1346
1300
 
1347
1301
  /**
@@ -1359,6 +1313,7 @@ export class Flock {
1359
1313
  * @returns The return value of the callback
1360
1314
  * @throws Error if nested transaction attempted
1361
1315
  * @throws Error if import is called during the transaction (auto-commits first)
1316
+ * @throws Error if called while autoDebounceCommit is active
1362
1317
  *
1363
1318
  * @example
1364
1319
  * ```ts
@@ -1371,6 +1326,11 @@ export class Flock {
1371
1326
  * ```
1372
1327
  */
1373
1328
  txn<T>(callback: () => T): T {
1329
+ if (this.eventBatcher.isAutoDebounceActive()) {
1330
+ throw new Error(
1331
+ "Cannot start transaction while autoDebounceCommit is active",
1332
+ );
1333
+ }
1374
1334
  txn_begin_ffi(this.inner);
1375
1335
  try {
1376
1336
  const result = callback();