@adaas/a-utils 0.1.17 → 0.1.19

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.
Files changed (47) hide show
  1. package/dist/index.d.mts +964 -354
  2. package/dist/index.d.ts +964 -354
  3. package/dist/index.js +1426 -714
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +1426 -714
  6. package/dist/index.mjs.map +1 -1
  7. package/examples/A-Channel-examples.ts +13 -11
  8. package/examples/A-Command-examples-2.ts +429 -0
  9. package/examples/A-Command-examples.ts +487 -202
  10. package/examples/A-StateMachine-examples.ts +609 -0
  11. package/package.json +3 -2
  12. package/src/index.ts +1 -2
  13. package/src/lib/A-Channel/A-Channel.component.ts +14 -74
  14. package/src/lib/A-Channel/A-Channel.error.ts +5 -5
  15. package/src/lib/A-Channel/A-Channel.types.ts +2 -10
  16. package/src/lib/A-Channel/A-ChannelRequest.context.ts +25 -74
  17. package/src/lib/A-Command/A-Command.constants.ts +78 -23
  18. package/src/lib/A-Command/A-Command.entity.ts +447 -119
  19. package/src/lib/A-Command/A-Command.error.ts +11 -0
  20. package/src/lib/A-Command/A-Command.types.ts +96 -20
  21. package/src/lib/A-Command/A-CommandExecution.context.ts +0 -0
  22. package/src/lib/A-Command/README.md +164 -68
  23. package/src/lib/A-Config/A-Config.container.ts +2 -2
  24. package/src/lib/A-Config/A-Config.context.ts +19 -5
  25. package/src/lib/A-Config/components/ConfigReader.component.ts +1 -1
  26. package/src/lib/A-Logger/A-Logger.component.ts +211 -35
  27. package/src/lib/A-Logger/A-Logger.constants.ts +50 -10
  28. package/src/lib/A-Logger/A-Logger.env.ts +17 -1
  29. package/src/lib/A-Memory/A-Memory.component.ts +440 -0
  30. package/src/lib/A-Memory/A-Memory.constants.ts +49 -0
  31. package/src/lib/A-Memory/A-Memory.context.ts +14 -118
  32. package/src/lib/A-Memory/A-Memory.error.ts +21 -0
  33. package/src/lib/A-Memory/A-Memory.types.ts +21 -0
  34. package/src/lib/A-Operation/A-Operation.context.ts +58 -0
  35. package/src/lib/A-Operation/A-Operation.types.ts +47 -0
  36. package/src/lib/A-StateMachine/A-StateMachine.component.ts +258 -0
  37. package/src/lib/A-StateMachine/A-StateMachine.constants.ts +18 -0
  38. package/src/lib/A-StateMachine/A-StateMachine.error.ts +10 -0
  39. package/src/lib/A-StateMachine/A-StateMachine.types.ts +20 -0
  40. package/src/lib/A-StateMachine/A-StateMachineTransition.context.ts +41 -0
  41. package/src/lib/A-StateMachine/README.md +391 -0
  42. package/tests/A-Channel.test.ts +17 -14
  43. package/tests/A-Command.test.ts +548 -460
  44. package/tests/A-Logger.test.ts +8 -4
  45. package/tests/A-Memory.test.ts +151 -115
  46. package/tests/A-Schedule.test.ts +2 -2
  47. package/tests/A-StateMachine.test.ts +760 -0
@@ -0,0 +1,391 @@
1
+ # A_StateMachine
2
+
3
+ A powerful and flexible state machine implementation that allows you to define and manage complex state transitions with validation, hooks, and error handling.
4
+
5
+ ## Overview
6
+
7
+ `A_StateMachine` is a TypeScript-based state machine that extends `A_Component` from the `@adaas/a-concept` framework. It provides a robust foundation for implementing state-driven workflows with proper lifecycle management, error handling, and extensibility through hooks.
8
+
9
+ ## Features
10
+
11
+ - ✅ **Type-safe state definitions** - Define states and their associated data types using TypeScript interfaces
12
+ - ✅ **Lifecycle hooks** - Extend behavior with initialization, pre/post-transition, and error handling hooks
13
+ - ✅ **Asynchronous transitions** - Full async/await support for complex state transitions
14
+ - ✅ **Isolated scopes** - Each transition runs in its own scope with proper cleanup
15
+ - ✅ **Error handling** - Comprehensive error wrapping and handling with custom error types
16
+ - ✅ **Extensible** - Use the feature system to add custom transition logic and validation
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ npm install @adaas/a-utils
22
+ ```
23
+
24
+ ## Basic Usage
25
+
26
+ ### 1. Define State Types
27
+
28
+ ```typescript
29
+ interface TrafficLightStates {
30
+ red: { duration: number };
31
+ yellow: { duration: number; fromState: 'red' | 'green' };
32
+ green: { duration: number };
33
+ }
34
+ ```
35
+
36
+ ### 2. Create State Machine
37
+
38
+ ```typescript
39
+ import { A_StateMachine } from '@adaas/a-utils/lib/A-StateMachine';
40
+
41
+ class TrafficLightMachine extends A_StateMachine<TrafficLightStates> {
42
+ // The state machine is ready to use with basic functionality
43
+ }
44
+
45
+ const trafficLight = new TrafficLightMachine();
46
+ ```
47
+
48
+ ### 3. Execute Transitions
49
+
50
+ ```typescript
51
+ // Wait for initialization
52
+ await trafficLight.ready;
53
+
54
+ // Execute transitions
55
+ await trafficLight.transition('red', 'green', { duration: 30000 });
56
+ await trafficLight.transition('green', 'yellow', {
57
+ duration: 3000,
58
+ fromState: 'green'
59
+ });
60
+ await trafficLight.transition('yellow', 'red', { duration: 60000 });
61
+ ```
62
+
63
+ ## Advanced Usage
64
+
65
+ ### Custom Transition Logic
66
+
67
+ Define custom logic for specific transitions by implementing methods following the naming convention `{fromState}_{toState}`:
68
+
69
+ ```typescript
70
+ class OrderStateMachine extends A_StateMachine<OrderStates> {
71
+
72
+ // Custom transition from pending to processing
73
+ async pending_processing(scope: A_Scope): Promise<void> {
74
+ const operation = scope.resolve(A_OperationContext)!;
75
+ const { orderId, processedBy } = operation.props;
76
+
77
+ // Validate the transition
78
+ if (!processedBy) {
79
+ throw new Error('processedBy is required for processing transition');
80
+ }
81
+
82
+ // Custom business logic
83
+ console.log(`Order ${orderId} is now being processed by ${processedBy}`);
84
+
85
+ // You can set results in the operation context
86
+ operation.result = { processedAt: new Date() };
87
+ }
88
+
89
+ // Custom transition from processing to completed
90
+ async processing_completed(scope: A_Scope): Promise<void> {
91
+ const operation = scope.resolve(A_OperationContext)!;
92
+ const { orderId } = operation.props;
93
+
94
+ // Finalization logic
95
+ console.log(`Order ${orderId} has been completed`);
96
+ }
97
+ }
98
+ ```
99
+
100
+ ### Lifecycle Hooks
101
+
102
+ Extend the state machine with hooks that run at specific points in the transition lifecycle:
103
+
104
+ ```typescript
105
+ class MonitoredStateMachine extends A_StateMachine<MyStates> {
106
+
107
+ @A_Feature.Extend()
108
+ async [A_StateMachineFeatures.onInitialize](): Promise<void> {
109
+ console.log('State machine initialized');
110
+ // Custom initialization logic
111
+ }
112
+
113
+ @A_Feature.Extend()
114
+ async [A_StateMachineFeatures.onBeforeTransition](scope: A_Scope): Promise<void> {
115
+ const operation = scope.resolve(A_OperationContext)!;
116
+ console.log(`About to transition from ${operation.props.from} to ${operation.props.to}`);
117
+
118
+ // Add validation logic
119
+ await this.validateTransition(operation.props.from, operation.props.to);
120
+ }
121
+
122
+ @A_Feature.Extend()
123
+ async [A_StateMachineFeatures.onAfterTransition](scope: A_Scope): Promise<void> {
124
+ const operation = scope.resolve(A_OperationContext)!;
125
+ console.log(`Successfully transitioned to ${operation.props.to}`);
126
+
127
+ // Log to audit system, update metrics, etc.
128
+ await this.logTransition(operation);
129
+ }
130
+
131
+ @A_Feature.Extend()
132
+ async [A_StateMachineFeatures.onError](scope: A_Scope): Promise<void> {
133
+ const error = scope.resolve(A_StateMachineError);
134
+ console.error('Transition failed:', error?.message);
135
+
136
+ // Custom error handling - notifications, cleanup, etc.
137
+ await this.handleTransitionError(error);
138
+ }
139
+
140
+ private async validateTransition(from: keyof MyStates, to: keyof MyStates): Promise<void> {
141
+ // Custom validation logic
142
+ }
143
+
144
+ private async logTransition(operation: A_OperationContext): Promise<void> {
145
+ // Custom logging logic
146
+ }
147
+
148
+ private async handleTransitionError(error: A_StateMachineError | undefined): Promise<void> {
149
+ // Custom error handling logic
150
+ }
151
+ }
152
+ ```
153
+
154
+ ### External Components
155
+
156
+ You can also extend state machine behavior using external components:
157
+
158
+ ```typescript
159
+ @A_Component()
160
+ class StateLogger extends A_Component {
161
+
162
+ @A_Feature.Extend({ scope: [OrderStateMachine] })
163
+ async [A_StateMachineFeatures.onBeforeTransition](scope: A_Scope): Promise<void> {
164
+ const operation = scope.resolve(A_OperationContext)!;
165
+ console.log(`[LOGGER] Transition: ${operation.props.from} -> ${operation.props.to}`);
166
+ }
167
+
168
+ @A_Feature.Extend({ scope: [OrderStateMachine] })
169
+ async [A_StateMachineFeatures.onError](scope: A_Scope): Promise<void> {
170
+ const error = scope.resolve(A_StateMachineError);
171
+ console.error(`[LOGGER] Transition error: ${error?.message}`);
172
+ }
173
+ }
174
+
175
+ // Register the component in your context
176
+ A_Context.root.register(StateLogger);
177
+ ```
178
+
179
+ ## API Reference
180
+
181
+ ### Class: A_StateMachine<T>
182
+
183
+ #### Type Parameters
184
+
185
+ - `T extends Record<string, any>` - Defines the state structure where keys are state names and values are the data types for each state.
186
+
187
+ #### Properties
188
+
189
+ ##### `ready: Promise<void>`
190
+
191
+ A promise that resolves when the state machine is fully initialized and ready for transitions.
192
+
193
+ ```typescript
194
+ await stateMachine.ready;
195
+ ```
196
+
197
+ #### Methods
198
+
199
+ ##### `transition(from, to, props?): Promise<void>`
200
+
201
+ Executes a state transition.
202
+
203
+ **Parameters:**
204
+ - `from: keyof T` - The state to transition from
205
+ - `to: keyof T` - The state to transition to
206
+ - `props?: T[keyof T]` - Optional properties for the transition
207
+
208
+ **Returns:** `Promise<void>`
209
+
210
+ **Throws:** `A_StateMachineError` if the transition fails
211
+
212
+ #### Lifecycle Hooks
213
+
214
+ ##### `onInitialize(...args: any[]): Promise<void>`
215
+
216
+ Called during state machine initialization.
217
+
218
+ ##### `onBeforeTransition(...args: any[]): Promise<void>`
219
+
220
+ Called before every transition. Receives the transition scope as an argument.
221
+
222
+ ##### `onAfterTransition(...args: any[]): Promise<void>`
223
+
224
+ Called after successful transitions. Receives the transition scope as an argument.
225
+
226
+ ##### `onError(...args: any[]): Promise<void>`
227
+
228
+ Called when transitions fail. Receives the error scope as an argument.
229
+
230
+ ### Transition Method Convention
231
+
232
+ For custom transition logic, implement methods using the naming convention:
233
+
234
+ ```
235
+ {fromState}_{toState}(scope: A_Scope): Promise<void>
236
+ ```
237
+
238
+ Example:
239
+ ```typescript
240
+ async pending_processing(scope: A_Scope): Promise<void> {
241
+ // Custom transition logic
242
+ }
243
+ ```
244
+
245
+ ## Error Handling
246
+
247
+ The state machine provides comprehensive error handling:
248
+
249
+ ### A_StateMachineError
250
+
251
+ All transition errors are wrapped in `A_StateMachineError` instances:
252
+
253
+ ```typescript
254
+ try {
255
+ await stateMachine.transition('invalid', 'state');
256
+ } catch (error) {
257
+ if (error instanceof A_StateMachineError) {
258
+ console.log('Error type:', error.title);
259
+ console.log('Description:', error.description);
260
+ console.log('Original error:', error.originalError);
261
+ }
262
+ }
263
+ ```
264
+
265
+ ### Error Types
266
+
267
+ - `A_StateMachineError.InitializationError` - Errors during initialization
268
+ - `A_StateMachineError.TransitionError` - Errors during state transitions
269
+
270
+ ## Best Practices
271
+
272
+ ### 1. Define Clear State Interfaces
273
+
274
+ ```typescript
275
+ interface DocumentStates {
276
+ draft: {
277
+ content: string;
278
+ authorId: string;
279
+ };
280
+ review: {
281
+ content: string;
282
+ authorId: string;
283
+ reviewerId: string;
284
+ requestedAt: Date;
285
+ };
286
+ published: {
287
+ content: string;
288
+ authorId: string;
289
+ publishedAt: Date;
290
+ version: number;
291
+ };
292
+ archived: {
293
+ content: string;
294
+ authorId: string;
295
+ archivedAt: Date;
296
+ reason: string;
297
+ };
298
+ }
299
+ ```
300
+
301
+ ### 2. Implement Validation in Transitions
302
+
303
+ ```typescript
304
+ async draft_review(scope: A_Scope): Promise<void> {
305
+ const operation = scope.resolve(A_OperationContext)!;
306
+ const { content, reviewerId } = operation.props;
307
+
308
+ if (!content || content.trim().length === 0) {
309
+ throw new Error('Content cannot be empty when submitting for review');
310
+ }
311
+
312
+ if (!reviewerId) {
313
+ throw new Error('Reviewer must be assigned');
314
+ }
315
+
316
+ // Additional validation and business logic
317
+ }
318
+ ```
319
+
320
+ ### 3. Use Hooks for Cross-Cutting Concerns
321
+
322
+ ```typescript
323
+ @A_Feature.Extend()
324
+ async [A_StateMachineFeatures.onAfterTransition](scope: A_Scope): Promise<void> {
325
+ const operation = scope.resolve(A_OperationContext)!;
326
+
327
+ // Always log transitions
328
+ await this.auditService.logTransition({
329
+ from: operation.props.from,
330
+ to: operation.props.to,
331
+ timestamp: new Date(),
332
+ props: operation.props.props
333
+ });
334
+
335
+ // Send notifications for specific transitions
336
+ if (operation.props.to === 'review') {
337
+ await this.notificationService.notifyReviewer(operation.props.props.reviewerId);
338
+ }
339
+ }
340
+ ```
341
+
342
+ ### 4. Handle Async Operations Properly
343
+
344
+ ```typescript
345
+ async processing_completed(scope: A_Scope): Promise<void> {
346
+ const operation = scope.resolve(A_OperationContext)!;
347
+ const { orderId } = operation.props;
348
+
349
+ try {
350
+ // Multiple async operations
351
+ const [invoice, notification, analytics] = await Promise.all([
352
+ this.invoiceService.generate(orderId),
353
+ this.notificationService.notifyCustomer(orderId),
354
+ this.analyticsService.trackCompletion(orderId)
355
+ ]);
356
+
357
+ operation.result = {
358
+ invoiceId: invoice.id,
359
+ notificationSent: notification.success,
360
+ analyticsTracked: analytics.success
361
+ };
362
+ } catch (error) {
363
+ throw new Error(`Failed to complete order processing: ${error.message}`);
364
+ }
365
+ }
366
+ ```
367
+
368
+ ## Examples
369
+
370
+ For complete working examples, see the `examples/A-StateMachine/` directory:
371
+
372
+ - `basic-example.ts` - Simple traffic light state machine
373
+ - `order-processing.ts` - E-commerce order workflow
374
+ - `user-authentication.ts` - User authentication flow
375
+
376
+ ## Testing
377
+
378
+ The state machine includes comprehensive test coverage. See `tests/A-StateMachine.test.ts` for examples of:
379
+
380
+ - Basic state machine creation and transitions
381
+ - Custom transition logic testing
382
+ - Lifecycle hook verification
383
+ - Error handling scenarios
384
+ - Complex workflow testing
385
+
386
+ ## Related Components
387
+
388
+ - `A_Command` - For command pattern implementation
389
+ - `A_OperationContext` - Used internally for transition context
390
+ - `A_Component` - Base class that provides the feature system
391
+ - `A_Scope` - Provides dependency injection and lifecycle management
@@ -4,6 +4,7 @@ import { A_Channel } from '@adaas/a-utils/lib/A-Channel/A-Channel.component';
4
4
  import { A_ChannelFeatures } from '@adaas/a-utils/lib/A-Channel/A-Channel.constants';
5
5
  import { A_ChannelRequest } from '@adaas/a-utils/lib/A-Channel/A-ChannelRequest.context';
6
6
  import { A_ChannelError } from '@adaas/a-utils/lib/A-Channel/A-Channel.error';
7
+ import { A_OperationContext } from '@adaas/a-utils/lib/A-Operation/A-Operation.context';
7
8
 
8
9
  jest.retryTimes(0);
9
10
 
@@ -122,7 +123,7 @@ describe('A-Channel tests', () => {
122
123
  ) {
123
124
  processingOrder.push('during');
124
125
  // Simulate processing and setting result
125
- (context as any)._result = { processed: true, original: context.params };
126
+ context.succeed({ processed: true, original: context.params });
126
127
  }
127
128
 
128
129
  @A_Feature.Extend({ scope: [TestChannel] })
@@ -162,7 +163,7 @@ describe('A-Channel tests', () => {
162
163
  @A_Inject(A_ChannelRequest) context: A_ChannelRequest
163
164
  ) {
164
165
  errorCalls.push('error-handled');
165
- expect(context.failed).toBe(true);
166
+ expect(context.error).toBeDefined();
166
167
  }
167
168
  }
168
169
 
@@ -172,10 +173,11 @@ describe('A-Channel tests', () => {
172
173
  A_Context.root.register(channel);
173
174
 
174
175
  const params = { action: 'fail' };
175
- const context = await channel.request(params);
176
+
177
+ // Request should throw an error since the processing fails
178
+ await expect(channel.request(params)).rejects.toThrow();
176
179
 
177
180
  expect(errorCalls).toEqual(['error-handled']);
178
- expect(context.failed).toBe(true);
179
181
  expect(channel.processing).toBe(false); // Should reset even on error
180
182
  });
181
183
 
@@ -199,11 +201,11 @@ describe('A-Channel tests', () => {
199
201
  @A_Inject(A_ChannelRequest) context: A_ChannelRequest<TestParams, TestResult>
200
202
  ) {
201
203
  const { userId, action } = context.params;
202
- (context as any)._result = {
204
+ context.succeed({
203
205
  success: true,
204
206
  userId,
205
207
  timestamp: new Date().toISOString()
206
- };
208
+ });
207
209
  }
208
210
  }
209
211
 
@@ -243,7 +245,7 @@ describe('A-Channel tests', () => {
243
245
  class SendProcessor extends A_Component {
244
246
  @A_Feature.Extend({ scope: [SendChannel] })
245
247
  async [A_ChannelFeatures.onSend](
246
- @A_Inject(A_ChannelRequest) context: A_ChannelRequest
248
+ @A_Inject(A_OperationContext) context: A_OperationContext
247
249
  ) {
248
250
  sentMessages.push(context.params);
249
251
  }
@@ -278,10 +280,10 @@ describe('A-Channel tests', () => {
278
280
 
279
281
  @A_Feature.Extend({ scope: [ErrorSendChannel] })
280
282
  async [A_ChannelFeatures.onError](
281
- @A_Inject(A_ChannelRequest) context: A_ChannelRequest
283
+ @A_Inject(A_OperationContext) context: A_OperationContext
282
284
  ) {
283
285
  errorCalls.push('send-error-handled');
284
- expect(context.failed).toBe(true);
286
+ expect(context.error).toBeDefined();
285
287
  }
286
288
  }
287
289
 
@@ -344,9 +346,10 @@ describe('A-Channel tests', () => {
344
346
  const channel = new MultiErrorChannel();
345
347
  A_Context.root.register(channel);
346
348
 
347
- await channel.request({ errorType: 'network' });
348
- await channel.request({ errorType: 'validation' });
349
- await channel.request({ errorType: 'timeout' });
349
+ // These requests should all throw errors, but the error handler should be called
350
+ try { await channel.request({ errorType: 'network' }); } catch (e) { /* expected */ }
351
+ try { await channel.request({ errorType: 'validation' }); } catch (e) { /* expected */ }
352
+ try { await channel.request({ errorType: 'timeout' }); } catch (e) { /* expected */ }
350
353
  await channel.request({ errorType: 'none' }); // Should not error
351
354
 
352
355
  expect(errorTypes).toEqual(['network', 'validation', 'timeout']);
@@ -414,7 +417,7 @@ describe('A-Channel tests', () => {
414
417
  class WebSocketProcessor extends A_Component {
415
418
  @A_Feature.Extend({ scope: [WebSocketChannel] })
416
419
  async [A_ChannelFeatures.onSend](
417
- @A_Inject(A_ChannelRequest) context: A_ChannelRequest
420
+ @A_Inject(A_OperationContext) context: A_OperationContext
418
421
  ) {
419
422
  wsCalls.push(`WS: ${context.params.message}`);
420
423
  }
@@ -451,7 +454,7 @@ describe('A-Channel tests', () => {
451
454
  const delay = context.params.delay || 0;
452
455
  await new Promise(resolve => setTimeout(resolve, delay));
453
456
  processingOrder.push(context.params.id);
454
- (context as any)._result = { processed: context.params.id };
457
+ context.succeed({ processed: context.params.id });
455
458
  }
456
459
  }
457
460