@rotorsoft/act 0.14.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
@@ -55,12 +55,12 @@ var ZodEmpty = z.record(z.string(), z.never());
55
55
  var ActorSchema = z.object({
56
56
  id: z.string(),
57
57
  name: z.string()
58
- }).readonly();
58
+ }).loose().readonly();
59
59
  var TargetSchema = z.object({
60
60
  stream: z.string(),
61
61
  actor: ActorSchema,
62
62
  expectedVersion: z.number().optional()
63
- }).readonly();
63
+ }).loose().readonly();
64
64
  var CausationEventSchema = z.object({
65
65
  id: z.number(),
66
66
  name: z.string(),
@@ -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
  }
@@ -726,7 +729,7 @@ var Act = class {
726
729
  * 5. Applies events to create new state
727
730
  * 6. Commits events to the store with optimistic concurrency control
728
731
  *
729
- * @template K - Action name from registered actions
732
+ * @template TKey - Action name from registered actions
730
733
  * @param action - The name of the action to execute
731
734
  * @param target - Target specification with stream ID and actor context
732
735
  * @param payload - Action payload matching the action's schema
@@ -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;
1275
1253
  }
1276
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;
1264
+ }
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
@@ -1412,7 +1456,18 @@ function act(states = /* @__PURE__ */ new Map(), registry = {
1412
1456
  },
1413
1457
  withProjection: (proj) => {
1414
1458
  mergeProjection(proj, registry.events);
1415
- return act(states, registry, pendingProjections);
1459
+ return act(
1460
+ states,
1461
+ registry,
1462
+ pendingProjections
1463
+ );
1464
+ },
1465
+ withActor: () => {
1466
+ return act(
1467
+ states,
1468
+ registry,
1469
+ pendingProjections
1470
+ );
1416
1471
  },
1417
1472
  on: (event) => ({
1418
1473
  do: (handler, options) => {
@@ -1449,7 +1504,10 @@ function act(states = /* @__PURE__ */ new Map(), registry = {
1449
1504
  for (const proj of pendingProjections) {
1450
1505
  mergeProjection(proj, registry.events);
1451
1506
  }
1452
- return new Act(registry, states);
1507
+ return new Act(
1508
+ registry,
1509
+ states
1510
+ );
1453
1511
  },
1454
1512
  events: registry.events
1455
1513
  };
@@ -1531,7 +1589,12 @@ function slice(states = /* @__PURE__ */ new Map(), actions = {}, events = {}, pr
1531
1589
  },
1532
1590
  withProjection: (proj) => {
1533
1591
  projections.push(proj);
1534
- return slice(states, actions, events, projections);
1592
+ return slice(
1593
+ states,
1594
+ actions,
1595
+ events,
1596
+ projections
1597
+ );
1535
1598
  },
1536
1599
  on: (event) => ({
1537
1600
  do: (handler, options) => {
@@ -1627,7 +1690,10 @@ function action_builder(state2) {
1627
1690
  const schema = entry[action2];
1628
1691
  if (action2 in state2.actions)
1629
1692
  throw new Error(`Duplicate action "${action2}"`);
1630
- const actions = { ...state2.actions, [action2]: schema };
1693
+ const actions = {
1694
+ ...state2.actions,
1695
+ [action2]: schema
1696
+ };
1631
1697
  const on = { ...state2.on };
1632
1698
  const _given = { ...state2.given };
1633
1699
  function given(rules) {
@@ -1651,7 +1717,10 @@ function action_builder(state2) {
1651
1717
  return { given, emit };
1652
1718
  },
1653
1719
  snap(snap2) {
1654
- return action_builder({ ...state2, snap: snap2 });
1720
+ return action_builder({
1721
+ ...state2,
1722
+ snap: snap2
1723
+ });
1655
1724
  },
1656
1725
  build() {
1657
1726
  return state2;