@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.js CHANGED
@@ -714,18 +714,87 @@ var Act = class {
714
714
  return this;
715
715
  }
716
716
  /**
717
- * Executes an action (command) against a state machine, emitting and committing the resulting event(s).
717
+ * Executes an action on a state instance, committing resulting events.
718
718
  *
719
- * @template K The type of action to execute
720
- * @param action The action name (key of the action schema)
721
- * @param target The target (stream and actor) for the action
722
- * @param payload The action payload (validated against the schema)
723
- * @param reactingTo (Optional) The event this action is reacting to
724
- * @param skipValidation (Optional) If true, skips schema validation (not recommended)
725
- * @returns The snapshot of the committed event
719
+ * This is the primary method for modifying state. It:
720
+ * 1. Validates the action payload against the schema
721
+ * 2. Loads the current state snapshot
722
+ * 3. Checks invariants (business rules)
723
+ * 4. Executes the action handler to generate events
724
+ * 5. Applies events to create new state
725
+ * 6. Commits events to the store with optimistic concurrency control
726
726
  *
727
- * @example
728
- * await app.do("increment", { stream: "counter1", actor }, { by: 1 });
727
+ * @template K - Action name from registered actions
728
+ * @param action - The name of the action to execute
729
+ * @param target - Target specification with stream ID and actor context
730
+ * @param payload - Action payload matching the action's schema
731
+ * @param reactingTo - Optional event that triggered this action (for correlation)
732
+ * @param skipValidation - Skip schema validation (use carefully, for performance)
733
+ * @returns Array of snapshots for all affected states (usually one)
734
+ *
735
+ * @throws {ValidationError} If payload doesn't match action schema
736
+ * @throws {InvariantError} If business rules are violated
737
+ * @throws {ConcurrencyError} If another process modified the stream
738
+ *
739
+ * @example Basic action execution
740
+ * ```typescript
741
+ * const snapshots = await app.do(
742
+ * "increment",
743
+ * {
744
+ * stream: "counter-1",
745
+ * actor: { id: "user1", name: "Alice" }
746
+ * },
747
+ * { by: 5 }
748
+ * );
749
+ *
750
+ * console.log(snapshots[0].state.count); // Current count after increment
751
+ * ```
752
+ *
753
+ * @example With error handling
754
+ * ```typescript
755
+ * try {
756
+ * await app.do(
757
+ * "withdraw",
758
+ * { stream: "account-123", actor: { id: "user1", name: "Alice" } },
759
+ * { amount: 1000 }
760
+ * );
761
+ * } catch (error) {
762
+ * if (error instanceof InvariantError) {
763
+ * console.error("Business rule violated:", error.description);
764
+ * } else if (error instanceof ConcurrencyError) {
765
+ * console.error("Concurrent modification detected, retry...");
766
+ * } else if (error instanceof ValidationError) {
767
+ * console.error("Invalid payload:", error.details);
768
+ * }
769
+ * }
770
+ * ```
771
+ *
772
+ * @example Reaction triggering another action
773
+ * ```typescript
774
+ * const app = act()
775
+ * .with(Order)
776
+ * .with(Inventory)
777
+ * .on("OrderPlaced")
778
+ * .do(async (event, context) => {
779
+ * // This action is triggered by an event
780
+ * const result = await context.app.do(
781
+ * "reduceStock",
782
+ * {
783
+ * stream: "inventory-1",
784
+ * actor: event.meta.causation.action.actor
785
+ * },
786
+ * { amount: event.data.items.length },
787
+ * event // Pass event for correlation tracking
788
+ * );
789
+ * return result;
790
+ * })
791
+ * .to("inventory-1")
792
+ * .build();
793
+ * ```
794
+ *
795
+ * @see {@link Target} for target structure
796
+ * @see {@link Snapshot} for return value structure
797
+ * @see {@link ValidationError}, {@link InvariantError}, {@link ConcurrencyError}
729
798
  */
730
799
  async do(action2, target, payload, reactingTo, skipValidation = false) {
731
800
  const snapshots = await action(
@@ -741,31 +810,97 @@ var Act = class {
741
810
  return snapshots;
742
811
  }
743
812
  /**
744
- * Loads the current state snapshot for a given state machine and stream.
813
+ * Loads the current state snapshot for a specific stream.
745
814
  *
746
- * @template SX The type of state
747
- * @template EX The type of events
748
- * @template AX The type of actions
749
- * @param state The state machine definition
750
- * @param stream The stream (instance) to load
751
- * @param callback (Optional) Callback to receive the loaded snapshot
752
- * @returns The snapshot of the loaded state
815
+ * Reconstructs the current state by replaying events from the event store.
816
+ * Uses snapshots when available to optimize loading performance.
753
817
  *
754
- * @example
755
- * const snapshot = await app.load(Counter, "counter1");
818
+ * @template SX - State schema type
819
+ * @template EX - Event schemas type
820
+ * @template AX - Action schemas type
821
+ * @param state - The state definition to load
822
+ * @param stream - The stream ID (state instance identifier)
823
+ * @param callback - Optional callback invoked with the loaded snapshot
824
+ * @returns The current state snapshot for the stream
825
+ *
826
+ * @example Load current state
827
+ * ```typescript
828
+ * const snapshot = await app.load(Counter, "counter-1");
829
+ * console.log(snapshot.state.count); // Current count
830
+ * console.log(snapshot.version); // Number of events applied
831
+ * console.log(snapshot.patches); // Events since last snapshot
832
+ * ```
833
+ *
834
+ * @example With callback
835
+ * ```typescript
836
+ * const snapshot = await app.load(User, "user-123", (snap) => {
837
+ * console.log("Loaded user:", snap.state.name);
838
+ * });
839
+ * ```
840
+ *
841
+ * @example Load multiple states
842
+ * ```typescript
843
+ * const [user, account] = await Promise.all([
844
+ * app.load(User, "user-123"),
845
+ * app.load(BankAccount, "account-456")
846
+ * ]);
847
+ * ```
848
+ *
849
+ * @see {@link Snapshot} for snapshot structure
756
850
  */
757
851
  async load(state2, stream, callback) {
758
852
  return await load(state2, stream, callback);
759
853
  }
760
854
  /**
761
- * Query the event store for events matching a filter.
855
+ * Queries the event store for events matching a filter.
762
856
  *
763
- * @param query The query filter (e.g., by stream, event name, or time range)
764
- * @param callback (Optional) Callback for each event found
765
- * @returns An object with the first and last event found, and the total count
857
+ * Use this for analyzing event streams, generating reports, or debugging.
858
+ * The callback is invoked for each matching event, and the method returns
859
+ * summary information (first event, last event, total count).
766
860
  *
767
- * @example
768
- * const { count } = await app.query({ stream: "counter1" }, (event) => console.log(event));
861
+ * For small result sets, consider using {@link query_array} instead.
862
+ *
863
+ * @param query - The query filter
864
+ * @param query.stream - Filter by stream ID
865
+ * @param query.name - Filter by event name
866
+ * @param query.after - Filter events after this event ID
867
+ * @param query.before - Filter events before this event ID
868
+ * @param query.created_after - Filter events after this timestamp
869
+ * @param query.created_before - Filter events before this timestamp
870
+ * @param query.limit - Maximum number of events to return
871
+ * @param callback - Optional callback invoked for each matching event
872
+ * @returns Object with first event, last event, and total count
873
+ *
874
+ * @example Query all events for a stream
875
+ * ```typescript
876
+ * const { first, last, count } = await app.query(
877
+ * { stream: "counter-1" },
878
+ * (event) => console.log(event.name, event.data)
879
+ * );
880
+ * console.log(`Found ${count} events from ${first?.id} to ${last?.id}`);
881
+ * ```
882
+ *
883
+ * @example Query specific event types
884
+ * ```typescript
885
+ * const { count } = await app.query(
886
+ * { name: "UserCreated", limit: 100 },
887
+ * (event) => {
888
+ * console.log("User created:", event.data.email);
889
+ * }
890
+ * );
891
+ * ```
892
+ *
893
+ * @example Query events in time range
894
+ * ```typescript
895
+ * const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000);
896
+ * const { count } = await app.query({
897
+ * created_after: yesterday,
898
+ * stream: "user-123"
899
+ * });
900
+ * console.log(`User had ${count} events in last 24 hours`);
901
+ * ```
902
+ *
903
+ * @see {@link query_array} for loading events into memory
769
904
  */
770
905
  async query(query, callback) {
771
906
  let first = void 0, last = void 0;
@@ -777,14 +912,30 @@ var Act = class {
777
912
  return { first, last, count };
778
913
  }
779
914
  /**
780
- * Query the event store for events matching a filter.
781
- * Use this version with caution, as it return events in memory.
915
+ * Queries the event store and returns all matching events in memory.
782
916
  *
783
- * @param query The query filter (e.g., by stream, event name, or time range)
784
- * @returns The matching events
917
+ * **Use with caution** - this loads all results into memory. For large result sets,
918
+ * use {@link query} with a callback instead to process events incrementally.
785
919
  *
786
- * @example
787
- * const { count } = await app.query({ stream: "counter1" }, (event) => console.log(event));
920
+ * @param query - The query filter (same as {@link query})
921
+ * @returns Array of all matching events
922
+ *
923
+ * @example Load all events for a stream
924
+ * ```typescript
925
+ * const events = await app.query_array({ stream: "counter-1" });
926
+ * console.log(`Loaded ${events.length} events`);
927
+ * events.forEach(event => console.log(event.name, event.data));
928
+ * ```
929
+ *
930
+ * @example Get recent events
931
+ * ```typescript
932
+ * const recent = await app.query_array({
933
+ * stream: "user-123",
934
+ * limit: 10
935
+ * });
936
+ * ```
937
+ *
938
+ * @see {@link query} for large result sets
788
939
  */
789
940
  async query_array(query) {
790
941
  const events = [];
@@ -830,14 +981,68 @@ var Act = class {
830
981
  return { lease, handled, at };
831
982
  }
832
983
  /**
833
- * Drains and processes events from the store, triggering reactions and updating state.
984
+ * Processes pending reactions by draining uncommitted events from the event store.
834
985
  *
835
- * This is typically called in a background loop or after committing new events.
986
+ * The drain process:
987
+ * 1. Polls the store for streams with uncommitted events
988
+ * 2. Leases streams to prevent concurrent processing
989
+ * 3. Fetches events for each leased stream
990
+ * 4. Executes matching reaction handlers
991
+ * 5. Acknowledges successful reactions or blocks failing ones
836
992
  *
837
- * @returns The number of events drained and processed
993
+ * Drain uses a dual-frontier strategy to balance processing of new streams (lagging)
994
+ * vs active streams (leading). The ratio adapts based on event pressure.
838
995
  *
839
- * @example
996
+ * Call this method periodically in a background loop, or after committing events.
997
+ *
998
+ * @param options - Drain configuration options
999
+ * @param options.streamLimit - Maximum number of streams to process per cycle (default: 10)
1000
+ * @param options.eventLimit - Maximum events to fetch per stream (default: 10)
1001
+ * @param options.leaseMillis - Lease duration in milliseconds (default: 10000)
1002
+ * @returns Drain statistics with fetched, leased, acked, and blocked counts
1003
+ *
1004
+ * @example Basic drain loop
1005
+ * ```typescript
1006
+ * // Process reactions after each action
1007
+ * await app.do("createUser", target, payload);
840
1008
  * await app.drain();
1009
+ * ```
1010
+ *
1011
+ * @example Background drain worker
1012
+ * ```typescript
1013
+ * setInterval(async () => {
1014
+ * try {
1015
+ * const result = await app.drain({
1016
+ * streamLimit: 20,
1017
+ * eventLimit: 50
1018
+ * });
1019
+ * if (result.acked.length) {
1020
+ * console.log(`Processed ${result.acked.length} streams`);
1021
+ * }
1022
+ * } catch (error) {
1023
+ * console.error("Drain error:", error);
1024
+ * }
1025
+ * }, 5000); // Every 5 seconds
1026
+ * ```
1027
+ *
1028
+ * @example With lifecycle listeners
1029
+ * ```typescript
1030
+ * app.on("acked", (leases) => {
1031
+ * console.log(`Acknowledged ${leases.length} streams`);
1032
+ * });
1033
+ *
1034
+ * app.on("blocked", (blocked) => {
1035
+ * console.error(`Blocked ${blocked.length} streams due to errors`);
1036
+ * blocked.forEach(({ stream, error }) => {
1037
+ * console.error(`Stream ${stream}: ${error}`);
1038
+ * });
1039
+ * });
1040
+ *
1041
+ * await app.drain();
1042
+ * ```
1043
+ *
1044
+ * @see {@link correlate} for dynamic stream discovery
1045
+ * @see {@link start_correlations} for automatic correlation
841
1046
  */
842
1047
  async drain({
843
1048
  streamLimit = 10,
@@ -934,9 +1139,49 @@ var Act = class {
934
1139
  return { fetched: [], leased: [], acked: [], blocked: [] };
935
1140
  }
936
1141
  /**
937
- * Correlates streams using reaction resolvers.
938
- * @param query - The query filter (e.g., by stream, event name, or starting point).
939
- * @returns The leases of newly correlated streams, and the last seen event ID.
1142
+ * Discovers and registers new streams dynamically based on reaction resolvers.
1143
+ *
1144
+ * Correlation enables "dynamic reactions" where target streams are determined at runtime
1145
+ * based on event content. For example, you might create a stats stream for each user
1146
+ * when they perform certain actions.
1147
+ *
1148
+ * This method scans events matching the query and identifies new target streams based
1149
+ * on reaction resolvers. It then registers these streams so they'll be picked up by
1150
+ * the next drain cycle.
1151
+ *
1152
+ * @param query - Query filter to scan for new correlations
1153
+ * @param query.after - Start scanning after this event ID (default: -1)
1154
+ * @param query.limit - Maximum events to scan (default: 10)
1155
+ * @returns Object with newly leased streams and last scanned event ID
1156
+ *
1157
+ * @example Manual correlation
1158
+ * ```typescript
1159
+ * // Scan for new streams
1160
+ * const { leased, last_id } = await app.correlate({ after: 0, limit: 100 });
1161
+ * console.log(`Found ${leased.length} new streams`);
1162
+ *
1163
+ * // Save last_id for next scan
1164
+ * await saveCheckpoint(last_id);
1165
+ * ```
1166
+ *
1167
+ * @example Dynamic stream creation
1168
+ * ```typescript
1169
+ * const app = act()
1170
+ * .with(User)
1171
+ * .with(UserStats)
1172
+ * .on("UserLoggedIn")
1173
+ * .do(async (event) => ["incrementLoginCount", {}])
1174
+ * .to((event) => ({
1175
+ * target: `stats-${event.stream}` // Dynamic target per user
1176
+ * }))
1177
+ * .build();
1178
+ *
1179
+ * // Discover stats streams as users log in
1180
+ * await app.correlate();
1181
+ * ```
1182
+ *
1183
+ * @see {@link start_correlations} for automatic periodic correlation
1184
+ * @see {@link stop_correlations} to stop automatic correlation
940
1185
  */
941
1186
  async correlate(query = { after: -1, limit: 10 }) {
942
1187
  const correlated = /* @__PURE__ */ new Map();
@@ -969,19 +1214,59 @@ var Act = class {
969
1214
  return { leased: [], last_id };
970
1215
  }
971
1216
  /**
972
- * Starts correlation worker that identifies and registers new streams using reaction resolvers.
1217
+ * Starts automatic periodic correlation worker for discovering new streams.
1218
+ *
1219
+ * The correlation worker runs in the background, scanning for new events and identifying
1220
+ * new target streams based on reaction resolvers. It maintains a sliding window that
1221
+ * advances with each scan, ensuring all events are eventually correlated.
973
1222
  *
974
- * Enables "dynamic reactions", allowing streams to be auto-discovered based on event content.
975
- * - Uses a correlation sliding window over the event stream to identify new streams.
976
- * - Once registered, these streams are picked up by the main `drain` loop.
977
- * - Users should have full control over their correlation strategy.
978
- * - The starting point keeps increasing with each new batch of events.
979
- * - Users are responsible for storing the last seen event ID.
1223
+ * This is useful for dynamic stream creation patterns where you don't know all streams
1224
+ * upfront - they're discovered as events arrive.
980
1225
  *
981
- * @param query - The query filter (e.g., by stream, event name, or starting point).
982
- * @param frequency - The frequency of correlation checks (in milliseconds).
983
- * @param callback - Callback to report stats (new strems, last seen event ID, etc.).
984
- * @returns true if the correlation worker started, false otherwise (already started).
1226
+ * **Note:** Only one correlation worker can run at a time per Act instance.
1227
+ *
1228
+ * @param query - Query filter for correlation scans
1229
+ * @param query.after - Initial starting point (default: -1, start from beginning)
1230
+ * @param query.limit - Events to scan per cycle (default: 100)
1231
+ * @param frequency - Correlation frequency in milliseconds (default: 10000)
1232
+ * @param callback - Optional callback invoked with newly discovered streams
1233
+ * @returns `true` if worker started, `false` if already running
1234
+ *
1235
+ * @example Start automatic correlation
1236
+ * ```typescript
1237
+ * // Start correlation worker scanning every 5 seconds
1238
+ * app.start_correlations(
1239
+ * { after: 0, limit: 100 },
1240
+ * 5000,
1241
+ * (leased) => {
1242
+ * console.log(`Discovered ${leased.length} new streams`);
1243
+ * }
1244
+ * );
1245
+ *
1246
+ * // Later, stop it
1247
+ * app.stop_correlations();
1248
+ * ```
1249
+ *
1250
+ * @example With checkpoint persistence
1251
+ * ```typescript
1252
+ * // Load last checkpoint
1253
+ * const lastId = await loadCheckpoint();
1254
+ *
1255
+ * app.start_correlations(
1256
+ * { after: lastId, limit: 100 },
1257
+ * 10000,
1258
+ * async (leased) => {
1259
+ * // Save checkpoint for next restart
1260
+ * if (leased.length) {
1261
+ * const maxId = Math.max(...leased.map(l => l.at));
1262
+ * await saveCheckpoint(maxId);
1263
+ * }
1264
+ * }
1265
+ * );
1266
+ * ```
1267
+ *
1268
+ * @see {@link correlate} for manual one-time correlation
1269
+ * @see {@link stop_correlations} to stop the worker
985
1270
  */
986
1271
  start_correlations(query = {}, frequency = 1e4, callback) {
987
1272
  if (this._correlation_interval) return false;
@@ -996,6 +1281,23 @@ var Act = class {
996
1281
  );
997
1282
  return true;
998
1283
  }
1284
+ /**
1285
+ * Stops the automatic correlation worker.
1286
+ *
1287
+ * Call this to stop the background correlation worker started by {@link start_correlations}.
1288
+ * This is automatically called when the Act instance is disposed.
1289
+ *
1290
+ * @example
1291
+ * ```typescript
1292
+ * // Start correlation
1293
+ * app.start_correlations();
1294
+ *
1295
+ * // Later, stop it
1296
+ * app.stop_correlations();
1297
+ * ```
1298
+ *
1299
+ * @see {@link start_correlations}
1300
+ */
999
1301
  stop_correlations() {
1000
1302
  if (this._correlation_interval) {
1001
1303
  clearInterval(this._correlation_interval);