@loro-dev/flock 4.3.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,11 +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>;
858
+ private listeners: Set<(batch: EventBatch) => void> = new Set();
859
+ private nativeUnsubscribe: (() => void) | undefined;
860
+ private readonly eventBatcher: EventBatcher<Event>;
847
861
 
848
862
  constructor(peerId?: string) {
849
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
+ });
850
870
  }
851
871
 
852
872
  private static fromInner(inner: ReturnType<typeof newFlock>): Flock {
@@ -1167,14 +1187,105 @@ export class Flock {
1167
1187
  }));
1168
1188
  }
1169
1189
 
1190
+ private ensureNativeSubscription(): void {
1191
+ if (this.nativeUnsubscribe !== undefined) {
1192
+ return;
1193
+ }
1194
+ this.nativeUnsubscribe = subscribe_ffi(this.inner, (payload: unknown) => {
1195
+ const batch = decodeEventBatch(payload);
1196
+ this.handleBatch(batch);
1197
+ }) as () => void;
1198
+ }
1199
+
1200
+ private handleBatch(batch: EventBatch): void {
1201
+ const bufferable = batch.source === "local";
1202
+ this.eventBatcher.handleCommitEvents(batch.source, batch.events, bufferable);
1203
+ }
1204
+
1205
+ private deliverBatch(batch: EventBatch): void {
1206
+ if (this.listeners.size === 0) {
1207
+ return;
1208
+ }
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
+ }
1216
+ }
1217
+ }
1218
+
1170
1219
  subscribe(listener: (batch: EventBatch) => void): () => void {
1171
- const unsubscribe = subscribe_ffi(this.inner, (payload: unknown) => {
1172
- listener(decodeEventBatch(payload));
1173
- });
1174
- if (typeof unsubscribe !== "function") {
1175
- throw new TypeError("subscribe ffi did not return a function");
1220
+ this.listeners.add(listener);
1221
+ this.ensureNativeSubscription();
1222
+
1223
+ return () => {
1224
+ this.listeners.delete(listener);
1225
+ // Optionally clean up native subscription when no listeners remain
1226
+ if (this.listeners.size === 0 && this.nativeUnsubscribe !== undefined) {
1227
+ this.nativeUnsubscribe();
1228
+ this.nativeUnsubscribe = undefined;
1229
+ }
1230
+ };
1231
+ }
1232
+
1233
+ /**
1234
+ * Enable auto-debounce mode. Events will be accumulated and emitted after
1235
+ * the specified timeout of inactivity. Each new operation resets the timer.
1236
+ *
1237
+ * Use `commit()` to force immediate emission of pending events.
1238
+ * Use `disableAutoDebounceCommit()` to disable and emit pending events.
1239
+ *
1240
+ * @param timeout - Debounce timeout in milliseconds
1241
+ * @param options - Optional configuration object with maxDebounceTime (default: 10000ms)
1242
+ * @throws Error if called while a transaction is active
1243
+ * @throws Error if autoDebounceCommit is already active
1244
+ *
1245
+ * @example
1246
+ * ```ts
1247
+ * flock.autoDebounceCommit(100);
1248
+ * flock.put(["a"], 1);
1249
+ * flock.put(["b"], 2);
1250
+ * // No events emitted yet...
1251
+ * // After 100ms of inactivity, subscribers receive single EventBatch
1252
+ * // If operations keep coming, commit happens after maxDebounceTime (10s default)
1253
+ * ```
1254
+ */
1255
+ autoDebounceCommit(
1256
+ timeout: number,
1257
+ options?: { maxDebounceTime?: number },
1258
+ ): void {
1259
+ if (this.isInTxn()) {
1260
+ throw new Error(
1261
+ "Cannot enable autoDebounceCommit while transaction is active",
1262
+ );
1176
1263
  }
1177
- return unsubscribe as () => void;
1264
+ this.eventBatcher.autoDebounceCommit(timeout, options);
1265
+ }
1266
+
1267
+ /**
1268
+ * Disable auto-debounce mode and emit any pending events immediately.
1269
+ * No-op if autoDebounceCommit is not active.
1270
+ */
1271
+ disableAutoDebounceCommit(): void {
1272
+ this.eventBatcher.disableAutoDebounceCommit();
1273
+ }
1274
+
1275
+ /**
1276
+ * Force immediate emission of any pending debounced events.
1277
+ * Does not disable auto-debounce mode - new operations will continue to be debounced.
1278
+ * No-op if autoDebounceCommit is not active or no events are pending.
1279
+ */
1280
+ commit(): void {
1281
+ this.eventBatcher.commit();
1282
+ }
1283
+
1284
+ /**
1285
+ * Check if auto-debounce mode is currently active.
1286
+ */
1287
+ isAutoDebounceActive(): boolean {
1288
+ return this.eventBatcher.isAutoDebounceActive();
1178
1289
  }
1179
1290
 
1180
1291
  /**
@@ -1192,6 +1303,7 @@ export class Flock {
1192
1303
  * @returns The return value of the callback
1193
1304
  * @throws Error if nested transaction attempted
1194
1305
  * @throws Error if import is called during the transaction (auto-commits first)
1306
+ * @throws Error if called while autoDebounceCommit is active
1195
1307
  *
1196
1308
  * @example
1197
1309
  * ```ts
@@ -1204,6 +1316,11 @@ export class Flock {
1204
1316
  * ```
1205
1317
  */
1206
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
+ }
1207
1324
  txn_begin_ffi(this.inner);
1208
1325
  try {
1209
1326
  const result = callback();