@geekmidas/services 9.0.2 → 10.0.0-alpha.0

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,564 +0,0 @@
1
- import { EnvironmentParser } from '@geekmidas/envkit';
2
- import { ConsoleLogger } from '@geekmidas/logger/console';
3
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4
- import { serviceContext } from '../context';
5
- import type { Service } from '../index';
6
- import { ServiceDiscovery } from '../index';
7
-
8
- describe('ServiceDiscovery', () => {
9
- let _logger: ConsoleLogger;
10
- let envParser: EnvironmentParser<{}>;
11
-
12
- beforeEach(() => {
13
- _logger = new ConsoleLogger({ app: 'test' });
14
- envParser = new EnvironmentParser({ ...process.env });
15
-
16
- // Reset singleton between tests
17
- (ServiceDiscovery as any)._instance = undefined;
18
- });
19
-
20
- afterEach(() => {
21
- vi.clearAllMocks();
22
- });
23
-
24
- describe('Singleton Pattern', () => {
25
- it('should return same instance on multiple calls', () => {
26
- const instance1 = ServiceDiscovery.getInstance(envParser);
27
- const instance2 = ServiceDiscovery.getInstance(envParser);
28
-
29
- expect(instance1).toBe(instance2);
30
- });
31
-
32
- it('should create instance only once', () => {
33
- const instance1 = ServiceDiscovery.getInstance(envParser);
34
- const instance2 = ServiceDiscovery.getInstance(envParser);
35
- const instance3 = ServiceDiscovery.getInstance(envParser);
36
-
37
- expect(instance1).toBe(instance2);
38
- expect(instance2).toBe(instance3);
39
- });
40
-
41
- it('should provide access to envParser', () => {
42
- const discovery = ServiceDiscovery.getInstance(envParser);
43
-
44
- expect(discovery.envParser).toBe(envParser);
45
- });
46
- });
47
-
48
- describe('Service Registration', () => {
49
- it('should register a single service', async () => {
50
- const mockService = {
51
- serviceName: 'database' as const,
52
- async register() {
53
- return { connected: true };
54
- },
55
- } satisfies Service<'database', { connected: boolean }>;
56
-
57
- const discovery = ServiceDiscovery.getInstance(envParser);
58
- const services = await discovery.register([mockService]);
59
-
60
- expect(services).toHaveProperty('database');
61
- expect(services.database).toEqual({ connected: true });
62
- });
63
-
64
- it('should register multiple services', async () => {
65
- const databaseService = {
66
- serviceName: 'database' as const,
67
- async register() {
68
- return { connected: true };
69
- },
70
- } satisfies Service<'database', { connected: boolean }>;
71
-
72
- const cacheService = {
73
- serviceName: 'cache' as const,
74
- async register() {
75
- return { ready: true };
76
- },
77
- } satisfies Service<'cache', { ready: boolean }>;
78
-
79
- const discovery = ServiceDiscovery.getInstance(envParser);
80
- const services = await discovery.register([
81
- databaseService,
82
- cacheService,
83
- ]);
84
-
85
- expect(services).toHaveProperty('database');
86
- expect(services).toHaveProperty('cache');
87
- expect(services.database).toEqual({ connected: true });
88
- expect(services.cache).toEqual({ ready: true });
89
- });
90
-
91
- it('should call register method with envParser and context', async () => {
92
- const registerSpy = vi.fn().mockResolvedValue({ data: 'test' });
93
-
94
- const mockService = {
95
- serviceName: 'test' as const,
96
- register: registerSpy,
97
- } satisfies Service<'test', { data: string }>;
98
-
99
- const discovery = ServiceDiscovery.getInstance(envParser);
100
- await discovery.register([mockService]);
101
-
102
- expect(registerSpy).toHaveBeenCalledWith({
103
- envParser,
104
- context: serviceContext,
105
- });
106
- expect(registerSpy).toHaveBeenCalledTimes(1);
107
- });
108
-
109
- it('should handle synchronous register methods', async () => {
110
- const mockService = {
111
- serviceName: 'sync' as const,
112
- register() {
113
- return { value: 42 };
114
- },
115
- } satisfies Service<'sync', { value: number }>;
116
-
117
- const discovery = ServiceDiscovery.getInstance(envParser);
118
- const services = await discovery.register([mockService]);
119
-
120
- expect(services.sync).toEqual({ value: 42 });
121
- });
122
- });
123
-
124
- describe('Service Caching', () => {
125
- it('should cache service instances', async () => {
126
- const registerSpy = vi.fn().mockResolvedValue({ instance: 1 });
127
-
128
- const mockService = {
129
- serviceName: 'cached' as const,
130
- register: registerSpy,
131
- } satisfies Service<'cached', { instance: number }>;
132
-
133
- const discovery = ServiceDiscovery.getInstance(envParser);
134
-
135
- const services1 = await discovery.register([mockService]);
136
- const services2 = await discovery.register([mockService]);
137
-
138
- // Should only call register once
139
- expect(registerSpy).toHaveBeenCalledTimes(1);
140
-
141
- // Should return same instance
142
- expect(services1.cached).toBe(services2.cached);
143
- expect(services2.cached).toEqual({ instance: 1 });
144
- });
145
-
146
- it('should return cached instance even after registration', async () => {
147
- const mockService = {
148
- serviceName: 'persistent' as const,
149
- async register() {
150
- return { id: Math.random() };
151
- },
152
- } satisfies Service<'persistent', { id: number }>;
153
-
154
- const discovery = ServiceDiscovery.getInstance(envParser);
155
-
156
- const services1 = await discovery.register([mockService]);
157
- const firstId = services1.persistent.id;
158
-
159
- const services2 = await discovery.register([mockService]);
160
- const secondId = services2.persistent.id;
161
-
162
- // Same instance returned
163
- expect(firstId).toBe(secondId);
164
- });
165
-
166
- it('should cache different services independently', async () => {
167
- const service1 = {
168
- serviceName: 'service1' as const,
169
- async register() {
170
- return { value: 1 };
171
- },
172
- } satisfies Service<'service1', { value: number }>;
173
-
174
- const service2 = {
175
- serviceName: 'service2' as const,
176
- async register() {
177
- return { value: 2 };
178
- },
179
- } satisfies Service<'service2', { value: number }>;
180
-
181
- const discovery = ServiceDiscovery.getInstance(envParser);
182
-
183
- const result = await discovery.register([service1, service2]);
184
-
185
- expect(result.service1).toEqual({ value: 1 });
186
- expect(result.service2).toEqual({ value: 2 });
187
- expect(result.service1).not.toBe(result.service2);
188
- });
189
- });
190
-
191
- describe('get() method', () => {
192
- it('should throw error for non-existent service', () => {
193
- const discovery = ServiceDiscovery.getInstance(envParser);
194
-
195
- // Note: get() throws synchronously, not as a rejected Promise
196
- // Also relies on services Map which is not populated by register()
197
- expect(() => discovery.get('nonexistent')).toThrow(
198
- "Service 'nonexistent' not found in service discovery",
199
- );
200
- });
201
-
202
- it.skip('should retrieve registered service - SKIPPED: requires services Map to be populated', async () => {
203
- // This test is skipped because the current implementation doesn't
204
- // populate the services Map in the register() method
205
- const mockService = {
206
- serviceName: 'test' as const,
207
- async register() {
208
- return { value: 1 };
209
- },
210
- } satisfies Service<'test', { value: number }>;
211
-
212
- const discovery = ServiceDiscovery.getInstance(envParser);
213
- await discovery.register([mockService]);
214
-
215
- // This would fail because services Map is not populated
216
- // const result = await discovery.get('test');
217
- // expect(result).toEqual({ value: 1 });
218
- });
219
- });
220
-
221
- describe('getMany() method', () => {
222
- it.skip('should get multiple services at once - SKIPPED: requires services Map', async () => {
223
- // This test is skipped because getMany() relies on get() which
224
- // requires services Map to be populated by register()
225
- const service1 = {
226
- serviceName: 'db' as const,
227
- async register() {
228
- return { connected: true };
229
- },
230
- } satisfies Service<'db', { connected: boolean }>;
231
-
232
- const service2 = {
233
- serviceName: 'cache' as const,
234
- async register() {
235
- return { ready: true };
236
- },
237
- } satisfies Service<'cache', { ready: boolean }>;
238
-
239
- const discovery = ServiceDiscovery.getInstance(envParser);
240
- await discovery.register([service1, service2]);
241
-
242
- // This would fail because services Map is not populated
243
- // const result = await discovery.getMany(['db', 'cache']);
244
- });
245
-
246
- it('should return empty object for empty array', async () => {
247
- const discovery = ServiceDiscovery.getInstance(envParser);
248
-
249
- const result = await discovery.getMany([]);
250
-
251
- expect(result).toEqual({});
252
- });
253
-
254
- it.skip('should throw for any non-existent service in array - SKIPPED: requires services Map', async () => {
255
- // Skipped because getMany() requires services Map to be populated
256
- const service1 = {
257
- serviceName: 'exists' as const,
258
- async register() {
259
- return { value: 1 };
260
- },
261
- } satisfies Service<'exists', { value: number }>;
262
-
263
- const discovery = ServiceDiscovery.getInstance(envParser);
264
- await discovery.register([service1]);
265
-
266
- // Would fail because services Map not populated
267
- // await expect(
268
- // discovery.getMany(['exists', 'missing'] as any),
269
- // ).rejects.toThrow("Service 'missing' not found in service discovery");
270
- });
271
- });
272
-
273
- describe('has() method', () => {
274
- it.skip('should return true for registered service by name - SKIPPED: requires services Map', async () => {
275
- // Skipped because has() checks services Map which isn't populated by register()
276
- const mockService = {
277
- serviceName: 'test' as const,
278
- async register() {
279
- return { value: 1 };
280
- },
281
- } satisfies Service<'test', { value: number }>;
282
-
283
- const discovery = ServiceDiscovery.getInstance(envParser);
284
- await discovery.register([mockService]);
285
-
286
- // Would return false because services Map is not populated
287
- // expect(discovery.has('test')).toBe(true);
288
- });
289
-
290
- it('should return false for non-registered service by name', () => {
291
- const discovery = ServiceDiscovery.getInstance(envParser);
292
-
293
- expect(discovery.has('nonexistent')).toBe(false);
294
- });
295
-
296
- it.skip('should return true for registered service by instance - SKIPPED: requires services Map', async () => {
297
- // Skipped because has() checks services Map
298
- const mockService = {
299
- serviceName: 'test' as const,
300
- async register() {
301
- return { value: 1 };
302
- },
303
- } satisfies Service<'test', { value: number }>;
304
-
305
- const discovery = ServiceDiscovery.getInstance(envParser);
306
- await discovery.register([mockService]);
307
-
308
- // Would return false because services Map is not populated
309
- // expect(discovery.has(mockService)).toBe(true);
310
- });
311
-
312
- it('should return false for non-registered service by instance', () => {
313
- const mockService = {
314
- serviceName: 'test' as const,
315
- async register() {
316
- return { value: 1 };
317
- },
318
- } satisfies Service<'test', { value: number }>;
319
-
320
- const discovery = ServiceDiscovery.getInstance(envParser);
321
-
322
- expect(discovery.has(mockService)).toBe(false);
323
- });
324
- });
325
-
326
- describe('Error Handling', () => {
327
- it('should propagate errors from service registration', async () => {
328
- const errorService = {
329
- serviceName: 'failing' as const,
330
- async register() {
331
- throw new Error('Registration failed');
332
- },
333
- } satisfies Service<'failing', never>;
334
-
335
- const discovery = ServiceDiscovery.getInstance(envParser);
336
-
337
- await expect(discovery.register([errorService])).rejects.toThrow(
338
- 'Registration failed',
339
- );
340
- });
341
-
342
- it('should handle errors with custom error messages', async () => {
343
- const errorService = {
344
- serviceName: 'custom-error' as const,
345
- async register() {
346
- throw new Error('Custom error message');
347
- },
348
- } satisfies Service<'custom-error', never>;
349
-
350
- const discovery = ServiceDiscovery.getInstance(envParser);
351
-
352
- await expect(discovery.register([errorService])).rejects.toThrow(
353
- 'Custom error message',
354
- );
355
- });
356
-
357
- it('should handle synchronous errors', async () => {
358
- const errorService = {
359
- serviceName: 'sync-error' as const,
360
- register() {
361
- throw new Error('Synchronous error');
362
- },
363
- } satisfies Service<'sync-error', never>;
364
-
365
- const discovery = ServiceDiscovery.getInstance(envParser);
366
-
367
- await expect(discovery.register([errorService])).rejects.toThrow(
368
- 'Synchronous error',
369
- );
370
- });
371
- });
372
-
373
- describe('Service Interface', () => {
374
- it('should work with class-based services', async () => {
375
- class DatabaseService {
376
- connected = false;
377
-
378
- async connect() {
379
- this.connected = true;
380
- }
381
- }
382
-
383
- const databaseService = {
384
- serviceName: 'database' as const,
385
- async register() {
386
- const db = new DatabaseService();
387
- await db.connect();
388
- return db;
389
- },
390
- } satisfies Service<'database', DatabaseService>;
391
-
392
- const discovery = ServiceDiscovery.getInstance(envParser);
393
- const services = await discovery.register([databaseService]);
394
-
395
- expect(services.database).toBeInstanceOf(DatabaseService);
396
- expect(services.database.connected).toBe(true);
397
- });
398
-
399
- it('should work with factory functions', async () => {
400
- const createCache = () => ({
401
- data: new Map<string, any>(),
402
- set(key: string, value: any) {
403
- this.data.set(key, value);
404
- },
405
- get(key: string) {
406
- return this.data.get(key);
407
- },
408
- });
409
-
410
- const cacheService = {
411
- serviceName: 'cache' as const,
412
- async register() {
413
- return createCache();
414
- },
415
- } satisfies Service<'cache', ReturnType<typeof createCache>>;
416
-
417
- const discovery = ServiceDiscovery.getInstance(envParser);
418
- const services = await discovery.register([cacheService]);
419
-
420
- services.cache.set('key', 'value');
421
- expect(services.cache.get('key')).toBe('value');
422
- });
423
-
424
- it('should work with complex service dependencies', async () => {
425
- const loggerService = {
426
- serviceName: 'logger' as const,
427
- async register() {
428
- return { log: (msg: string) => msg };
429
- },
430
- } satisfies Service<'logger', { log: (msg: string) => string }>;
431
-
432
- const databaseService = {
433
- serviceName: 'database' as const,
434
- async register(_envParser) {
435
- // Service can access other services through discovery if needed
436
- return { connected: true, logger: 'attached' };
437
- },
438
- } satisfies Service<'database', { connected: boolean; logger: string }>;
439
-
440
- const discovery = ServiceDiscovery.getInstance(envParser);
441
- const services = await discovery.register([
442
- loggerService,
443
- databaseService,
444
- ]);
445
-
446
- expect(services.logger).toBeDefined();
447
- expect(services.database).toBeDefined();
448
- expect(services.database.connected).toBe(true);
449
- });
450
- });
451
-
452
- describe('Real-world Scenarios', () => {
453
- it('should handle database service pattern', async () => {
454
- interface Database {
455
- host: string;
456
- port: number;
457
- connected: boolean;
458
- query: (sql: string) => Promise<any[]>;
459
- }
460
-
461
- const databaseService = {
462
- serviceName: 'database' as const,
463
- async register() {
464
- // Create a new EnvironmentParser instance for service-specific config
465
- const serviceEnv = new EnvironmentParser({ ...process.env });
466
- const config = serviceEnv
467
- .create((get) => ({
468
- host: get('DB_HOST').string().default('localhost'),
469
- port: get('DB_PORT')
470
- .string()
471
- .default('5432')
472
- .transform((val) => Number(val)),
473
- }))
474
- .parse();
475
-
476
- const db: Database = {
477
- host: config.host,
478
- port: config.port,
479
- connected: true,
480
- query: async () => [],
481
- };
482
-
483
- return db;
484
- },
485
- } satisfies Service<'database', Database>;
486
-
487
- const discovery = ServiceDiscovery.getInstance(envParser);
488
- const services = await discovery.register([databaseService]);
489
-
490
- expect(services.database.connected).toBe(true);
491
- expect(services.database.host).toBe('localhost');
492
- expect(services.database.port).toBe(5432);
493
- });
494
-
495
- it('should handle cache service pattern', async () => {
496
- interface Cache {
497
- get(key: string): Promise<any>;
498
- set(key: string, value: any, ttl?: number): Promise<void>;
499
- delete(key: string): Promise<void>;
500
- }
501
-
502
- const cacheService = {
503
- serviceName: 'cache' as const,
504
- async register() {
505
- const storage = new Map<string, any>();
506
-
507
- return {
508
- async get(key: string) {
509
- return storage.get(key);
510
- },
511
- async set(key: string, value: any) {
512
- storage.set(key, value);
513
- },
514
- async delete(key: string) {
515
- storage.delete(key);
516
- },
517
- } satisfies Cache;
518
- },
519
- } satisfies Service<'cache', Cache>;
520
-
521
- const discovery = ServiceDiscovery.getInstance(envParser);
522
- const services = await discovery.register([cacheService]);
523
-
524
- await services.cache.set('test', 'value');
525
- expect(await services.cache.get('test')).toBe('value');
526
-
527
- await services.cache.delete('test');
528
- expect(await services.cache.get('test')).toBeUndefined();
529
- });
530
-
531
- it('should handle multiple service initialization in order', async () => {
532
- const initOrder: string[] = [];
533
-
534
- const service1 = {
535
- serviceName: 'first' as const,
536
- async register() {
537
- initOrder.push('first');
538
- return { order: 1 };
539
- },
540
- } satisfies Service<'first', { order: number }>;
541
-
542
- const service2 = {
543
- serviceName: 'second' as const,
544
- async register() {
545
- initOrder.push('second');
546
- return { order: 2 };
547
- },
548
- } satisfies Service<'second', { order: number }>;
549
-
550
- const service3 = {
551
- serviceName: 'third' as const,
552
- async register() {
553
- initOrder.push('third');
554
- return { order: 3 };
555
- },
556
- } satisfies Service<'third', { order: number }>;
557
-
558
- const discovery = ServiceDiscovery.getInstance(envParser);
559
- await discovery.register([service1, service2, service3]);
560
-
561
- expect(initOrder).toEqual(['first', 'second', 'third']);
562
- });
563
- });
564
- });