@adaas/a-utils 0.1.18 → 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,760 @@
1
+ import { A_StateMachine } from '@adaas/a-utils/lib/A-StateMachine/A-StateMachine.component';
2
+ import { A_StateMachineFeatures } from '@adaas/a-utils/lib/A-StateMachine/A-StateMachine.constants';
3
+ import { A_StateMachineError } from '@adaas/a-utils/lib/A-StateMachine/A-StateMachine.error';
4
+ import { A_Component, A_Context, A_Dependency, A_Error, A_Feature, A_Inject, A_Scope } from '@adaas/a-concept';
5
+ import { A_OperationContext } from '@adaas/a-utils/lib/A-Operation/A-Operation.context';
6
+ import { A_StateMachineTransition } from '@adaas/a-utils/lib/A-StateMachine/A-StateMachineTransition.context';
7
+
8
+ jest.retryTimes(0);
9
+
10
+ describe('A-StateMachine tests', () => {
11
+
12
+ it('Should allow to create a state machine', async () => {
13
+ interface SimpleStates {
14
+ idle: { message: string };
15
+ running: { message: string };
16
+ }
17
+
18
+ const stateMachine = new A_StateMachine<SimpleStates>();
19
+ A_Context.root.register(stateMachine);
20
+
21
+ expect(stateMachine).toBeInstanceOf(A_StateMachine);
22
+ expect(stateMachine).toBeInstanceOf(A_Component);
23
+ });
24
+
25
+ it('Should allow to initialize a state machine', async () => {
26
+ const stateMachine = new A_StateMachine();
27
+ A_Context.root.register(stateMachine);
28
+
29
+ await stateMachine.ready;
30
+
31
+ expect(stateMachine.ready).toBeInstanceOf(Promise);
32
+ });
33
+
34
+ it('Should allow to create a state machine with custom generic types', async () => {
35
+ interface OrderStates {
36
+ pending: { orderId: string; amount: number };
37
+ processing: { orderId: string; amount: number; processedBy: string };
38
+ completed: { orderId: string; amount: number; completedAt: Date };
39
+ cancelled: { orderId: string; amount: number; reason: string };
40
+ }
41
+
42
+ class OrderStateMachine extends A_StateMachine<OrderStates> { }
43
+
44
+ const orderMachine = new OrderStateMachine();
45
+ A_Context.root.register(orderMachine);
46
+
47
+ expect(orderMachine).toBeInstanceOf(A_StateMachine);
48
+ expect(orderMachine).toBeInstanceOf(A_Component);
49
+ });
50
+
51
+ it('Should allow to execute a basic transition', async () => {
52
+ interface SimpleStates {
53
+ idle: { timestamp: Date };
54
+ running: { timestamp: Date };
55
+ }
56
+
57
+ const stateMachine = new A_StateMachine<SimpleStates>();
58
+ A_Context.root.register(stateMachine);
59
+
60
+ await stateMachine.ready;
61
+
62
+ const transitionData = { timestamp: new Date() };
63
+ await stateMachine.transition('idle', 'running', transitionData);
64
+
65
+ // If we get here without throwing, the transition succeeded
66
+ expect(true).toBe(true);
67
+ });
68
+
69
+ it('Should execute transition lifecycle hooks in correct order', async () => {
70
+ const lifecycleOrder: string[] = [];
71
+
72
+ interface TestStates {
73
+ start: { value: string };
74
+ end: { value: string };
75
+ }
76
+
77
+ class TestStateMachine extends A_StateMachine<TestStates> {
78
+ @A_Feature.Extend()
79
+ async [A_StateMachineFeatures.onInitialize](): Promise<void> {
80
+ lifecycleOrder.push('initialize');
81
+ }
82
+
83
+ @A_Feature.Extend()
84
+ async [A_StateMachineFeatures.onBeforeTransition](): Promise<void> {
85
+ lifecycleOrder.push('beforeTransition');
86
+ }
87
+
88
+ @A_Feature.Extend()
89
+ async [A_StateMachineFeatures.onAfterTransition](): Promise<void> {
90
+ lifecycleOrder.push('afterTransition');
91
+ }
92
+ }
93
+
94
+ const stateMachine = new TestStateMachine();
95
+ A_Context.root.register(stateMachine);
96
+
97
+ await stateMachine.ready;
98
+ await stateMachine.transition('start', 'end', { value: 'test' });
99
+
100
+ expect(lifecycleOrder).toEqual(['initialize', 'beforeTransition', 'afterTransition']);
101
+ });
102
+
103
+ it('Should allow custom transition logic', async () => {
104
+ interface OrderStates {
105
+ pending: { orderId: string; amount: number };
106
+ validated: { orderId: string; amount: number; validatedAt: Date };
107
+ }
108
+
109
+ class OrderStateMachine extends A_StateMachine<OrderStates> {
110
+ async pending_validated(scope: A_Scope): Promise<void> {
111
+ const operation = scope.resolve(A_OperationContext)!;
112
+ const orderData = operation.params.props;
113
+
114
+ // Custom validation logic
115
+ if (orderData.amount <= 0) {
116
+ throw new Error('Order amount must be positive');
117
+ }
118
+
119
+ operation.succeed({
120
+ ...orderData,
121
+ validatedAt: new Date()
122
+ });
123
+ }
124
+ }
125
+
126
+ const orderMachine = new OrderStateMachine();
127
+ A_Context.root.register(orderMachine);
128
+
129
+ await orderMachine.ready;
130
+
131
+ // Should succeed with valid data
132
+ await orderMachine.transition('pending', 'validated', {
133
+ orderId: 'ORD-001',
134
+ amount: 99.99
135
+ });
136
+
137
+ // Should fail with invalid data
138
+ try {
139
+ await orderMachine.transition('pending', 'validated', {
140
+ orderId: 'ORD-002',
141
+ amount: 0
142
+ });
143
+ } catch (error) {
144
+ expect(error).toBeInstanceOf(A_Error);
145
+ expect((error as A_Error).originalError.message).toBe('Order amount must be positive');
146
+ }
147
+
148
+ });
149
+
150
+ it('Should handle transition errors correctly', async () => {
151
+ interface ErrorStates {
152
+ start: { value: string };
153
+ end: { value: string };
154
+ }
155
+
156
+ class ErrorStateMachine extends A_StateMachine<ErrorStates> {
157
+ async start_end(scope: A_Scope): Promise<void> {
158
+ throw new Error('Custom transition error');
159
+ }
160
+ }
161
+
162
+ const stateMachine = new ErrorStateMachine();
163
+ A_Context.root.register(stateMachine);
164
+
165
+ await stateMachine.ready;
166
+
167
+ try {
168
+ await stateMachine.transition('start', 'end', { value: 'test' });
169
+ } catch (error) {
170
+ expect(error).toBeInstanceOf(A_Error);
171
+ expect((error as A_Error).originalError.message).toBe('Custom transition error');
172
+ }
173
+ });
174
+
175
+ it('Should allow external components to extend transition behavior', async () => {
176
+ A_Context.reset();
177
+
178
+ interface TestStates {
179
+ idle: { message: string };
180
+ active: { message: string };
181
+ }
182
+
183
+ class TestStateMachine extends A_StateMachine<TestStates> { }
184
+
185
+ class TestComponent extends A_Component {
186
+ static called = false;
187
+
188
+ @A_Feature.Extend({ scope: [TestStateMachine] })
189
+ async [A_StateMachineFeatures.onBeforeTransition](
190
+ @A_Inject(A_StateMachineTransition) transition: A_StateMachineTransition,
191
+ ): Promise<void> {
192
+ TestComponent.called = true;
193
+ const { props } = transition.params;
194
+
195
+ if (!props?.message) {
196
+ throw new Error('Message is required');
197
+ }
198
+ }
199
+ }
200
+
201
+ A_Context.root.register(TestComponent);
202
+
203
+ const stateMachine = new TestStateMachine();
204
+ A_Context.root.register(stateMachine);
205
+
206
+ await stateMachine.ready;
207
+
208
+ // Should fail without message
209
+ try {
210
+ await stateMachine.transition('idle', 'active', { message: '' })
211
+ } catch (error) {
212
+ expect(error).toBeInstanceOf(A_Error);
213
+ expect((error as A_Error).originalError.message).toBe('Message is required');
214
+ }
215
+
216
+
217
+ // Should succeed with message
218
+ await stateMachine.transition('idle', 'active', { message: 'Hello' });
219
+
220
+ expect(TestComponent.called).toBe(true);
221
+ });
222
+
223
+ describe('State Machine Lifecycle Tests', () => {
224
+ beforeEach(() => {
225
+ A_Context.reset();
226
+ });
227
+
228
+ it('Should properly initialize state machine', async () => {
229
+ let initializeCalled = false;
230
+
231
+ class InitTestMachine extends A_StateMachine {
232
+ @A_Feature.Extend()
233
+ async [A_StateMachineFeatures.onInitialize](): Promise<void> {
234
+ initializeCalled = true;
235
+ }
236
+ }
237
+
238
+ const machine = new InitTestMachine();
239
+ A_Context.root.register(machine);
240
+
241
+ expect(initializeCalled).toBe(false);
242
+
243
+ await machine.ready;
244
+
245
+ expect(initializeCalled).toBe(true);
246
+ });
247
+
248
+ it('Should call initialization only once', async () => {
249
+ let initializeCallCount = 0;
250
+
251
+ class OnceInitMachine extends A_StateMachine {
252
+ @A_Feature.Extend()
253
+ async [A_StateMachineFeatures.onInitialize](): Promise<void> {
254
+ initializeCallCount++;
255
+ }
256
+ }
257
+
258
+ const machine = new OnceInitMachine();
259
+ A_Context.root.register(machine);
260
+
261
+ await machine.ready;
262
+ await machine.ready;
263
+ await machine.ready;
264
+
265
+ expect(initializeCallCount).toBe(1);
266
+ });
267
+
268
+ it('Should handle initialization errors', async () => {
269
+ class FailingInitMachine extends A_StateMachine {
270
+ @A_Feature.Extend()
271
+ async [A_StateMachineFeatures.onInitialize](): Promise<void> {
272
+ throw new Error('Initialization failed');
273
+ }
274
+ }
275
+
276
+ const machine = new FailingInitMachine();
277
+ A_Context.root.register(machine);
278
+
279
+ try {
280
+ await machine.ready;
281
+ } catch (error) {
282
+ expect(error).toBeInstanceOf(Error);
283
+ expect((error as A_Error).originalError.message).toBe('Initialization failed');
284
+ }
285
+ });
286
+ });
287
+
288
+ describe('Transition Validation Tests', () => {
289
+ beforeEach(() => {
290
+ A_Context.reset();
291
+ });
292
+
293
+ it('Should validate transitions in beforeTransition hook', async () => {
294
+ interface ValidatedStates {
295
+ draft: { content: string };
296
+ published: { content: string; publishedAt: Date };
297
+ }
298
+
299
+ class ValidatedMachine extends A_StateMachine<ValidatedStates> {
300
+
301
+ @A_Feature.Extend()
302
+ async [A_StateMachineFeatures.onBeforeTransition](
303
+ @A_Inject(A_StateMachineTransition) transition: A_StateMachineTransition,
304
+ ): Promise<void> {
305
+ const { props } = transition.params;
306
+
307
+ if (transition.to === 'published' && (!props?.content || props.content.length < 10)) {
308
+ throw new Error('Content must be at least 10 characters to publish');
309
+ }
310
+ }
311
+ }
312
+
313
+ const machine = new ValidatedMachine();
314
+ A_Context.root.register(machine);
315
+ await machine.ready;
316
+
317
+ // Should fail with short content
318
+ try {
319
+ await machine.transition('draft', 'published', { content: 'short' })
320
+ } catch (error) {
321
+ expect(error).toBeInstanceOf(A_Error);
322
+ expect((error as A_Error).originalError.message).toBe('Content must be at least 10 characters to publish');
323
+ }
324
+
325
+
326
+
327
+ // Should succeed with long content
328
+ await machine.transition('draft', 'published', {
329
+ content: 'This is a long enough content for publishing',
330
+ publishedAt: new Date()
331
+ });
332
+ });
333
+
334
+ it('Should allow conditional transitions based on current state', async () => {
335
+ interface ConditionalStates {
336
+ pending: { orderId: string };
337
+ approved: { orderId: string; approvedBy: string };
338
+ rejected: { orderId: string; rejectedBy: string };
339
+ completed: { orderId: string; completedAt: Date };
340
+ }
341
+
342
+ class ConditionalMachine extends A_StateMachine<ConditionalStates> {
343
+ private currentState: keyof ConditionalStates = 'pending';
344
+
345
+ @A_Feature.Extend()
346
+ async [A_StateMachineFeatures.onBeforeTransition](
347
+ @A_Inject(A_StateMachineTransition) transition: A_StateMachineTransition
348
+ ): Promise<void> {
349
+
350
+ // Only approved orders can be completed
351
+ if (transition.to === 'completed' && transition.from !== 'approved') {
352
+ throw new A_Error(
353
+ 'Only approved orders can be completed',
354
+ `Unable to transition from ${transition.from} to ${transition.to}`
355
+ );
356
+ }
357
+ }
358
+
359
+ @A_Feature.Extend()
360
+ async [A_StateMachineFeatures.onAfterTransition](
361
+ @A_Inject(A_StateMachineTransition) transition: A_StateMachineTransition
362
+ ): Promise<void> {
363
+ this.currentState = transition.to as keyof ConditionalStates;
364
+ }
365
+
366
+ getCurrentState(): keyof ConditionalStates {
367
+ return this.currentState;
368
+ }
369
+ }
370
+
371
+ const machine = new ConditionalMachine();
372
+ A_Context.root.register(machine);
373
+ await machine.ready;
374
+
375
+ // Should fail to complete from pending
376
+
377
+ try {
378
+ await machine.transition('pending', 'completed', {
379
+ orderId: 'ORD-001',
380
+ completedAt: new Date()
381
+ })
382
+ } catch (error) {
383
+ expect(error).toBeInstanceOf(A_Error);
384
+ expect((error as A_Error).originalError.title).toBe('Only approved orders can be completed');
385
+
386
+ }
387
+
388
+ // Should succeed: pending -> approved -> completed
389
+ await machine.transition('pending', 'approved', {
390
+ orderId: 'ORD-001',
391
+ approvedBy: 'manager'
392
+ });
393
+
394
+ expect(machine.getCurrentState()).toBe('approved');
395
+
396
+ await machine.transition('approved', 'completed', {
397
+ orderId: 'ORD-001',
398
+ completedAt: new Date()
399
+ });
400
+
401
+ expect(machine.getCurrentState()).toBe('completed');
402
+ });
403
+ });
404
+
405
+ describe('Error Handling Tests', () => {
406
+ beforeEach(() => {
407
+ A_Context.reset();
408
+ });
409
+
410
+ it('Should call onError hook when transition fails', async () => {
411
+ let errorHandled = false;
412
+ let capturedError: A_StateMachineError | undefined;
413
+
414
+ interface ErrorStates {
415
+ start: { value: string };
416
+ end: { value: string };
417
+ }
418
+
419
+ class ErrorHandlingMachine extends A_StateMachine<ErrorStates> {
420
+ @A_Feature.Extend()
421
+ async [A_StateMachineFeatures.onError](): Promise<void> {
422
+ errorHandled = true;
423
+ // In real scenario, you might resolve the error from scope
424
+ // capturedError = scope.resolve(A_StateMachineError);
425
+ }
426
+
427
+ @A_Feature.Extend()
428
+ async start_end(): Promise<void> {
429
+ throw new Error('Transition failed');
430
+ }
431
+ }
432
+
433
+ const machine = new ErrorHandlingMachine();
434
+ A_Context.root.register(machine);
435
+ await machine.ready;
436
+
437
+ await expect(
438
+ machine.transition('start', 'end', { value: 'test' })
439
+ ).rejects.toThrow(A_StateMachineError);
440
+
441
+ expect(errorHandled).toBe(true);
442
+ });
443
+
444
+ it('Should wrap original errors in A_StateMachineError', async () => {
445
+ interface ErrorStates {
446
+ start: { value: string };
447
+ end: { value: string };
448
+ }
449
+
450
+ class ErrorWrappingMachine extends A_StateMachine<ErrorStates> {
451
+ async start_end(): Promise<void> {
452
+ throw new Error('Original error message');
453
+ }
454
+ }
455
+
456
+ const machine = new ErrorWrappingMachine();
457
+ A_Context.root.register(machine);
458
+ await machine.ready;
459
+
460
+ try {
461
+ await machine.transition('start', 'end', { value: 'test' });
462
+ } catch (error) {
463
+ expect(error).toBeInstanceOf(A_StateMachineError);
464
+ expect((error as A_StateMachineError).title).toBe(A_StateMachineError.TransitionError);
465
+ expect((error as A_StateMachineError).originalError).toBeInstanceOf(Error);
466
+ expect((error as A_StateMachineError).originalError.message).toBe('Original error message');
467
+ }
468
+ });
469
+
470
+ it('Should handle errors in lifecycle hooks', async () => {
471
+ interface HookErrorStates {
472
+ start: { value: string };
473
+ end: { value: string };
474
+ }
475
+
476
+ class HookErrorMachine extends A_StateMachine<HookErrorStates> {
477
+ @A_Feature.Extend()
478
+ async [A_StateMachineFeatures.onBeforeTransition](): Promise<void> {
479
+ throw new Error('Before transition error');
480
+ }
481
+ }
482
+
483
+ const machine = new HookErrorMachine();
484
+ A_Context.root.register(machine);
485
+ await machine.ready;
486
+
487
+ await expect(
488
+ machine.transition('start', 'end', { value: 'test' })
489
+ ).rejects.toThrow(A_StateMachineError);
490
+ });
491
+ });
492
+
493
+ describe('Complex Workflow Tests', () => {
494
+ beforeEach(() => {
495
+ A_Context.reset();
496
+ });
497
+
498
+ it('Should handle multi-step workflow with state tracking', async () => {
499
+ interface WorkflowStates {
500
+ created: { docId: string; author: string };
501
+ draft: { docId: string; author: string; content: string };
502
+ review: { docId: string; author: string; content: string; reviewer: string };
503
+ approved: { docId: string; author: string; content: string; approvedBy: string; approvedAt: Date };
504
+ published: { docId: string; author: string; content: string; publishedAt: Date; version: number };
505
+ }
506
+
507
+ class DocumentWorkflow extends A_StateMachine<WorkflowStates> {
508
+ private history: Array<{ from: string; to: string; timestamp: Date }> = [];
509
+
510
+ @A_Feature.Extend()
511
+ async [A_StateMachineFeatures.onAfterTransition](
512
+ @A_Inject(A_StateMachineTransition) transition: A_StateMachineTransition,
513
+ ): Promise<void> {
514
+
515
+ this.history.push({
516
+ from: transition.from,
517
+ to: transition.to,
518
+ timestamp: new Date()
519
+ });
520
+ }
521
+
522
+ @A_Feature.Extend()
523
+ async created_draft(
524
+ @A_Inject(A_StateMachineTransition) transition: A_StateMachineTransition,
525
+ ): Promise<void> {
526
+ const data = transition.params.props;
527
+
528
+ transition.succeed({
529
+ ...data,
530
+ content: 'Initial draft content'
531
+ });
532
+ }
533
+
534
+ @A_Feature.Extend()
535
+ async draft_review(
536
+ @A_Inject(A_StateMachineTransition) transition: A_StateMachineTransition,
537
+ ): Promise<void> {
538
+ const data = transition.params.props;
539
+
540
+ if (!data.content || data.content.length < 10) {
541
+ throw new Error('Content too short for review');
542
+ }
543
+
544
+ transition.succeed({
545
+ ...data,
546
+ reviewer: 'reviewer@example.com'
547
+ });
548
+ }
549
+
550
+ @A_Feature.Extend()
551
+ async review_approved(
552
+ @A_Inject(A_StateMachineTransition) transition: A_StateMachineTransition,
553
+ ): Promise<void> {
554
+ const data = transition.params.props;
555
+
556
+ transition.succeed({
557
+ docId: data.docId,
558
+ author: data.author,
559
+ content: data.content,
560
+ approvedBy: data.reviewer,
561
+ approvedAt: new Date()
562
+ });
563
+ }
564
+
565
+ @A_Feature.Extend()
566
+ async approved_published(
567
+ @A_Inject(A_StateMachineTransition) transition: A_StateMachineTransition,
568
+ ): Promise<void> {
569
+ const data = transition.params.props;
570
+
571
+ transition.succeed({
572
+ docId: data.docId,
573
+ author: data.author,
574
+ content: data.content,
575
+ publishedAt: new Date(),
576
+ version: 1
577
+ });
578
+ }
579
+
580
+ getHistory() {
581
+ return this.history;
582
+ }
583
+ }
584
+
585
+ const workflow = new DocumentWorkflow();
586
+ A_Context.root.register(workflow);
587
+ await workflow.ready;
588
+
589
+ const docData = {
590
+ docId: 'DOC-001',
591
+ author: 'author@example.com'
592
+ };
593
+
594
+ // Execute complete workflow
595
+ await workflow.transition('created', 'draft', docData);
596
+
597
+ await workflow.transition('draft', 'review', {
598
+ ...docData,
599
+ content: 'This is a comprehensive document with enough content for review'
600
+ });
601
+
602
+ await workflow.transition('review', 'approved', {
603
+ ...docData,
604
+ content: 'This is a comprehensive document with enough content for review',
605
+ reviewer: 'reviewer@example.com'
606
+ });
607
+
608
+ await workflow.transition('approved', 'published', {
609
+ docId: 'DOC-001',
610
+ author: 'author@example.com',
611
+ content: 'This is a comprehensive document with enough content for review',
612
+ approvedBy: 'reviewer@example.com',
613
+ approvedAt: new Date()
614
+ });
615
+
616
+ const history = workflow.getHistory();
617
+ expect(history).toHaveLength(4);
618
+ expect(history[0].from).toBe('created');
619
+ expect(history[0].to).toBe('draft');
620
+ expect(history[3].from).toBe('approved');
621
+ expect(history[3].to).toBe('published');
622
+ });
623
+
624
+ it('Should handle parallel state machines with shared components', async () => {
625
+ A_Context.reset();
626
+
627
+ interface SharedStates {
628
+ init: { id: string };
629
+ processed: { id: string; processedAt: Date };
630
+ }
631
+
632
+ class SharedMachine extends A_StateMachine<SharedStates> { }
633
+
634
+ class SharedLogger extends A_Component {
635
+ static logs: string[] = [];
636
+
637
+ @A_Feature.Extend({ scope: [SharedMachine] })
638
+ async [A_StateMachineFeatures.onAfterTransition](
639
+ @A_Inject(A_StateMachineTransition) transition: A_StateMachineTransition,
640
+ ): Promise<void> {
641
+ const { from, to, props } = transition.params;
642
+ SharedLogger.logs.push(`${props.id}: ${String(from)} -> ${String(to)}`);
643
+ }
644
+ }
645
+
646
+ A_Context.root.register(SharedLogger);
647
+
648
+ const machine1 = new SharedMachine();
649
+ const machine2 = new SharedMachine();
650
+
651
+ A_Context.root.register(machine1);
652
+ A_Context.root.register(machine2);
653
+
654
+ await Promise.all([machine1.ready, machine2.ready]);
655
+
656
+ await Promise.all([
657
+ machine1.transition('init', 'processed', { id: 'machine1', processedAt: new Date() }),
658
+ machine2.transition('init', 'processed', { id: 'machine2', processedAt: new Date() })
659
+ ]);
660
+
661
+ expect(SharedLogger.logs).toHaveLength(2);
662
+ expect(SharedLogger.logs).toContain('machine1: init -> processed');
663
+ expect(SharedLogger.logs).toContain('machine2: init -> processed');
664
+ });
665
+ });
666
+
667
+ describe('Operation Context Tests', () => {
668
+ beforeEach(() => {
669
+ A_Context.reset();
670
+ });
671
+
672
+ it('Should provide correct operation context in transitions', async () => {
673
+ interface ContextStates {
674
+ start: { userId: string; action: string };
675
+ end: { userId: string; action: string; result: string };
676
+ }
677
+
678
+ class ContextMachine extends A_StateMachine<ContextStates> {
679
+ async start_end(scope: A_Scope): Promise<void> {
680
+ const operation = scope.resolve(A_OperationContext)!;
681
+
682
+ expect(operation).toBeDefined();
683
+ expect(operation.name).toBe('start_end');
684
+ expect(operation.params).toBeDefined();
685
+ expect(operation.params.from).toBe('start');
686
+ expect(operation.params.to).toBe('end');
687
+ expect(operation.params.props).toEqual({
688
+ userId: 'user123',
689
+ action: 'test'
690
+ });
691
+
692
+ operation.succeed({
693
+ userId: operation.params.props.userId,
694
+ action: operation.params.props.action,
695
+ result: 'success'
696
+ });
697
+ }
698
+ }
699
+
700
+ const machine = new ContextMachine();
701
+ A_Context.root.register(machine);
702
+ await machine.ready;
703
+
704
+ await machine.transition('start', 'end', {
705
+ userId: 'user123',
706
+ action: 'test'
707
+ });
708
+ });
709
+
710
+ it('Should handle operation results correctly', async () => {
711
+ interface ResultStates {
712
+ input: { data: string };
713
+ output: { data: string; processed: boolean };
714
+ }
715
+
716
+ let capturedResult: any;
717
+
718
+ class ResultMachine extends A_StateMachine<ResultStates> {
719
+
720
+ @A_Feature.Extend()
721
+ async [A_StateMachineFeatures.onAfterTransition](
722
+ @A_Inject(A_Scope) scope: A_Scope,
723
+ @A_Dependency.Required()
724
+ @A_Inject(A_StateMachineTransition) transition: A_StateMachineTransition
725
+ ): Promise<void> {
726
+ capturedResult = transition.result;
727
+ }
728
+
729
+ @A_Feature.Extend()
730
+ async [A_StateMachineFeatures.onBeforeTransition](...args: any[]): Promise<void> {
731
+ }
732
+
733
+ @A_Feature.Extend()
734
+ async input_output(
735
+ @A_Inject(A_Scope) scope: A_Scope,
736
+ @A_Inject(A_StateMachineTransition) transition: A_StateMachineTransition
737
+ ): Promise<void> {
738
+ transition.succeed({
739
+ data: transition.params.props.data.toUpperCase(),
740
+ processed: true
741
+ });
742
+
743
+ }
744
+ }
745
+
746
+ const scope = new A_Scope({
747
+ components: [ResultMachine]
748
+ });
749
+
750
+ const machine = scope.resolve(ResultMachine)!;
751
+
752
+ await machine.transition('input', 'output', { data: 'hello world' });
753
+
754
+ expect(capturedResult).toEqual({
755
+ data: 'HELLO WORLD',
756
+ processed: true
757
+ });
758
+ });
759
+ });
760
+ });