@adaas/a-utils 0.1.12 → 0.1.14

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.
@@ -9,6 +9,7 @@ import {
9
9
  } from "./A-Command.constants";
10
10
  import { A_Context, A_Entity, A_Error, A_Scope } from "@adaas/a-concept";
11
11
  import { A_Memory } from "../A-Memory/A-Memory.context";
12
+ import { A_CommandError } from "./A-Command.error";
12
13
 
13
14
 
14
15
  export class A_Command<
@@ -155,9 +156,8 @@ export class A_Command<
155
156
 
156
157
  this._status = A_CONSTANTS__A_Command_Status.INITIALIZATION;
157
158
  this._startTime = new Date();
158
- if (!this.scope.isInheritedFrom(A_Context.scope(this))) {
159
- this.scope.inherit(A_Context.scope(this));
160
- }
159
+
160
+ this.checkScopeInheritance();
161
161
 
162
162
  this.emit('init');
163
163
  await this.call('init', this.scope);
@@ -170,24 +170,42 @@ export class A_Command<
170
170
  return;
171
171
  }
172
172
 
173
+ this.checkScopeInheritance();
174
+
173
175
  this._status = A_CONSTANTS__A_Command_Status.COMPILATION;
174
176
  this.emit('compile');
175
177
  await this.call('compile', this.scope);
176
178
  this._status = A_CONSTANTS__A_Command_Status.COMPILED;
177
179
  }
178
180
 
181
+ /**
182
+ * Processes the command execution
183
+ *
184
+ * @returns
185
+ */
186
+ async process() {
187
+ if (this._status !== A_CONSTANTS__A_Command_Status.COMPILED)
188
+ return;
189
+
190
+ this._status = A_CONSTANTS__A_Command_Status.IN_PROGRESS;
191
+
192
+ this.checkScopeInheritance();
193
+
194
+ this.emit('execute');
195
+
196
+ await this.call('execute', this.scope);
197
+ }
198
+
179
199
  /**
180
200
  * Executes the command logic.
181
201
  */
182
202
  async execute(): Promise<any> {
203
+ this.checkScopeInheritance();
204
+
183
205
  try {
184
206
  await this.init();
185
207
  await this.compile();
186
-
187
- if (this._status === A_CONSTANTS__A_Command_Status.COMPILED) {
188
- this.emit('execute');
189
- await this.call('execute', this.scope);
190
- }
208
+ await this.process();
191
209
  await this.complete();
192
210
 
193
211
  } catch (error) {
@@ -199,6 +217,8 @@ export class A_Command<
199
217
  * Marks the command as completed
200
218
  */
201
219
  async complete() {
220
+ this.checkScopeInheritance();
221
+
202
222
  this._status = A_CONSTANTS__A_Command_Status.COMPLETED;
203
223
  this._endTime = new Date();
204
224
  this._result = this.scope.resolve(A_Memory).toJSON() as ResultType;
@@ -212,6 +232,8 @@ export class A_Command<
212
232
  * Marks the command as failed
213
233
  */
214
234
  async fail() {
235
+ this.checkScopeInheritance();
236
+
215
237
  this._status = A_CONSTANTS__A_Command_Status.FAILED;
216
238
  this._endTime = new Date();
217
239
  this._errors = this.scope.resolve(A_Memory).Errors;
@@ -342,4 +364,23 @@ export class A_Command<
342
364
  errors: this.errors ? Array.from(this.errors).map(err => err.toJSON()) : undefined
343
365
  }
344
366
  };
367
+
368
+
369
+ protected checkScopeInheritance(): void {
370
+ let attachedScope: A_Scope;
371
+ try {
372
+ attachedScope = A_Context.scope(this);
373
+ } catch (error) {
374
+ throw new A_CommandError({
375
+ title: A_CommandError.CommandScopeBindingError,
376
+ description: `Command ${this.code} is not bound to any context scope. Ensure the command is properly registered within a context before execution.`,
377
+ originalError: error
378
+ });
379
+ }
380
+
381
+ if (!this.scope.isInheritedFrom(A_Context.scope(this))) {
382
+ this.scope.inherit(A_Context.scope(this));
383
+ }
384
+ }
385
+
345
386
  }
@@ -3,4 +3,6 @@ import { A_Error } from "@adaas/a-concept";
3
3
 
4
4
  export class A_CommandError extends A_Error {
5
5
 
6
+
7
+ static readonly CommandScopeBindingError = 'A-Command Scope Binding Error';
6
8
  }
@@ -1,16 +1,494 @@
1
1
  import './jest.setup';
2
- import { A_Context } from '@adaas/a-concept';
2
+ import { A_Context, A_Component, A_Feature, A_Inject } from '@adaas/a-concept';
3
+ import { A_Channel } from '@adaas/a-utils/lib/A-Channel/A-Channel.component';
4
+ import { A_ChannelFeatures } from '@adaas/a-utils/lib/A-Channel/A-Channel.constants';
5
+ import { A_ChannelRequestContext } from '@adaas/a-utils/lib/A-Channel/A-ChannelRequest.context';
6
+ import { A_ChannelError } from '@adaas/a-utils/lib/A-Channel/A-Channel.error';
3
7
 
4
8
  jest.retryTimes(0);
5
9
 
6
10
  describe('A-Channel tests', () => {
7
11
 
8
- it('Should Allow to create a channel', async () => {
9
- const { A_Channel } = await import('@adaas/a-utils/lib/A-Channel/A-Channel.component');
12
+ beforeEach(() => {
13
+ A_Context.reset();
14
+ });
15
+
16
+ describe('Basic Channel Creation and Properties', () => {
17
+ it('Should allow to create a channel', async () => {
18
+ const channel = new A_Channel();
19
+ A_Context.root.register(channel);
20
+
21
+ expect(channel).toBeInstanceOf(A_Channel);
22
+ expect(channel).toBeInstanceOf(A_Component);
23
+ expect(channel.processing).toBe(false);
24
+ });
25
+
26
+ it('Should have correct initial state', async () => {
27
+ const channel = new A_Channel();
28
+ A_Context.root.register(channel);
29
+
30
+ expect(channel.processing).toBe(false);
31
+ expect(channel.initialize).toBeInstanceOf(Promise);
32
+ });
33
+
34
+ it('Should initialize only once', async () => {
35
+ const channel = new A_Channel();
36
+ A_Context.root.register(channel);
37
+
38
+ const init1 = channel.initialize;
39
+ const init2 = channel.initialize;
40
+
41
+ expect(init1).toBe(init2); // Same promise instance
42
+ await init1;
43
+ await init2;
44
+ });
45
+ });
46
+
47
+ describe('Channel Connection Lifecycle', () => {
48
+ it('Should handle connection lifecycle', async () => {
49
+ const channel = new A_Channel();
50
+ A_Context.root.register(channel);
51
+
52
+ // Should connect successfully
53
+ await channel.connect();
54
+ expect(channel.initialize).toBeInstanceOf(Promise);
55
+ await channel.initialize;
56
+
57
+ // Should disconnect successfully
58
+ await channel.disconnect();
59
+ });
60
+
61
+ it('Should support custom connection logic', async () => {
62
+ const connectCalls: string[] = [];
63
+ const disconnectCalls: string[] = [];
64
+
65
+ class CustomChannel extends A_Channel {}
66
+
67
+ class ChannelConnector extends A_Component {
68
+ @A_Feature.Extend({ scope: [CustomChannel] })
69
+ async [A_ChannelFeatures.onConnect]() {
70
+ connectCalls.push('connected');
71
+ }
72
+
73
+ @A_Feature.Extend({ scope: [CustomChannel] })
74
+ async [A_ChannelFeatures.onDisconnect]() {
75
+ disconnectCalls.push('disconnected');
76
+ }
77
+ }
78
+
79
+ A_Context.root.register(ChannelConnector);
80
+
81
+ const channel = new CustomChannel();
82
+ A_Context.root.register(channel);
83
+
84
+ await channel.connect();
85
+ expect(connectCalls).toEqual(['connected']);
86
+
87
+ await channel.disconnect();
88
+ expect(disconnectCalls).toEqual(['disconnected']);
89
+ });
90
+ });
91
+
92
+ describe('Request Processing', () => {
93
+ it('Should handle basic request', async () => {
94
+ const channel = new A_Channel();
95
+ A_Context.root.register(channel);
96
+
97
+ const params = { action: 'test', data: 'hello' };
98
+ const context = await channel.request(params);
99
+
100
+ expect(context).toBeInstanceOf(A_ChannelRequestContext);
101
+ expect(context.params).toEqual(params);
102
+ expect(channel.processing).toBe(false); // Should reset after processing
103
+ });
104
+
105
+ it('Should handle request with custom logic', async () => {
106
+ const processingOrder: string[] = [];
107
+
108
+ class TestChannel extends A_Channel {}
109
+
110
+ class RequestProcessor extends A_Component {
111
+ @A_Feature.Extend({ scope: [TestChannel] })
112
+ async [A_ChannelFeatures.onBeforeRequest](
113
+ @A_Inject(A_ChannelRequestContext) context: A_ChannelRequestContext
114
+ ) {
115
+ processingOrder.push('before');
116
+ expect(context.params.action).toBe('test');
117
+ }
118
+
119
+ @A_Feature.Extend({ scope: [TestChannel] })
120
+ async [A_ChannelFeatures.onRequest](
121
+ @A_Inject(A_ChannelRequestContext) context: A_ChannelRequestContext
122
+ ) {
123
+ processingOrder.push('during');
124
+ // Simulate processing and setting result
125
+ (context as any)._result = { processed: true, original: context.params };
126
+ }
127
+
128
+ @A_Feature.Extend({ scope: [TestChannel] })
129
+ async [A_ChannelFeatures.onAfterRequest](
130
+ @A_Inject(A_ChannelRequestContext) context: A_ChannelRequestContext
131
+ ) {
132
+ processingOrder.push('after');
133
+ expect(context.data).toBeDefined();
134
+ }
135
+ }
136
+
137
+ A_Context.root.register(RequestProcessor);
138
+
139
+ const channel = new TestChannel();
140
+ A_Context.root.register(channel);
141
+
142
+ const params = { action: 'test', data: 'hello' };
143
+ const context = await channel.request(params);
144
+
145
+ expect(processingOrder).toEqual(['before', 'during', 'after']);
146
+ expect(context.data).toEqual({ processed: true, original: params });
147
+ });
148
+
149
+ it('Should handle request errors gracefully', async () => {
150
+ const errorCalls: string[] = [];
151
+
152
+ class ErrorChannel extends A_Channel {}
153
+
154
+ class ErrorProcessor extends A_Component {
155
+ @A_Feature.Extend({ scope: [ErrorChannel] })
156
+ async [A_ChannelFeatures.onRequest]() {
157
+ throw new Error('Request processing failed');
158
+ }
159
+
160
+ @A_Feature.Extend({ scope: [ErrorChannel] })
161
+ async [A_ChannelFeatures.onError](
162
+ @A_Inject(A_ChannelRequestContext) context: A_ChannelRequestContext
163
+ ) {
164
+ errorCalls.push('error-handled');
165
+ expect(context.failed).toBe(true);
166
+ }
167
+ }
168
+
169
+ A_Context.root.register(ErrorProcessor);
170
+
171
+ const channel = new ErrorChannel();
172
+ A_Context.root.register(channel);
173
+
174
+ const params = { action: 'fail' };
175
+ const context = await channel.request(params);
176
+
177
+ expect(errorCalls).toEqual(['error-handled']);
178
+ expect(context.failed).toBe(true);
179
+ expect(channel.processing).toBe(false); // Should reset even on error
180
+ });
181
+
182
+ it('Should support typed requests', async () => {
183
+ interface TestParams {
184
+ userId: string;
185
+ action: 'create' | 'update' | 'delete';
186
+ }
187
+
188
+ interface TestResult {
189
+ success: boolean;
190
+ userId: string;
191
+ timestamp: string;
192
+ }
193
+
194
+ class TypedChannel extends A_Channel {}
195
+
196
+ class TypedProcessor extends A_Component {
197
+ @A_Feature.Extend({ scope: [TypedChannel] })
198
+ async [A_ChannelFeatures.onRequest](
199
+ @A_Inject(A_ChannelRequestContext) context: A_ChannelRequestContext<TestParams, TestResult>
200
+ ) {
201
+ const { userId, action } = context.params;
202
+ (context as any)._result = {
203
+ success: true,
204
+ userId,
205
+ timestamp: new Date().toISOString()
206
+ };
207
+ }
208
+ }
209
+
210
+ A_Context.root.register(TypedProcessor);
211
+
212
+ const channel = new TypedChannel();
213
+ A_Context.root.register(channel);
214
+
215
+ const params: TestParams = { userId: '123', action: 'create' };
216
+ const context = await channel.request<TestParams, TestResult>(params);
217
+
218
+ expect(context.params.userId).toBe('123');
219
+ expect(context.params.action).toBe('create');
220
+ expect(context.data?.success).toBe(true);
221
+ expect(context.data?.userId).toBe('123');
222
+ expect(context.data?.timestamp).toBeDefined();
223
+ });
224
+ });
225
+
226
+ describe('Send (Fire-and-Forget) Operations', () => {
227
+ it('Should handle basic send operation', async () => {
228
+ const channel = new A_Channel();
229
+ A_Context.root.register(channel);
230
+
231
+ const message = { type: 'notification', content: 'Hello World' };
232
+
233
+ // Should not throw
234
+ await expect(channel.send(message)).resolves.not.toThrow();
235
+ expect(channel.processing).toBe(false);
236
+ });
237
+
238
+ it('Should handle send with custom logic', async () => {
239
+ const sentMessages: any[] = [];
240
+
241
+ class SendChannel extends A_Channel {}
10
242
 
11
- const channel = new A_Channel();
243
+ class SendProcessor extends A_Component {
244
+ @A_Feature.Extend({ scope: [SendChannel] })
245
+ async [A_ChannelFeatures.onSend](
246
+ @A_Inject(A_ChannelRequestContext) context: A_ChannelRequestContext
247
+ ) {
248
+ sentMessages.push(context.params);
249
+ }
250
+ }
12
251
 
13
- const meta = A_Context.meta(channel);
252
+ A_Context.root.register(SendProcessor);
253
+
254
+ const channel = new SendChannel();
255
+ A_Context.root.register(channel);
256
+
257
+ const message1 = { type: 'email', to: 'user@example.com' };
258
+ const message2 = { type: 'sms', to: '+1234567890' };
259
+
260
+ await channel.send(message1);
261
+ await channel.send(message2);
262
+
263
+ expect(sentMessages).toHaveLength(2);
264
+ expect(sentMessages[0]).toEqual(message1);
265
+ expect(sentMessages[1]).toEqual(message2);
266
+ });
267
+
268
+ it('Should handle send errors gracefully', async () => {
269
+ const errorCalls: string[] = [];
270
+
271
+ class ErrorSendChannel extends A_Channel {}
272
+
273
+ class ErrorSendProcessor extends A_Component {
274
+ @A_Feature.Extend({ scope: [ErrorSendChannel] })
275
+ async [A_ChannelFeatures.onSend]() {
276
+ throw new Error('Send operation failed');
277
+ }
278
+
279
+ @A_Feature.Extend({ scope: [ErrorSendChannel] })
280
+ async [A_ChannelFeatures.onError](
281
+ @A_Inject(A_ChannelRequestContext) context: A_ChannelRequestContext
282
+ ) {
283
+ errorCalls.push('send-error-handled');
284
+ expect(context.failed).toBe(true);
285
+ }
286
+ }
287
+
288
+ A_Context.root.register(ErrorSendProcessor);
289
+
290
+ const channel = new ErrorSendChannel();
291
+ A_Context.root.register(channel);
292
+
293
+ const message = { type: 'failing-message' };
294
+
295
+ // Should not throw, errors are handled internally
296
+ await expect(channel.send(message)).resolves.not.toThrow();
297
+ expect(errorCalls).toEqual(['send-error-handled']);
298
+ expect(channel.processing).toBe(false);
299
+ });
300
+ });
301
+
302
+ describe('Error Handling', () => {
303
+ it('Should create proper channel errors', async () => {
304
+ const originalError = new Error('Original error message');
305
+ const channelError = new A_ChannelError(originalError);
306
+
307
+ expect(channelError).toBeInstanceOf(A_ChannelError);
308
+ expect(channelError.message).toContain('Original error message');
309
+ });
310
+
311
+ it('Should handle multiple error types', async () => {
312
+ const errorTypes: string[] = [];
313
+
314
+ class MultiErrorChannel extends A_Channel {}
315
+
316
+ class MultiErrorProcessor extends A_Component {
317
+ @A_Feature.Extend({ scope: [MultiErrorChannel] })
318
+ async [A_ChannelFeatures.onRequest](
319
+ @A_Inject(A_ChannelRequestContext) context: A_ChannelRequestContext
320
+ ) {
321
+ const errorType = context.params.errorType;
322
+ switch (errorType) {
323
+ case 'network':
324
+ throw new Error('Network error');
325
+ case 'validation':
326
+ throw new Error('Validation error');
327
+ case 'timeout':
328
+ throw new Error('Timeout error');
329
+ default:
330
+ // No error
331
+ }
332
+ }
333
+
334
+ @A_Feature.Extend({ scope: [MultiErrorChannel] })
335
+ async [A_ChannelFeatures.onError](
336
+ @A_Inject(A_ChannelRequestContext) context: A_ChannelRequestContext
337
+ ) {
338
+ errorTypes.push(context.params.errorType);
339
+ }
340
+ }
341
+
342
+ A_Context.root.register(MultiErrorProcessor);
343
+
344
+ const channel = new MultiErrorChannel();
345
+ A_Context.root.register(channel);
346
+
347
+ await channel.request({ errorType: 'network' });
348
+ await channel.request({ errorType: 'validation' });
349
+ await channel.request({ errorType: 'timeout' });
350
+ await channel.request({ errorType: 'none' }); // Should not error
351
+
352
+ expect(errorTypes).toEqual(['network', 'validation', 'timeout']);
353
+ });
14
354
  });
15
355
 
356
+ describe('Channel Integration and Extension', () => {
357
+ it('Should support multiple channel instances', async () => {
358
+ const channel1 = new A_Channel();
359
+ const channel2 = new A_Channel();
360
+
361
+ A_Context.root.register(channel1);
362
+ A_Context.root.register(channel2);
363
+
364
+ // Both should work independently
365
+ const result1 = await channel1.request({ id: 1 });
366
+ const result2 = await channel2.request({ id: 2 });
367
+
368
+ expect(result1.params.id).toBe(1);
369
+ expect(result2.params.id).toBe(2);
370
+ });
371
+
372
+ it('Should support channel inheritance', async () => {
373
+ class HttpChannel extends A_Channel {
374
+ async makeHttpRequest(url: string, method: string = 'GET') {
375
+ return this.request({ url, method, timestamp: Date.now() });
376
+ }
377
+ }
378
+
379
+ class WebSocketChannel extends A_Channel {
380
+ async sendMessage(message: string) {
381
+ return this.send({ message, type: 'websocket', timestamp: Date.now() });
382
+ }
383
+ }
384
+
385
+ const httpChannel = new HttpChannel();
386
+ const wsChannel = new WebSocketChannel();
387
+
388
+ A_Context.root.register(httpChannel);
389
+ A_Context.root.register(wsChannel);
390
+
391
+ const httpResult = await httpChannel.makeHttpRequest('https://api.example.com');
392
+ expect(httpResult.params.url).toBe('https://api.example.com');
393
+ expect(httpResult.params.method).toBe('GET');
394
+
395
+ await expect(wsChannel.sendMessage('Hello WebSocket')).resolves.not.toThrow();
396
+ });
397
+
398
+ it('Should support feature extension with different channel types', async () => {
399
+ const httpCalls: string[] = [];
400
+ const wsCalls: string[] = [];
401
+
402
+ class HttpChannel extends A_Channel {}
403
+ class WebSocketChannel extends A_Channel {}
404
+
405
+ class HttpProcessor extends A_Component {
406
+ @A_Feature.Extend({ scope: [HttpChannel] })
407
+ async [A_ChannelFeatures.onRequest](
408
+ @A_Inject(A_ChannelRequestContext) context: A_ChannelRequestContext
409
+ ) {
410
+ httpCalls.push(`HTTP: ${context.params.method} ${context.params.url}`);
411
+ }
412
+ }
413
+
414
+ class WebSocketProcessor extends A_Component {
415
+ @A_Feature.Extend({ scope: [WebSocketChannel] })
416
+ async [A_ChannelFeatures.onSend](
417
+ @A_Inject(A_ChannelRequestContext) context: A_ChannelRequestContext
418
+ ) {
419
+ wsCalls.push(`WS: ${context.params.message}`);
420
+ }
421
+ }
422
+
423
+ A_Context.root.register(HttpProcessor);
424
+ A_Context.root.register(WebSocketProcessor);
425
+
426
+ const httpChannel = new HttpChannel();
427
+ const wsChannel = new WebSocketChannel();
428
+
429
+ A_Context.root.register(httpChannel);
430
+ A_Context.root.register(wsChannel);
431
+
432
+ await httpChannel.request({ method: 'POST', url: '/api/users' });
433
+ await wsChannel.send({ message: 'Hello World' });
434
+
435
+ expect(httpCalls).toEqual(['HTTP: POST /api/users']);
436
+ expect(wsCalls).toEqual(['WS: Hello World']);
437
+ });
438
+ });
439
+
440
+ describe('Performance and Concurrency', () => {
441
+ it('Should handle concurrent requests', async () => {
442
+ const processingOrder: number[] = [];
443
+
444
+ class ConcurrentChannel extends A_Channel {}
445
+
446
+ class ConcurrentProcessor extends A_Component {
447
+ @A_Feature.Extend({ scope: [ConcurrentChannel] })
448
+ async [A_ChannelFeatures.onRequest](
449
+ @A_Inject(A_ChannelRequestContext) context: A_ChannelRequestContext
450
+ ) {
451
+ const delay = context.params.delay || 0;
452
+ await new Promise(resolve => setTimeout(resolve, delay));
453
+ processingOrder.push(context.params.id);
454
+ (context as any)._result = { processed: context.params.id };
455
+ }
456
+ }
457
+
458
+ A_Context.root.register(ConcurrentProcessor);
459
+
460
+ const channel = new ConcurrentChannel();
461
+ A_Context.root.register(channel);
462
+
463
+ // Start multiple requests concurrently
464
+ const requests = [
465
+ channel.request({ id: 1, delay: 100 }),
466
+ channel.request({ id: 2, delay: 50 }),
467
+ channel.request({ id: 3, delay: 25 })
468
+ ];
469
+
470
+ const results = await Promise.all(requests);
471
+
472
+ // Results should be in completion order (3, 2, 1 due to delays)
473
+ expect(processingOrder).toEqual([3, 2, 1]);
474
+ expect(results[0].data?.processed).toBe(1);
475
+ expect(results[1].data?.processed).toBe(2);
476
+ expect(results[2].data?.processed).toBe(3);
477
+ });
478
+
479
+ it('Should handle processing state correctly during concurrent operations', async () => {
480
+ const channel = new A_Channel();
481
+ A_Context.root.register(channel);
482
+
483
+ const request1Promise = channel.request({ id: 1 });
484
+ const request2Promise = channel.request({ id: 2 });
485
+
486
+ // Both requests should complete
487
+ const [result1, result2] = await Promise.all([request1Promise, request2Promise]);
488
+
489
+ expect(result1.params.id).toBe(1);
490
+ expect(result2.params.id).toBe(2);
491
+ expect(channel.processing).toBe(false); // Should be false after all complete
492
+ });
493
+ });
16
494
  });