@loro-dev/flock 4.3.0 → 4.4.0
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 +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +48 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +173 -6
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -844,6 +844,18 @@ function isImportOptions(value: unknown): value is ImportOptions {
|
|
|
844
844
|
|
|
845
845
|
export class Flock {
|
|
846
846
|
private inner: ReturnType<typeof newFlock>;
|
|
847
|
+
private listeners: Set<(batch: EventBatch) => void> = new Set();
|
|
848
|
+
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;
|
|
847
859
|
|
|
848
860
|
constructor(peerId?: string) {
|
|
849
861
|
this.inner = newFlock(normalizePeerId(peerId));
|
|
@@ -1167,14 +1179,169 @@ export class Flock {
|
|
|
1167
1179
|
}));
|
|
1168
1180
|
}
|
|
1169
1181
|
|
|
1182
|
+
private ensureNativeSubscription(): void {
|
|
1183
|
+
if (this.nativeUnsubscribe !== undefined) {
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
this.nativeUnsubscribe = subscribe_ffi(this.inner, (payload: unknown) => {
|
|
1187
|
+
const batch = decodeEventBatch(payload);
|
|
1188
|
+
this.handleBatch(batch);
|
|
1189
|
+
}) as () => void;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
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
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
|
|
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) {
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
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);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1170
1231
|
subscribe(listener: (batch: EventBatch) => void): () => void {
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1232
|
+
this.listeners.add(listener);
|
|
1233
|
+
this.ensureNativeSubscription();
|
|
1234
|
+
|
|
1235
|
+
return () => {
|
|
1236
|
+
this.listeners.delete(listener);
|
|
1237
|
+
// Optionally clean up native subscription when no listeners remain
|
|
1238
|
+
if (this.listeners.size === 0 && this.nativeUnsubscribe !== undefined) {
|
|
1239
|
+
this.nativeUnsubscribe();
|
|
1240
|
+
this.nativeUnsubscribe = undefined;
|
|
1241
|
+
}
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
/**
|
|
1246
|
+
* Enable auto-debounce mode. Events will be accumulated and emitted after
|
|
1247
|
+
* the specified timeout of inactivity. Each new operation resets the timer.
|
|
1248
|
+
*
|
|
1249
|
+
* Use `commit()` to force immediate emission of pending events.
|
|
1250
|
+
* Use `disableAutoDebounceCommit()` to disable and emit pending events.
|
|
1251
|
+
*
|
|
1252
|
+
* @param timeout - Debounce timeout in milliseconds
|
|
1253
|
+
* @param options - Optional configuration object with maxDebounceTime (default: 10000ms)
|
|
1254
|
+
* @throws Error if called while a transaction is active
|
|
1255
|
+
* @throws Error if autoDebounceCommit is already active
|
|
1256
|
+
*
|
|
1257
|
+
* @example
|
|
1258
|
+
* ```ts
|
|
1259
|
+
* flock.autoDebounceCommit(100);
|
|
1260
|
+
* flock.put(["a"], 1);
|
|
1261
|
+
* flock.put(["b"], 2);
|
|
1262
|
+
* // No events emitted yet...
|
|
1263
|
+
* // After 100ms of inactivity, subscribers receive single EventBatch
|
|
1264
|
+
* // If operations keep coming, commit happens after maxDebounceTime (10s default)
|
|
1265
|
+
* ```
|
|
1266
|
+
*/
|
|
1267
|
+
autoDebounceCommit(
|
|
1268
|
+
timeout: number,
|
|
1269
|
+
options?: { maxDebounceTime?: number },
|
|
1270
|
+
): void {
|
|
1271
|
+
if (this.isInTxn()) {
|
|
1272
|
+
throw new Error(
|
|
1273
|
+
"Cannot enable autoDebounceCommit while transaction is active",
|
|
1274
|
+
);
|
|
1275
|
+
}
|
|
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
|
+
};
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
/**
|
|
1292
|
+
* Disable auto-debounce mode and emit any pending events immediately.
|
|
1293
|
+
* No-op if autoDebounceCommit is not active.
|
|
1294
|
+
*/
|
|
1295
|
+
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
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
/**
|
|
1315
|
+
* Force immediate emission of any pending debounced events.
|
|
1316
|
+
* Does not disable auto-debounce mode - new operations will continue to be debounced.
|
|
1317
|
+
* No-op if autoDebounceCommit is not active or no events are pending.
|
|
1318
|
+
*/
|
|
1319
|
+
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 = [];
|
|
1176
1337
|
}
|
|
1177
|
-
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
/**
|
|
1341
|
+
* Check if auto-debounce mode is currently active.
|
|
1342
|
+
*/
|
|
1343
|
+
isAutoDebounceActive(): boolean {
|
|
1344
|
+
return this.debounceState !== undefined;
|
|
1178
1345
|
}
|
|
1179
1346
|
|
|
1180
1347
|
/**
|