@adaas/a-utils 0.1.13 → 0.1.15

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.
@@ -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_ChannelRequest } 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_ChannelRequest);
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_ChannelRequest) context: A_ChannelRequest
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_ChannelRequest) context: A_ChannelRequest
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_ChannelRequest) context: A_ChannelRequest
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_ChannelRequest) context: A_ChannelRequest
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_ChannelRequest) context: A_ChannelRequest<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_ChannelRequest) context: A_ChannelRequest
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_ChannelRequest) context: A_ChannelRequest
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_ChannelRequest) context: A_ChannelRequest
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_ChannelRequest) context: A_ChannelRequest
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_ChannelRequest) context: A_ChannelRequest
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_ChannelRequest) context: A_ChannelRequest
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_ChannelRequest) context: A_ChannelRequest
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
  });