@rotorsoft/act 0.6.27 → 0.6.29

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 CHANGED
@@ -779,18 +779,87 @@ var Act = class {
779
779
  return this;
780
780
  }
781
781
  /**
782
- * Executes an action (command) against a state machine, emitting and committing the resulting event(s).
782
+ * Executes an action on a state instance, committing resulting events.
783
783
  *
784
- * @template K The type of action to execute
785
- * @param action The action name (key of the action schema)
786
- * @param target The target (stream and actor) for the action
787
- * @param payload The action payload (validated against the schema)
788
- * @param reactingTo (Optional) The event this action is reacting to
789
- * @param skipValidation (Optional) If true, skips schema validation (not recommended)
790
- * @returns The snapshot of the committed event
784
+ * This is the primary method for modifying state. It:
785
+ * 1. Validates the action payload against the schema
786
+ * 2. Loads the current state snapshot
787
+ * 3. Checks invariants (business rules)
788
+ * 4. Executes the action handler to generate events
789
+ * 5. Applies events to create new state
790
+ * 6. Commits events to the store with optimistic concurrency control
791
791
  *
792
- * @example
793
- * await app.do("increment", { stream: "counter1", actor }, { by: 1 });
792
+ * @template K - Action name from registered actions
793
+ * @param action - The name of the action to execute
794
+ * @param target - Target specification with stream ID and actor context
795
+ * @param payload - Action payload matching the action's schema
796
+ * @param reactingTo - Optional event that triggered this action (for correlation)
797
+ * @param skipValidation - Skip schema validation (use carefully, for performance)
798
+ * @returns Array of snapshots for all affected states (usually one)
799
+ *
800
+ * @throws {ValidationError} If payload doesn't match action schema
801
+ * @throws {InvariantError} If business rules are violated
802
+ * @throws {ConcurrencyError} If another process modified the stream
803
+ *
804
+ * @example Basic action execution
805
+ * ```typescript
806
+ * const snapshots = await app.do(
807
+ * "increment",
808
+ * {
809
+ * stream: "counter-1",
810
+ * actor: { id: "user1", name: "Alice" }
811
+ * },
812
+ * { by: 5 }
813
+ * );
814
+ *
815
+ * console.log(snapshots[0].state.count); // Current count after increment
816
+ * ```
817
+ *
818
+ * @example With error handling
819
+ * ```typescript
820
+ * try {
821
+ * await app.do(
822
+ * "withdraw",
823
+ * { stream: "account-123", actor: { id: "user1", name: "Alice" } },
824
+ * { amount: 1000 }
825
+ * );
826
+ * } catch (error) {
827
+ * if (error instanceof InvariantError) {
828
+ * console.error("Business rule violated:", error.description);
829
+ * } else if (error instanceof ConcurrencyError) {
830
+ * console.error("Concurrent modification detected, retry...");
831
+ * } else if (error instanceof ValidationError) {
832
+ * console.error("Invalid payload:", error.details);
833
+ * }
834
+ * }
835
+ * ```
836
+ *
837
+ * @example Reaction triggering another action
838
+ * ```typescript
839
+ * const app = act()
840
+ * .with(Order)
841
+ * .with(Inventory)
842
+ * .on("OrderPlaced")
843
+ * .do(async (event, context) => {
844
+ * // This action is triggered by an event
845
+ * const result = await context.app.do(
846
+ * "reduceStock",
847
+ * {
848
+ * stream: "inventory-1",
849
+ * actor: event.meta.causation.action.actor
850
+ * },
851
+ * { amount: event.data.items.length },
852
+ * event // Pass event for correlation tracking
853
+ * );
854
+ * return result;
855
+ * })
856
+ * .to("inventory-1")
857
+ * .build();
858
+ * ```
859
+ *
860
+ * @see {@link Target} for target structure
861
+ * @see {@link Snapshot} for return value structure
862
+ * @see {@link ValidationError}, {@link InvariantError}, {@link ConcurrencyError}
794
863
  */
795
864
  async do(action2, target, payload, reactingTo, skipValidation = false) {
796
865
  const snapshots = await action(
@@ -806,31 +875,97 @@ var Act = class {
806
875
  return snapshots;
807
876
  }
808
877
  /**
809
- * Loads the current state snapshot for a given state machine and stream.
878
+ * Loads the current state snapshot for a specific stream.
810
879
  *
811
- * @template SX The type of state
812
- * @template EX The type of events
813
- * @template AX The type of actions
814
- * @param state The state machine definition
815
- * @param stream The stream (instance) to load
816
- * @param callback (Optional) Callback to receive the loaded snapshot
817
- * @returns The snapshot of the loaded state
880
+ * Reconstructs the current state by replaying events from the event store.
881
+ * Uses snapshots when available to optimize loading performance.
818
882
  *
819
- * @example
820
- * const snapshot = await app.load(Counter, "counter1");
883
+ * @template SX - State schema type
884
+ * @template EX - Event schemas type
885
+ * @template AX - Action schemas type
886
+ * @param state - The state definition to load
887
+ * @param stream - The stream ID (state instance identifier)
888
+ * @param callback - Optional callback invoked with the loaded snapshot
889
+ * @returns The current state snapshot for the stream
890
+ *
891
+ * @example Load current state
892
+ * ```typescript
893
+ * const snapshot = await app.load(Counter, "counter-1");
894
+ * console.log(snapshot.state.count); // Current count
895
+ * console.log(snapshot.version); // Number of events applied
896
+ * console.log(snapshot.patches); // Events since last snapshot
897
+ * ```
898
+ *
899
+ * @example With callback
900
+ * ```typescript
901
+ * const snapshot = await app.load(User, "user-123", (snap) => {
902
+ * console.log("Loaded user:", snap.state.name);
903
+ * });
904
+ * ```
905
+ *
906
+ * @example Load multiple states
907
+ * ```typescript
908
+ * const [user, account] = await Promise.all([
909
+ * app.load(User, "user-123"),
910
+ * app.load(BankAccount, "account-456")
911
+ * ]);
912
+ * ```
913
+ *
914
+ * @see {@link Snapshot} for snapshot structure
821
915
  */
822
916
  async load(state2, stream, callback) {
823
917
  return await load(state2, stream, callback);
824
918
  }
825
919
  /**
826
- * Query the event store for events matching a filter.
920
+ * Queries the event store for events matching a filter.
827
921
  *
828
- * @param query The query filter (e.g., by stream, event name, or time range)
829
- * @param callback (Optional) Callback for each event found
830
- * @returns An object with the first and last event found, and the total count
922
+ * Use this for analyzing event streams, generating reports, or debugging.
923
+ * The callback is invoked for each matching event, and the method returns
924
+ * summary information (first event, last event, total count).
831
925
  *
832
- * @example
833
- * const { count } = await app.query({ stream: "counter1" }, (event) => console.log(event));
926
+ * For small result sets, consider using {@link query_array} instead.
927
+ *
928
+ * @param query - The query filter
929
+ * @param query.stream - Filter by stream ID
930
+ * @param query.name - Filter by event name
931
+ * @param query.after - Filter events after this event ID
932
+ * @param query.before - Filter events before this event ID
933
+ * @param query.created_after - Filter events after this timestamp
934
+ * @param query.created_before - Filter events before this timestamp
935
+ * @param query.limit - Maximum number of events to return
936
+ * @param callback - Optional callback invoked for each matching event
937
+ * @returns Object with first event, last event, and total count
938
+ *
939
+ * @example Query all events for a stream
940
+ * ```typescript
941
+ * const { first, last, count } = await app.query(
942
+ * { stream: "counter-1" },
943
+ * (event) => console.log(event.name, event.data)
944
+ * );
945
+ * console.log(`Found ${count} events from ${first?.id} to ${last?.id}`);
946
+ * ```
947
+ *
948
+ * @example Query specific event types
949
+ * ```typescript
950
+ * const { count } = await app.query(
951
+ * { name: "UserCreated", limit: 100 },
952
+ * (event) => {
953
+ * console.log("User created:", event.data.email);
954
+ * }
955
+ * );
956
+ * ```
957
+ *
958
+ * @example Query events in time range
959
+ * ```typescript
960
+ * const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000);
961
+ * const { count } = await app.query({
962
+ * created_after: yesterday,
963
+ * stream: "user-123"
964
+ * });
965
+ * console.log(`User had ${count} events in last 24 hours`);
966
+ * ```
967
+ *
968
+ * @see {@link query_array} for loading events into memory
834
969
  */
835
970
  async query(query, callback) {
836
971
  let first = void 0, last = void 0;
@@ -842,14 +977,30 @@ var Act = class {
842
977
  return { first, last, count };
843
978
  }
844
979
  /**
845
- * Query the event store for events matching a filter.
846
- * Use this version with caution, as it return events in memory.
980
+ * Queries the event store and returns all matching events in memory.
847
981
  *
848
- * @param query The query filter (e.g., by stream, event name, or time range)
849
- * @returns The matching events
982
+ * **Use with caution** - this loads all results into memory. For large result sets,
983
+ * use {@link query} with a callback instead to process events incrementally.
850
984
  *
851
- * @example
852
- * const { count } = await app.query({ stream: "counter1" }, (event) => console.log(event));
985
+ * @param query - The query filter (same as {@link query})
986
+ * @returns Array of all matching events
987
+ *
988
+ * @example Load all events for a stream
989
+ * ```typescript
990
+ * const events = await app.query_array({ stream: "counter-1" });
991
+ * console.log(`Loaded ${events.length} events`);
992
+ * events.forEach(event => console.log(event.name, event.data));
993
+ * ```
994
+ *
995
+ * @example Get recent events
996
+ * ```typescript
997
+ * const recent = await app.query_array({
998
+ * stream: "user-123",
999
+ * limit: 10
1000
+ * });
1001
+ * ```
1002
+ *
1003
+ * @see {@link query} for large result sets
853
1004
  */
854
1005
  async query_array(query) {
855
1006
  const events = [];
@@ -895,14 +1046,68 @@ var Act = class {
895
1046
  return { lease, handled, at };
896
1047
  }
897
1048
  /**
898
- * Drains and processes events from the store, triggering reactions and updating state.
1049
+ * Processes pending reactions by draining uncommitted events from the event store.
899
1050
  *
900
- * This is typically called in a background loop or after committing new events.
1051
+ * The drain process:
1052
+ * 1. Polls the store for streams with uncommitted events
1053
+ * 2. Leases streams to prevent concurrent processing
1054
+ * 3. Fetches events for each leased stream
1055
+ * 4. Executes matching reaction handlers
1056
+ * 5. Acknowledges successful reactions or blocks failing ones
901
1057
  *
902
- * @returns The number of events drained and processed
1058
+ * Drain uses a dual-frontier strategy to balance processing of new streams (lagging)
1059
+ * vs active streams (leading). The ratio adapts based on event pressure.
903
1060
  *
904
- * @example
1061
+ * Call this method periodically in a background loop, or after committing events.
1062
+ *
1063
+ * @param options - Drain configuration options
1064
+ * @param options.streamLimit - Maximum number of streams to process per cycle (default: 10)
1065
+ * @param options.eventLimit - Maximum events to fetch per stream (default: 10)
1066
+ * @param options.leaseMillis - Lease duration in milliseconds (default: 10000)
1067
+ * @returns Drain statistics with fetched, leased, acked, and blocked counts
1068
+ *
1069
+ * @example Basic drain loop
1070
+ * ```typescript
1071
+ * // Process reactions after each action
1072
+ * await app.do("createUser", target, payload);
905
1073
  * await app.drain();
1074
+ * ```
1075
+ *
1076
+ * @example Background drain worker
1077
+ * ```typescript
1078
+ * setInterval(async () => {
1079
+ * try {
1080
+ * const result = await app.drain({
1081
+ * streamLimit: 20,
1082
+ * eventLimit: 50
1083
+ * });
1084
+ * if (result.acked.length) {
1085
+ * console.log(`Processed ${result.acked.length} streams`);
1086
+ * }
1087
+ * } catch (error) {
1088
+ * console.error("Drain error:", error);
1089
+ * }
1090
+ * }, 5000); // Every 5 seconds
1091
+ * ```
1092
+ *
1093
+ * @example With lifecycle listeners
1094
+ * ```typescript
1095
+ * app.on("acked", (leases) => {
1096
+ * console.log(`Acknowledged ${leases.length} streams`);
1097
+ * });
1098
+ *
1099
+ * app.on("blocked", (blocked) => {
1100
+ * console.error(`Blocked ${blocked.length} streams due to errors`);
1101
+ * blocked.forEach(({ stream, error }) => {
1102
+ * console.error(`Stream ${stream}: ${error}`);
1103
+ * });
1104
+ * });
1105
+ *
1106
+ * await app.drain();
1107
+ * ```
1108
+ *
1109
+ * @see {@link correlate} for dynamic stream discovery
1110
+ * @see {@link start_correlations} for automatic correlation
906
1111
  */
907
1112
  async drain({
908
1113
  streamLimit = 10,
@@ -999,9 +1204,49 @@ var Act = class {
999
1204
  return { fetched: [], leased: [], acked: [], blocked: [] };
1000
1205
  }
1001
1206
  /**
1002
- * Correlates streams using reaction resolvers.
1003
- * @param query - The query filter (e.g., by stream, event name, or starting point).
1004
- * @returns The leases of newly correlated streams, and the last seen event ID.
1207
+ * Discovers and registers new streams dynamically based on reaction resolvers.
1208
+ *
1209
+ * Correlation enables "dynamic reactions" where target streams are determined at runtime
1210
+ * based on event content. For example, you might create a stats stream for each user
1211
+ * when they perform certain actions.
1212
+ *
1213
+ * This method scans events matching the query and identifies new target streams based
1214
+ * on reaction resolvers. It then registers these streams so they'll be picked up by
1215
+ * the next drain cycle.
1216
+ *
1217
+ * @param query - Query filter to scan for new correlations
1218
+ * @param query.after - Start scanning after this event ID (default: -1)
1219
+ * @param query.limit - Maximum events to scan (default: 10)
1220
+ * @returns Object with newly leased streams and last scanned event ID
1221
+ *
1222
+ * @example Manual correlation
1223
+ * ```typescript
1224
+ * // Scan for new streams
1225
+ * const { leased, last_id } = await app.correlate({ after: 0, limit: 100 });
1226
+ * console.log(`Found ${leased.length} new streams`);
1227
+ *
1228
+ * // Save last_id for next scan
1229
+ * await saveCheckpoint(last_id);
1230
+ * ```
1231
+ *
1232
+ * @example Dynamic stream creation
1233
+ * ```typescript
1234
+ * const app = act()
1235
+ * .with(User)
1236
+ * .with(UserStats)
1237
+ * .on("UserLoggedIn")
1238
+ * .do(async (event) => ["incrementLoginCount", {}])
1239
+ * .to((event) => ({
1240
+ * target: `stats-${event.stream}` // Dynamic target per user
1241
+ * }))
1242
+ * .build();
1243
+ *
1244
+ * // Discover stats streams as users log in
1245
+ * await app.correlate();
1246
+ * ```
1247
+ *
1248
+ * @see {@link start_correlations} for automatic periodic correlation
1249
+ * @see {@link stop_correlations} to stop automatic correlation
1005
1250
  */
1006
1251
  async correlate(query = { after: -1, limit: 10 }) {
1007
1252
  const correlated = /* @__PURE__ */ new Map();
@@ -1034,19 +1279,59 @@ var Act = class {
1034
1279
  return { leased: [], last_id };
1035
1280
  }
1036
1281
  /**
1037
- * Starts correlation worker that identifies and registers new streams using reaction resolvers.
1282
+ * Starts automatic periodic correlation worker for discovering new streams.
1283
+ *
1284
+ * The correlation worker runs in the background, scanning for new events and identifying
1285
+ * new target streams based on reaction resolvers. It maintains a sliding window that
1286
+ * advances with each scan, ensuring all events are eventually correlated.
1038
1287
  *
1039
- * Enables "dynamic reactions", allowing streams to be auto-discovered based on event content.
1040
- * - Uses a correlation sliding window over the event stream to identify new streams.
1041
- * - Once registered, these streams are picked up by the main `drain` loop.
1042
- * - Users should have full control over their correlation strategy.
1043
- * - The starting point keeps increasing with each new batch of events.
1044
- * - Users are responsible for storing the last seen event ID.
1288
+ * This is useful for dynamic stream creation patterns where you don't know all streams
1289
+ * upfront - they're discovered as events arrive.
1045
1290
  *
1046
- * @param query - The query filter (e.g., by stream, event name, or starting point).
1047
- * @param frequency - The frequency of correlation checks (in milliseconds).
1048
- * @param callback - Callback to report stats (new strems, last seen event ID, etc.).
1049
- * @returns true if the correlation worker started, false otherwise (already started).
1291
+ * **Note:** Only one correlation worker can run at a time per Act instance.
1292
+ *
1293
+ * @param query - Query filter for correlation scans
1294
+ * @param query.after - Initial starting point (default: -1, start from beginning)
1295
+ * @param query.limit - Events to scan per cycle (default: 100)
1296
+ * @param frequency - Correlation frequency in milliseconds (default: 10000)
1297
+ * @param callback - Optional callback invoked with newly discovered streams
1298
+ * @returns `true` if worker started, `false` if already running
1299
+ *
1300
+ * @example Start automatic correlation
1301
+ * ```typescript
1302
+ * // Start correlation worker scanning every 5 seconds
1303
+ * app.start_correlations(
1304
+ * { after: 0, limit: 100 },
1305
+ * 5000,
1306
+ * (leased) => {
1307
+ * console.log(`Discovered ${leased.length} new streams`);
1308
+ * }
1309
+ * );
1310
+ *
1311
+ * // Later, stop it
1312
+ * app.stop_correlations();
1313
+ * ```
1314
+ *
1315
+ * @example With checkpoint persistence
1316
+ * ```typescript
1317
+ * // Load last checkpoint
1318
+ * const lastId = await loadCheckpoint();
1319
+ *
1320
+ * app.start_correlations(
1321
+ * { after: lastId, limit: 100 },
1322
+ * 10000,
1323
+ * async (leased) => {
1324
+ * // Save checkpoint for next restart
1325
+ * if (leased.length) {
1326
+ * const maxId = Math.max(...leased.map(l => l.at));
1327
+ * await saveCheckpoint(maxId);
1328
+ * }
1329
+ * }
1330
+ * );
1331
+ * ```
1332
+ *
1333
+ * @see {@link correlate} for manual one-time correlation
1334
+ * @see {@link stop_correlations} to stop the worker
1050
1335
  */
1051
1336
  start_correlations(query = {}, frequency = 1e4, callback) {
1052
1337
  if (this._correlation_interval) return false;
@@ -1061,6 +1346,23 @@ var Act = class {
1061
1346
  );
1062
1347
  return true;
1063
1348
  }
1349
+ /**
1350
+ * Stops the automatic correlation worker.
1351
+ *
1352
+ * Call this to stop the background correlation worker started by {@link start_correlations}.
1353
+ * This is automatically called when the Act instance is disposed.
1354
+ *
1355
+ * @example
1356
+ * ```typescript
1357
+ * // Start correlation
1358
+ * app.start_correlations();
1359
+ *
1360
+ * // Later, stop it
1361
+ * app.stop_correlations();
1362
+ * ```
1363
+ *
1364
+ * @see {@link start_correlations}
1365
+ */
1064
1366
  stop_correlations() {
1065
1367
  if (this._correlation_interval) {
1066
1368
  clearInterval(this._correlation_interval);