@rotorsoft/act 0.15.0 → 0.16.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.js CHANGED
@@ -697,13 +697,16 @@ var Act = class {
697
697
  dispose(() => {
698
698
  this._emitter.removeAllListeners();
699
699
  this.stop_correlations();
700
+ this.stop_settling();
700
701
  return Promise.resolve();
701
702
  });
702
703
  }
703
704
  _emitter = new EventEmitter();
704
705
  _drain_locked = false;
705
706
  _drain_lag2lead_ratio = 0.5;
706
- _correlation_interval = void 0;
707
+ _correlation_timer = void 0;
708
+ _settle_timer = void 0;
709
+ _settling = false;
707
710
  emit(event, args) {
708
711
  return this._emitter.emit(event, args);
709
712
  }
@@ -953,7 +956,7 @@ var Act = class {
953
956
  /**
954
957
  * Processes pending reactions by draining uncommitted events from the event store.
955
958
  *
956
- * The drain process:
959
+ * Runs a single drain cycle:
957
960
  * 1. Polls the store for streams with uncommitted events
958
961
  * 2. Leases streams to prevent concurrent processing
959
962
  * 3. Fetches events for each leased stream
@@ -963,7 +966,8 @@ var Act = class {
963
966
  * Drain uses a dual-frontier strategy to balance processing of new streams (lagging)
964
967
  * vs active streams (leading). The ratio adapts based on event pressure.
965
968
  *
966
- * Call this method periodically in a background loop, or after committing events.
969
+ * Call `correlate()` before `drain()` to discover target streams. For a higher-level
970
+ * API that handles debouncing, correlation, and signaling automatically, use {@link settle}.
967
971
  *
968
972
  * @param options - Drain configuration options
969
973
  * @param options.streamLimit - Maximum number of streams to process per cycle (default: 10)
@@ -971,46 +975,20 @@ var Act = class {
971
975
  * @param options.leaseMillis - Lease duration in milliseconds (default: 10000)
972
976
  * @returns Drain statistics with fetched, leased, acked, and blocked counts
973
977
  *
974
- * @example Basic drain loop
978
+ * @example In tests and scripts
975
979
  * ```typescript
976
- * // Process reactions after each action
977
980
  * await app.do("createUser", target, payload);
981
+ * await app.correlate();
978
982
  * await app.drain();
979
983
  * ```
980
984
  *
981
- * @example Background drain worker
985
+ * @example In production, prefer settle()
982
986
  * ```typescript
983
- * setInterval(async () => {
984
- * try {
985
- * const result = await app.drain({
986
- * streamLimit: 20,
987
- * eventLimit: 50
988
- * });
989
- * if (result.acked.length) {
990
- * console.log(`Processed ${result.acked.length} streams`);
991
- * }
992
- * } catch (error) {
993
- * console.error("Drain error:", error);
994
- * }
995
- * }, 5000); // Every 5 seconds
996
- * ```
997
- *
998
- * @example With lifecycle listeners
999
- * ```typescript
1000
- * app.on("acked", (leases) => {
1001
- * console.log(`Acknowledged ${leases.length} streams`);
1002
- * });
1003
- *
1004
- * app.on("blocked", (blocked) => {
1005
- * console.error(`Blocked ${blocked.length} streams due to errors`);
1006
- * blocked.forEach(({ stream, error }) => {
1007
- * console.error(`Stream ${stream}: ${error}`);
1008
- * });
1009
- * });
1010
- *
1011
- * await app.drain();
987
+ * await app.do("CreateItem", target, input);
988
+ * app.settle(); // debounced correlate→drain, emits "settled"
1012
989
  * ```
1013
990
  *
991
+ * @see {@link settle} for debounced correlate→drain with lifecycle events
1014
992
  * @see {@link correlate} for dynamic stream discovery
1015
993
  * @see {@link start_correlations} for automatic correlation
1016
994
  */
@@ -1239,10 +1217,10 @@ var Act = class {
1239
1217
  * @see {@link stop_correlations} to stop the worker
1240
1218
  */
1241
1219
  start_correlations(query = {}, frequency = 1e4, callback) {
1242
- if (this._correlation_interval) return false;
1220
+ if (this._correlation_timer) return false;
1243
1221
  const limit = query.limit || 100;
1244
1222
  let after = query.after || -1;
1245
- this._correlation_interval = setInterval(
1223
+ this._correlation_timer = setInterval(
1246
1224
  () => this.correlate({ ...query, after, limit }).then((result) => {
1247
1225
  after = result.last_id;
1248
1226
  if (callback && result.leased.length) callback(result.leased);
@@ -1269,11 +1247,77 @@ var Act = class {
1269
1247
  * @see {@link start_correlations}
1270
1248
  */
1271
1249
  stop_correlations() {
1272
- if (this._correlation_interval) {
1273
- clearInterval(this._correlation_interval);
1274
- this._correlation_interval = void 0;
1250
+ if (this._correlation_timer) {
1251
+ clearInterval(this._correlation_timer);
1252
+ this._correlation_timer = void 0;
1253
+ }
1254
+ }
1255
+ /**
1256
+ * Cancels any pending or active settle cycle.
1257
+ *
1258
+ * @see {@link settle}
1259
+ */
1260
+ stop_settling() {
1261
+ if (this._settle_timer) {
1262
+ clearTimeout(this._settle_timer);
1263
+ this._settle_timer = void 0;
1275
1264
  }
1276
1265
  }
1266
+ /**
1267
+ * Debounced, non-blocking correlate→drain cycle.
1268
+ *
1269
+ * Call this after `app.do()` to schedule a background drain. Multiple rapid
1270
+ * calls within the debounce window are coalesced into a single cycle. Runs
1271
+ * correlate→drain in a loop until the system reaches a consistent state,
1272
+ * then emits the `"settled"` lifecycle event.
1273
+ *
1274
+ * @param options - Settle configuration options
1275
+ * @param options.debounceMs - Debounce window in milliseconds (default: 10)
1276
+ * @param options.correlate - Query filter for correlation scans (default: `{ after: -1, limit: 100 }`)
1277
+ * @param options.maxPasses - Maximum correlate→drain loops (default: 5)
1278
+ * @param options.streamLimit - Maximum streams per drain cycle (default: 10)
1279
+ * @param options.eventLimit - Maximum events per stream (default: 10)
1280
+ * @param options.leaseMillis - Lease duration in milliseconds (default: 10000)
1281
+ *
1282
+ * @example API mutations
1283
+ * ```typescript
1284
+ * await app.do("CreateItem", target, input);
1285
+ * app.settle(); // non-blocking, returns immediately
1286
+ *
1287
+ * app.on("settled", (drain) => {
1288
+ * // notify SSE clients, invalidate caches, etc.
1289
+ * });
1290
+ * ```
1291
+ *
1292
+ * @see {@link drain} for single synchronous drain cycles
1293
+ * @see {@link correlate} for manual correlation
1294
+ */
1295
+ settle(options = {}) {
1296
+ const {
1297
+ debounceMs = 10,
1298
+ correlate: correlateQuery = { after: -1, limit: 100 },
1299
+ maxPasses = 5,
1300
+ ...drainOptions
1301
+ } = options;
1302
+ if (this._settle_timer) clearTimeout(this._settle_timer);
1303
+ this._settle_timer = setTimeout(() => {
1304
+ this._settle_timer = void 0;
1305
+ if (this._settling) return;
1306
+ this._settling = true;
1307
+ (async () => {
1308
+ let lastDrain;
1309
+ for (let i = 0; i < maxPasses; i++) {
1310
+ const { leased } = await this.correlate(correlateQuery);
1311
+ if (leased.length === 0 && i > 0) break;
1312
+ lastDrain = await this.drain(drainOptions);
1313
+ if (!lastDrain.acked.length && !lastDrain.blocked.length) break;
1314
+ }
1315
+ if (lastDrain) this.emit("settled", lastDrain);
1316
+ })().catch((err) => logger.error(err)).finally(() => {
1317
+ this._settling = false;
1318
+ });
1319
+ }, debounceMs);
1320
+ }
1277
1321
  };
1278
1322
 
1279
1323
  // src/merge.ts