@onebun/core 0.1.14 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onebun/core",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
4
4
  "description": "Core package for OneBun framework - decorators, DI, modules, controllers",
5
5
  "license": "LGPL-3.0",
6
6
  "author": "RemRyahirev",
@@ -154,6 +154,94 @@ describe('OneBunModule', () => {
154
154
 
155
155
  expect(() => new OneBunModule(TestClass, mockLoggerLayer)).toThrow();
156
156
  });
157
+
158
+ test('should detect circular dependencies and provide detailed error message', () => {
159
+ const { registerDependencies } = require('../decorators/decorators');
160
+ const { Effect, Layer } = require('effect');
161
+ const { LoggerService } = require('@onebun/logger');
162
+
163
+ // Collect error messages
164
+ const errorMessages: string[] = [];
165
+
166
+ // Create mock logger that captures error messages
167
+ // Using Effect.sync to ensure the message is captured synchronously when Effect.runSync is called
168
+ const captureLogger = {
169
+ trace: () => Effect.sync(() => undefined),
170
+ debug: () => Effect.sync(() => undefined),
171
+ info: () => Effect.sync(() => undefined),
172
+ warn: () => Effect.sync(() => undefined),
173
+ error: (msg: string) =>
174
+ Effect.sync(() => {
175
+ errorMessages.push(msg);
176
+ }),
177
+ fatal: () => Effect.sync(() => undefined),
178
+ child: () => captureLogger,
179
+ };
180
+ const captureLoggerLayer = Layer.succeed(LoggerService, captureLogger);
181
+
182
+ // Create services - define all classes first
183
+ @Service()
184
+ class CircularServiceA {
185
+ getValue() {
186
+ return 'A';
187
+ }
188
+ }
189
+
190
+ @Service()
191
+ class CircularServiceB {
192
+ getValue() {
193
+ return 'B';
194
+ }
195
+ }
196
+
197
+ @Service()
198
+ class CircularServiceC {
199
+ getValue() {
200
+ return 'C';
201
+ }
202
+ }
203
+
204
+ // Now register circular dependencies manually: A -> B -> C -> A
205
+ registerDependencies(CircularServiceA, [CircularServiceC]);
206
+ registerDependencies(CircularServiceB, [CircularServiceA]);
207
+ registerDependencies(CircularServiceC, [CircularServiceB]);
208
+
209
+ @Module({
210
+ providers: [CircularServiceA, CircularServiceB, CircularServiceC],
211
+ })
212
+ class CircularModule {}
213
+
214
+ // Initialize module - should detect circular dependency
215
+ new OneBunModule(CircularModule, captureLoggerLayer);
216
+
217
+ // Verify error message was logged
218
+ expect(errorMessages.length).toBeGreaterThan(0);
219
+
220
+ // Find the circular dependency error message
221
+ const circularError = errorMessages.find((msg) =>
222
+ msg.includes('Circular dependency detected'),
223
+ );
224
+ expect(circularError).toBeDefined();
225
+
226
+ // Should contain module name
227
+ expect(circularError).toContain('CircularModule');
228
+
229
+ // Should contain "Unresolved services" section
230
+ expect(circularError).toContain('Unresolved services');
231
+
232
+ // Should contain at least one of the service names with their dependencies
233
+ const hasServiceInfo =
234
+ circularError!.includes('CircularServiceA') ||
235
+ circularError!.includes('CircularServiceB') ||
236
+ circularError!.includes('CircularServiceC');
237
+ expect(hasServiceInfo).toBe(true);
238
+
239
+ // Should contain "needs:" showing what dependencies are required
240
+ expect(circularError).toContain('needs:');
241
+
242
+ // Should contain "Dependency chain" showing the cycle
243
+ expect(circularError).toContain('Dependency chain');
244
+ });
157
245
  });
158
246
 
159
247
  describe('Module instance methods', () => {
@@ -229,6 +229,7 @@ export class OneBunModule implements ModuleInstance {
229
229
  // Create services in dependency order
230
230
  const pendingProviders = [...metadata.providers.filter((p) => typeof p === 'function')];
231
231
  const createdServices = new Set<string>();
232
+ const unresolvedDeps = new Map<string, string[]>(); // Track unresolved dependencies for error reporting
232
233
  let iterations = 0;
233
234
  const maxIterations = pendingProviders.length * 2; // Prevent infinite loops
234
235
 
@@ -245,8 +246,11 @@ export class OneBunModule implements ModuleInstance {
245
246
  continue;
246
247
  }
247
248
 
248
- // Use autoDetectDependencies to find dependencies from constructor
249
- const detectedDeps = autoDetectDependencies(provider, availableServiceClasses);
249
+ // Use getConstructorParamTypes first (for @Inject and TypeScript metadata),
250
+ // then fallback to autoDetectDependencies (for constructor source analysis)
251
+ const detectedDeps =
252
+ getConstructorParamTypes(provider) ||
253
+ autoDetectDependencies(provider, availableServiceClasses);
250
254
  const dependencies: unknown[] = [];
251
255
  let allDependenciesResolved = true;
252
256
 
@@ -259,6 +263,12 @@ export class OneBunModule implements ModuleInstance {
259
263
  // Check if it's a service that hasn't been created yet
260
264
  const isServiceInModule = availableServiceClasses.has(depType.name);
261
265
  if (isServiceInModule && !createdServices.has(depType.name)) {
266
+ // Track unresolved dependency for error reporting
267
+ const deps = unresolvedDeps.get(provider.name) || [];
268
+ if (!deps.includes(depType.name)) {
269
+ deps.push(depType.name);
270
+ unresolvedDeps.set(provider.name, deps);
271
+ }
262
272
  // This dependency will be created later, defer this service
263
273
  allDependenciesResolved = false;
264
274
  pendingProviders.push(provider);
@@ -317,7 +327,25 @@ export class OneBunModule implements ModuleInstance {
317
327
  }
318
328
 
319
329
  if (iterations >= maxIterations) {
320
- this.logger.error('Possible circular dependency detected in services');
330
+ const unresolvedServices = pendingProviders
331
+ .filter((p) => typeof p === 'function')
332
+ .map((p) => p.name);
333
+
334
+ const details = unresolvedServices
335
+ .map((serviceName) => {
336
+ const deps = unresolvedDeps.get(serviceName) || [];
337
+
338
+ return ` - ${serviceName} -> needs: [${deps.join(', ')}]`;
339
+ })
340
+ .join('\n');
341
+
342
+ const dependencyChain = this.buildDependencyChain(unresolvedDeps, unresolvedServices);
343
+
344
+ this.logger.error(
345
+ `Circular dependency detected in module ${this.moduleClass.name}!\n` +
346
+ `Unresolved services:\n${details}\n` +
347
+ `Dependency chain: ${dependencyChain}`,
348
+ );
321
349
  }
322
350
  }
323
351
 
@@ -479,6 +507,52 @@ export class OneBunModule implements ModuleInstance {
479
507
  return serviceInstance;
480
508
  }
481
509
 
510
+ /**
511
+ * Build a human-readable dependency chain for circular dependency error reporting
512
+ * Traverses the dependency graph to find and display the cycle
513
+ */
514
+ private buildDependencyChain(
515
+ unresolvedDeps: Map<string, string[]>,
516
+ unresolvedServices: string[],
517
+ ): string {
518
+ // Find cycle by traversing dependencies
519
+ const visited = new Set<string>();
520
+ const chain: string[] = [];
521
+
522
+ const findCycle = (service: string): boolean => {
523
+ if (visited.has(service)) {
524
+ chain.push(service);
525
+
526
+ return true;
527
+ }
528
+ visited.add(service);
529
+ chain.push(service);
530
+
531
+ const deps = unresolvedDeps.get(service) || [];
532
+ for (const dep of deps) {
533
+ if (unresolvedServices.includes(dep)) {
534
+ if (findCycle(dep)) {
535
+ return true;
536
+ }
537
+ }
538
+ }
539
+ chain.pop();
540
+
541
+ return false;
542
+ };
543
+
544
+ for (const service of unresolvedServices) {
545
+ visited.clear();
546
+ chain.length = 0;
547
+ if (findCycle(service)) {
548
+ return chain.join(' -> ');
549
+ }
550
+ }
551
+
552
+ // If no cycle found, just show all unresolved services
553
+ return unresolvedServices.join(' <-> ');
554
+ }
555
+
482
556
  /**
483
557
  * Setup the module and its dependencies
484
558
  */