@vercube/di 1.2.1 → 1.2.2

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/dist/index.d.mts CHANGED
@@ -3,7 +3,7 @@
3
3
  * This is base class for all property decorators used in application. Decorator class must extend this one.
4
4
  * This class instance is created using IOC container so you can @Inject() things here.
5
5
  */
6
- declare abstract class BaseDecorator<T = any, P = any> {
6
+ export declare abstract class BaseDecorator<T = any, P = any> {
7
7
  /** Holds options object that is passed as 2nd argument in createDecorator() factory */
8
8
  options: T;
9
9
  /** Holds class instance that is decorated */
@@ -29,11 +29,11 @@ declare abstract class BaseDecorator<T = any, P = any> {
29
29
  }
30
30
  //#endregion
31
31
  //#region src/Domain/ContainerEvents.d.ts
32
- type OnExpandedEvent = (serviceKeys: IOC.ServiceKey[]) => void;
32
+ export type OnExpandedEvent = (serviceKeys: IOC.ServiceKey[]) => void;
33
33
  /**
34
34
  * This class allows for container to listen on various IOC events.
35
35
  */
36
- declare class ContainerEvents {
36
+ export declare class ContainerEvents {
37
37
  private fOnExpanded;
38
38
  /**
39
39
  * Registers to container "onExpanded" event.
@@ -52,7 +52,7 @@ declare class ContainerEvents {
52
52
  * This is new implementation of IOC Container. It mimics Inversify.js container a little bit but its
53
53
  * simpler and (probably) more performant on larger scales.
54
54
  */
55
- declare class Container {
55
+ export declare class Container {
56
56
  protected fContext: string | undefined;
57
57
  protected fLocked: boolean;
58
58
  protected fDefaultParams: IOC.ContainerParams;
@@ -61,6 +61,7 @@ declare class Container {
61
61
  protected fSingletonInstances: Map<IOC.ServiceKey, IOC.Instance>;
62
62
  protected fInjectMethod: IOC.InjectMethod;
63
63
  protected fContainerEvents: ContainerEvents;
64
+ protected fRevision: number;
64
65
  /**
65
66
  * Constructor for container.
66
67
  * @param params initial params for container
@@ -92,6 +93,13 @@ declare class Container {
92
93
  * @returns {string} the context of the container
93
94
  */
94
95
  get context(): string | undefined;
96
+ /**
97
+ * Counter that changes whenever a binding is added, replaced or first
98
+ * instantiated - that is, whenever a description of this container would
99
+ * come out different.
100
+ * @returns {number} the current revision
101
+ */
102
+ get revision(): number;
95
103
  /**
96
104
  * Binds particular key to container in singleton scope. Multiple queries/injects of this
97
105
  * service will always return the same instance.
@@ -230,7 +238,7 @@ declare class Container {
230
238
  /**
231
239
  * This module exports various types used in IOC system.
232
240
  */
233
- declare namespace IOC {
241
+ export declare namespace IOC {
234
242
  /**
235
243
  * This is a key that we use to identify service. Symbol is new way of doing it, however we
236
244
  * also keep standard/abstract classes for backward compatability.
@@ -337,7 +345,7 @@ declare namespace IOC {
337
345
  * @param key key to inject
338
346
  * @returns decorator
339
347
  */
340
- declare function Inject(key: IOC.ServiceKey): Function;
348
+ export declare function Inject(key: IOC.ServiceKey): Function;
341
349
  //#endregion
342
350
  //#region src/Decorators/InjectOptional.d.ts
343
351
  /**
@@ -345,7 +353,7 @@ declare function Inject(key: IOC.ServiceKey): Function;
345
353
  * @param key key to inject
346
354
  * @returns decorator
347
355
  */
348
- declare function InjectOptional(key: IOC.ServiceKey): Function;
356
+ export declare function InjectOptional(key: IOC.ServiceKey): Function;
349
357
  //#endregion
350
358
  //#region src/Decorators/Init.d.ts
351
359
  /**
@@ -353,7 +361,7 @@ declare function InjectOptional(key: IOC.ServiceKey): Function;
353
361
  * Use this decorator to run initialization logic for runtime-loaded container dependencies.
354
362
  * @returns {Function} Decorator function that creates an InitDecorator instance
355
363
  */
356
- declare function Init(): Function;
364
+ export declare function Init(): Function;
357
365
  //#endregion
358
366
  //#region src/Decorators/Destroy.d.ts
359
367
  /**
@@ -372,7 +380,7 @@ declare function Init(): Function;
372
380
  * ```
373
381
  * @returns {Function} Decorator function that creates a DestroyDecorator instance
374
382
  */
375
- declare function Destroy(): Function;
383
+ export declare function Destroy(): Function;
376
384
  //#endregion
377
385
  //#region src/Decorators/Injectable.d.ts
378
386
  /**
@@ -382,48 +390,95 @@ declare function Destroy(): Function;
382
390
  * This decorator is intentionally a no-op at runtime - it exists solely as a
383
391
  * compile-time marker that the build-time scanner can detect via AST analysis.
384
392
  */
385
- declare function Injectable(): ClassDecorator;
393
+ export declare function Injectable(): ClassDecorator;
386
394
  //#endregion
387
- //#region src/Domain/DevtoolsHook.d.ts
388
- /**
389
- * Global property name for the IOC devtools hook.
390
- */
391
- declare const IOC_DEVTOOLS_HOOK_KEY = "__VERCUBE_DEVTOOLS_HOOK__";
392
- /**
393
- * A single service resolution record emitted when the container constructs an instance.
394
- */
395
- interface IOCResolveRecord {
396
- /** Service key that was resolved. */
397
- key: IOC.ServiceKey;
398
- /** Human readable name of the service key. */
399
- name: string;
400
- /** Factory type used to build the instance. */
401
- type: IOC.ServiceFactoryType;
402
- /** Context of the container that performed the resolution. */
403
- context: string | undefined;
404
- /** Timestamp (ms) before construction. */
405
- start: number;
406
- /** Timestamp (ms) after construction and injection. */
407
- end: number;
408
- }
395
+ //#region src/Types/DescribeTypes.d.ts
409
396
  /**
410
- * Optional hook for observing the IOC container.
397
+ * Types describing a container's contents.
398
+ *
399
+ * The description is built from binding metadata only. Nothing is resolved, so
400
+ * inspecting a container never constructs a service the application itself
401
+ * never asked for.
411
402
  */
412
- interface IOCDevtoolsHook {
413
- /** Called once for every {@link Container} instance after construction. */
414
- onContainerCreated?: (container: Container) => void;
415
- /** Called whenever the container constructs a new service instance. */
416
- onResolved?: (record: IOCResolveRecord) => void;
403
+ export declare namespace Describe {
404
+ /** How a binding produces its value. */
405
+ type ServiceKind = 'singleton' | 'transient' | 'instance';
406
+ /** One `@Inject` / `@InjectOptional` declaration. */
407
+ interface Dependency {
408
+ /** Id of the service this resolves to, or `unbound:Name` when nothing is bound. */
409
+ id: string;
410
+ /** Display name of the dependency key. */
411
+ name: string;
412
+ /** Property the dependency is injected into. */
413
+ property: string;
414
+ /** Whether the declaration was `@InjectOptional`. */
415
+ optional: boolean;
416
+ /** Whether something is actually bound under the key. */
417
+ bound: boolean;
418
+ }
419
+ /** A service in the container. */
420
+ interface ServiceNode {
421
+ /** Unique id. The display name, suffixed with `#n` when names collide. */
422
+ id: string;
423
+ /** Display name of the binding key. */
424
+ name: string;
425
+ kind: ServiceKind;
426
+ /**
427
+ * Category of the service.
428
+ *
429
+ * The container itself can only tell a class from a plain value; anything
430
+ * finer - controller, middleware, plugin - comes from the `annotate`
431
+ * callback, because only the framework layer knows what those are.
432
+ */
433
+ role: string;
434
+ /** Implementation class name, when it differs from the key. */
435
+ implementation: string | null;
436
+ /** Whether an instance already exists. */
437
+ instantiated: boolean;
438
+ /** Whether the key is a symbol. */
439
+ symbol: boolean;
440
+ dependencies: Dependency[];
441
+ /** Number of services depending on this one. */
442
+ dependents: number;
443
+ /** Extra fields contributed by `annotate`. */
444
+ [extra: string]: unknown;
445
+ }
446
+ /** A directed dependency edge. */
447
+ interface ServiceEdge {
448
+ from: string;
449
+ to: string;
450
+ property: string;
451
+ optional: boolean;
452
+ }
453
+ /** Everything known about a container's bindings. */
454
+ interface ContainerDescription {
455
+ /** Container label, when one was given. */
456
+ context?: string;
457
+ nodes: ServiceNode[];
458
+ edges: ServiceEdge[];
459
+ /** Dependency cycles, each as the ordered ids forming the loop. */
460
+ cycles: string[][];
461
+ /** Number of services that were never instantiated. */
462
+ unusedCount: number;
463
+ }
464
+ /** What `annotate` is told about the node it is classifying. */
465
+ interface AnnotateContext {
466
+ key: IOC.ServiceKey;
467
+ def: Readonly<IOC.ServiceDef>;
468
+ /** Constructor behind the binding, or null for plain values. */
469
+ ctor: (Function & {
470
+ prototype: unknown;
471
+ }) | null;
472
+ }
473
+ /** Options for {@link describeContainer}. */
474
+ interface Options {
475
+ /**
476
+ * Refines a node before it is added to the description. Typically sets
477
+ * `role` and attaches framework-specific fields.
478
+ */
479
+ annotate?: (node: ServiceNode, context: AnnotateContext) => void;
480
+ }
417
481
  }
418
- /**
419
- * Installs or replaces the devtools hook.
420
- * @param hook hook implementation, or `undefined` to uninstall
421
- */
422
- declare function setIOCDevtoolsHook(hook: IOCDevtoolsHook | undefined): void;
423
- /**
424
- * Returns the currently installed devtools hook, if any.
425
- */
426
- declare function getIOCDevtoolsHook(): IOCDevtoolsHook | undefined;
427
482
  //#endregion
428
483
  //#region src/Domain/Engine.d.ts
429
484
  /**
@@ -468,13 +523,119 @@ declare function getDeps(instance: IOC.Instance): IClassDep[];
468
523
  * @param method inject method, "lazy" queries dep during property access while "static" injects during class creation
469
524
  */
470
525
  declare function injectDeps(container: Container, instance: IOC.Instance, method: IOC.InjectMethod): void;
471
- declare const IOCEngine: {
526
+ export declare const IOCEngine: {
472
527
  registerInject: typeof registerInject;
473
528
  getEntryForClass: typeof getEntryForClass;
474
529
  injectDeps: typeof injectDeps;
475
530
  getDeps: typeof getDeps;
476
531
  };
477
532
  //#endregion
533
+ //#region src/Domain/Describe.d.ts
534
+ /**
535
+ * Produces a readable label for any service key.
536
+ *
537
+ * @param key - Service key from a `bind*` call
538
+ * @returns Display name for the key
539
+ */
540
+ export declare function describeKey(key: IOC.ServiceKey): string;
541
+ /**
542
+ * Names the value a key was bound to, or `null` when it adds no information.
543
+ *
544
+ * @param key - Service key
545
+ * @param value - Bound implementation or instance
546
+ * @returns Implementation name, or null
547
+ */
548
+ export declare function describeImplementation(key: IOC.ServiceKey, value: unknown): string | null;
549
+ /**
550
+ * Resolves the class constructor behind a service definition.
551
+ *
552
+ * @param def - Service definition from the container
553
+ * @returns The constructor, or null for plain values
554
+ */
555
+ export declare function resolveConstructor(def: Readonly<IOC.ServiceDef>): (Function & {
556
+ prototype: unknown;
557
+ }) | null;
558
+ /**
559
+ * Collects every injection declaration of a class, inherited ones included.
560
+ *
561
+ * @param ctor - Class constructor to inspect
562
+ * @returns Dependency declarations, nearest prototype first
563
+ */
564
+ export declare function collectClassDeps(ctor: (Function & {
565
+ prototype: unknown;
566
+ }) | null): IClassDep[];
567
+ /**
568
+ * Maps a binding's factory type onto its description kind.
569
+ *
570
+ * @param type - Factory type recorded by the container
571
+ * @returns The matching kind
572
+ */
573
+ export declare function toServiceKind(type: IOC.ServiceFactoryType): Describe.ServiceKind;
574
+ /**
575
+ * Builds a serialisable description of a container.
576
+ *
577
+ * Reads `container.services`, which is binding metadata, never
578
+ * `getAllServices()`, which would construct every registered service. An
579
+ * inspector that instantiates what it inspects is not an inspector.
580
+ *
581
+ * @param container - The container to describe
582
+ * @param options - Optional annotation callback
583
+ * @returns Nodes, edges, cycles and the unused-service count
584
+ */
585
+ export declare function describeContainer(container: Container, options?: Describe.Options): Describe.ContainerDescription;
586
+ //#endregion
587
+ //#region src/Domain/DevtoolsHook.d.ts
588
+ /**
589
+ * Global property name for the IOC observer registry.
590
+ */
591
+ export declare const IOC_DEVTOOLS_HOOK_KEY = "__VERCUBE_DEVTOOLS_HOOK__";
592
+ /**
593
+ * A single service resolution record emitted when the container constructs an instance.
594
+ */
595
+ export interface IOCResolveRecord {
596
+ /** Service key that was resolved. */
597
+ key: IOC.ServiceKey;
598
+ /** Human readable name of the service key. */
599
+ name: string;
600
+ /** Factory type used to build the instance. */
601
+ type: IOC.ServiceFactoryType;
602
+ /** Context of the container that performed the resolution. */
603
+ context: string | undefined;
604
+ /** Timestamp (ms) before construction. */
605
+ start: number;
606
+ /** Timestamp (ms) after construction and injection. */
607
+ end: number;
608
+ }
609
+ /**
610
+ * Optional hook for observing the IOC container.
611
+ */
612
+ export interface IOCDevtoolsHook {
613
+ /** Called once for every {@link Container} instance after construction. */
614
+ onContainerCreated?: (container: Container) => void;
615
+ /** Called whenever the container constructs a new service instance. */
616
+ onResolved?: (record: IOCResolveRecord) => void;
617
+ }
618
+ /**
619
+ * Installs an observer.
620
+ *
621
+ * @param hook - The observer to install
622
+ * @returns A function that removes it again
623
+ */
624
+ export declare function addIOCDevtoolsHook(hook: IOCDevtoolsHook): () => void;
625
+ /**
626
+ * Replaces every installed observer with `hook`, or removes them all.
627
+ *
628
+ * @param hook - The observer to install, or `undefined` to uninstall everything
629
+ * @deprecated Use {@link addIOCDevtoolsHook}, which does not evict other observers.
630
+ */
631
+ export declare function setIOCDevtoolsHook(hook: IOCDevtoolsHook | undefined): void;
632
+ /**
633
+ * Returns the fan-out hook the container calls into.
634
+ *
635
+ * @returns The registry, or undefined when no observer is installed
636
+ */
637
+ export declare function getIOCDevtoolsHook(): IOCDevtoolsHook | undefined;
638
+ //#endregion
478
639
  //#region src/Utils/Utils.d.ts
479
640
  /**
480
641
  * This function generates new service key for particular service. Providing
@@ -484,18 +645,18 @@ declare const IOCEngine: {
484
645
  * @param name name of the service
485
646
  * @returns unique service identity
486
647
  */
487
- declare function Identity(name: string): IOC.Identity;
648
+ export declare function Identity(name: string): IOC.Identity;
488
649
  /**
489
650
  * A simple type of constructor for class "T"
490
651
  */
491
- interface IClassType<T> {
652
+ export interface IClassType<T> {
492
653
  new (): T;
493
654
  }
494
655
  /**
495
656
  * Holds decorator entry struct. It is saved in __decorators array for every class and holds
496
657
  * various informations.
497
658
  */
498
- interface IDecoratorEntry {
659
+ export interface IDecoratorEntry {
499
660
  classType: any;
500
661
  params: any;
501
662
  target: any;
@@ -507,11 +668,11 @@ interface IDecoratorEntry {
507
668
  * array that holds information about all decorators used in this class. It will be used later for
508
669
  * turning those declarations for real code.
509
670
  */
510
- interface IDecoratedPrototype {
671
+ export interface IDecoratedPrototype {
511
672
  __decorators?: IDecoratorEntry[];
512
673
  __metadata?: any;
513
674
  }
514
- interface IDecoratedInstance {
675
+ export interface IDecoratedInstance {
515
676
  __decoratorInstances?: BaseDecorator<any>[];
516
677
  }
517
678
  /**
@@ -521,14 +682,14 @@ interface IDecoratedInstance {
521
682
  * @param params custom options object that will be availalbe in "options" property of decorator class
522
683
  * @return ES6 decorator function
523
684
  */
524
- declare function createDecorator<P, T extends BaseDecorator<P>>(decoratorClass: IClassType<T>, params: P): Function;
685
+ export declare function createDecorator<P, T extends BaseDecorator<P>>(decoratorClass: IClassType<T>, params: P): Function;
525
686
  /**
526
687
  * This function initializes all registered decorators on particular instance. It must be called in order
527
688
  * for decorator code work in particular class.
528
689
  * @param target class instance to sue
529
690
  * @param container IOC container for context
530
691
  */
531
- declare function initializeDecorators(target: IDecoratedInstance, container: Container): void;
692
+ export declare function initializeDecorators(target: IDecoratedInstance, container: Container): void;
532
693
  /**
533
694
  * This function releases all decorators applied to target instance by calling their .destroyed()
534
695
  * method. Its used to provide a way for cleanup for decorators. @see BaseDecorator.destroy()
@@ -536,18 +697,18 @@ declare function initializeDecorators(target: IDecoratedInstance, container: Con
536
697
  * @param target instance of class that should have decorators cleaned up
537
698
  * @param container ioc container
538
699
  */
539
- declare function destroyDecorators(target: IDecoratedInstance, container: Container): void;
700
+ export declare function destroyDecorators(target: IDecoratedInstance, container: Container): void;
540
701
  /**
541
702
  * This function is responsible for preparing IOC container to work with all decorators. It simply
542
703
  * initializes all decorators on all services registered.
543
704
  * @param container IOC container
544
705
  */
545
- declare function initializeContainer(container: Container): void;
706
+ export declare function initializeContainer(container: Container): void;
546
707
  /**
547
708
  * This function is responsible for preparing IOC container to work with all decorators. It simply
548
709
  * initializes all decorators on all services registered.
549
710
  * @param container IOC container
550
711
  */
551
- declare function destroyContainer(container: Container): void;
712
+ export declare function destroyContainer(container: Container): void;
552
713
  //#endregion
553
- export { BaseDecorator, Container, ContainerEvents, Destroy, type IClassDep, type IClassMapEntry, IClassType, IDecoratedInstance, IDecoratedPrototype, IDecoratorEntry, IOC, IOCDevtoolsHook, IOCEngine, IOCResolveRecord, IOC_DEVTOOLS_HOOK_KEY, Identity, Init, Inject, InjectOptional, Injectable, OnExpandedEvent, createDecorator, destroyContainer, destroyDecorators, getIOCDevtoolsHook, initializeContainer, initializeDecorators, setIOCDevtoolsHook };
714
+ export type { IClassDep, IClassMapEntry };
package/dist/index.mjs CHANGED
@@ -154,23 +154,64 @@ var ContainerEvents = class {
154
154
  //#endregion
155
155
  //#region src/Domain/DevtoolsHook.ts
156
156
  /**
157
- * Global property name for the IOC devtools hook.
157
+ * Global property name for the IOC observer registry.
158
158
  */
159
159
  const IOC_DEVTOOLS_HOOK_KEY = "__VERCUBE_DEVTOOLS_HOOK__";
160
- let devtoolsHook = globalThis[IOC_DEVTOOLS_HOOK_KEY];
161
160
  /**
162
- * Installs or replaces the devtools hook.
163
- * @param hook hook implementation, or `undefined` to uninstall
161
+ * Returns the global registry, creating it on first use.
162
+ *
163
+ * @returns The registry
164
+ */
165
+ function registry() {
166
+ let current = globalThis[IOC_DEVTOOLS_HOOK_KEY];
167
+ if (!current) {
168
+ const observers = [];
169
+ current = {
170
+ observers,
171
+ onContainerCreated(container) {
172
+ for (const observer of observers) observer.onContainerCreated?.(container);
173
+ },
174
+ onResolved(record) {
175
+ for (const observer of observers) observer.onResolved?.(record);
176
+ }
177
+ };
178
+ globalThis[IOC_DEVTOOLS_HOOK_KEY] = current;
179
+ }
180
+ return current;
181
+ }
182
+ /**
183
+ * Installs an observer.
184
+ *
185
+ * @param hook - The observer to install
186
+ * @returns A function that removes it again
187
+ */
188
+ function addIOCDevtoolsHook(hook) {
189
+ const observers = registry().observers;
190
+ observers.push(hook);
191
+ return () => {
192
+ const index = observers.indexOf(hook);
193
+ if (index !== -1) observers.splice(index, 1);
194
+ };
195
+ }
196
+ /**
197
+ * Replaces every installed observer with `hook`, or removes them all.
198
+ *
199
+ * @param hook - The observer to install, or `undefined` to uninstall everything
200
+ * @deprecated Use {@link addIOCDevtoolsHook}, which does not evict other observers.
164
201
  */
165
202
  function setIOCDevtoolsHook(hook) {
166
- devtoolsHook = hook;
167
- globalThis[IOC_DEVTOOLS_HOOK_KEY] = hook;
203
+ const current = registry();
204
+ current.observers.length = 0;
205
+ if (hook) current.observers.push(hook);
168
206
  }
169
207
  /**
170
- * Returns the currently installed devtools hook, if any.
208
+ * Returns the fan-out hook the container calls into.
209
+ *
210
+ * @returns The registry, or undefined when no observer is installed
171
211
  */
172
212
  function getIOCDevtoolsHook() {
173
- return devtoolsHook;
213
+ const current = globalThis[IOC_DEVTOOLS_HOOK_KEY];
214
+ return current && current.observers.length > 0 ? current : void 0;
174
215
  }
175
216
  //#endregion
176
217
  //#region src/Domain/Container.ts
@@ -190,6 +231,7 @@ var Container = class Container {
190
231
  fSingletonInstances = /* @__PURE__ */ new Map();
191
232
  fInjectMethod = IOC.InjectMethod.STATIC;
192
233
  fContainerEvents = new ContainerEvents();
234
+ fRevision = 0;
193
235
  /**
194
236
  * Constructor for container.
195
237
  * @param params initial params for container
@@ -239,6 +281,15 @@ var Container = class Container {
239
281
  return this.fContext;
240
282
  }
241
283
  /**
284
+ * Counter that changes whenever a binding is added, replaced or first
285
+ * instantiated - that is, whenever a description of this container would
286
+ * come out different.
287
+ * @returns {number} the current revision
288
+ */
289
+ get revision() {
290
+ return this.fRevision;
291
+ }
292
+ /**
242
293
  * Binds particular key to container in singleton scope. Multiple queries/injects of this
243
294
  * service will always return the same instance.
244
295
  *
@@ -258,6 +309,7 @@ var Container = class Container {
258
309
  this.fServices.delete(key);
259
310
  }
260
311
  this.fServices.set(key, newDef);
312
+ this.fRevision++;
261
313
  this.fNewQueue.set(key, newDef);
262
314
  }
263
315
  /**
@@ -275,6 +327,7 @@ var Container = class Container {
275
327
  const existingServiceDef = this.fServices.get(key);
276
328
  if (existingServiceDef) this.internalDispose(existingServiceDef);
277
329
  this.fServices.set(key, newDef);
330
+ this.fRevision++;
278
331
  this.fNewQueue.set(key, newDef);
279
332
  }
280
333
  /**
@@ -294,6 +347,7 @@ var Container = class Container {
294
347
  const existingServiceDef = this.fServices.get(key);
295
348
  if (existingServiceDef) this.internalDispose(existingServiceDef);
296
349
  this.fServices.set(key, newDef);
350
+ this.fRevision++;
297
351
  this.fNewQueue.set(key, newDef);
298
352
  }
299
353
  /**
@@ -319,6 +373,7 @@ var Container = class Container {
319
373
  const existingServiceDef = this.fServices.get(key);
320
374
  if (existingServiceDef) this.internalDispose(existingServiceDef);
321
375
  this.fServices.set(key, newDef);
376
+ this.fRevision++;
322
377
  this.fNewQueue.set(key, newDef);
323
378
  }
324
379
  /**
@@ -457,7 +512,10 @@ var Container = class Container {
457
512
  const start = onResolved ? performance.now() : 0;
458
513
  const constructor = serviceDef.serviceValue;
459
514
  const instance = new constructor();
460
- if (singleton) this.fSingletonInstances.set(serviceDef.serviceKey, instance);
515
+ if (singleton) {
516
+ this.fSingletonInstances.set(serviceDef.serviceKey, instance);
517
+ this.fRevision++;
518
+ }
461
519
  try {
462
520
  this.internalProcessInjects(instance, this.fInjectMethod);
463
521
  } catch (error) {
@@ -572,7 +630,7 @@ let IOC;
572
630
  */
573
631
  const classMap = /* @__PURE__ */ new Map();
574
632
  globalThis.__IOCClassMap = globalThis.__IOCClassMap ?? classMap;
575
- const ROOT_PROTO = Object.getPrototypeOf({});
633
+ const ROOT_PROTO$1 = Object.getPrototypeOf({});
576
634
  /**
577
635
  * Retrieves the class dependency metadata map used for IOC injection purposes.
578
636
  *
@@ -667,7 +725,7 @@ function injectDeps(container, instance, method) {
667
725
  }
668
726
  }
669
727
  prototype = Object.getPrototypeOf(prototype);
670
- } while (prototype && prototype !== ROOT_PROTO);
728
+ } while (prototype && prototype !== ROOT_PROTO$1);
671
729
  }
672
730
  const IOCEngine = {
673
731
  registerInject,
@@ -770,4 +828,215 @@ function Injectable() {
770
828
  return () => {};
771
829
  }
772
830
  //#endregion
773
- export { BaseDecorator, Container, ContainerEvents, Destroy, IOC, IOCEngine, IOC_DEVTOOLS_HOOK_KEY, Identity, Init, Inject, InjectOptional, Injectable, createDecorator, destroyContainer, destroyDecorators, getIOCDevtoolsHook, initializeContainer, initializeDecorators, setIOCDevtoolsHook };
831
+ //#region src/Domain/Describe.ts
832
+ /** Prototype of a plain object; the stop condition when walking prototype chains. */
833
+ const ROOT_PROTO = Object.getPrototypeOf({});
834
+ /**
835
+ * Produces a readable label for any service key.
836
+ *
837
+ * @param key - Service key from a `bind*` call
838
+ * @returns Display name for the key
839
+ */
840
+ function describeKey(key) {
841
+ if (typeof key === "symbol") return key.description ?? "Symbol()";
842
+ if (typeof key === "string") return key;
843
+ if (typeof key === "function") return key.name || "Anonymous";
844
+ if (typeof key === "object" && key !== null) return key.constructor?.name ?? "Object";
845
+ return "Unknown";
846
+ }
847
+ /**
848
+ * Names the value a key was bound to, or `null` when it adds no information.
849
+ *
850
+ * @param key - Service key
851
+ * @param value - Bound implementation or instance
852
+ * @returns Implementation name, or null
853
+ */
854
+ function describeImplementation(key, value) {
855
+ if (value === key) return null;
856
+ if (typeof value === "function") {
857
+ const name = value.name;
858
+ return name && name !== describeKey(key) ? name : null;
859
+ }
860
+ if (typeof value === "object" && value !== null) {
861
+ const name = value.constructor?.name;
862
+ return name && name !== describeKey(key) ? name : null;
863
+ }
864
+ return null;
865
+ }
866
+ /**
867
+ * Resolves the class constructor behind a service definition.
868
+ *
869
+ * @param def - Service definition from the container
870
+ * @returns The constructor, or null for plain values
871
+ */
872
+ function resolveConstructor(def) {
873
+ const value = def.serviceValue;
874
+ if (typeof value === "function") return value;
875
+ if (typeof value === "object" && value !== null) {
876
+ const ctor = value.constructor;
877
+ if (ctor === Object || ctor === Array || typeof ctor !== "function") return null;
878
+ return ctor;
879
+ }
880
+ return null;
881
+ }
882
+ /**
883
+ * Collects every injection declaration of a class, inherited ones included.
884
+ *
885
+ * @param ctor - Class constructor to inspect
886
+ * @returns Dependency declarations, nearest prototype first
887
+ */
888
+ function collectClassDeps(ctor) {
889
+ if (!ctor?.prototype) return [];
890
+ const deps = [];
891
+ const seen = /* @__PURE__ */ new Set();
892
+ let proto = ctor.prototype;
893
+ while (proto && proto !== ROOT_PROTO) {
894
+ const entry = IOCEngine.getEntryForClass({ prototype: proto });
895
+ for (const dep of entry?.deps ?? []) {
896
+ if (seen.has(dep.propertyName)) continue;
897
+ seen.add(dep.propertyName);
898
+ deps.push(dep);
899
+ }
900
+ proto = Object.getPrototypeOf(proto);
901
+ }
902
+ return deps;
903
+ }
904
+ /**
905
+ * Maps a binding's factory type onto its description kind.
906
+ *
907
+ * @param type - Factory type recorded by the container
908
+ * @returns The matching kind
909
+ */
910
+ function toServiceKind(type) {
911
+ switch (type) {
912
+ case IOC.ServiceFactoryType.CLASS_SINGLETON: return "singleton";
913
+ case IOC.ServiceFactoryType.CLASS: return "transient";
914
+ default: return "instance";
915
+ }
916
+ }
917
+ /**
918
+ * Builds a serialisable description of a container.
919
+ *
920
+ * Reads `container.services`, which is binding metadata, never
921
+ * `getAllServices()`, which would construct every registered service. An
922
+ * inspector that instantiates what it inspects is not an inspector.
923
+ *
924
+ * @param container - The container to describe
925
+ * @param options - Optional annotation callback
926
+ * @returns Nodes, edges, cycles and the unused-service count
927
+ */
928
+ function describeContainer(container, options = {}) {
929
+ const services = container.services;
930
+ const ids = buildIdMap(services);
931
+ const nodes = [];
932
+ const edges = [];
933
+ const dependentCounts = /* @__PURE__ */ new Map();
934
+ for (const [key, def] of services) {
935
+ const id = ids.get(key);
936
+ const name = describeKey(key);
937
+ const ctor = resolveConstructor(def);
938
+ const dependencies = [];
939
+ for (const dep of collectClassDeps(ctor)) {
940
+ const targetId = ids.get(dep.dependency);
941
+ const targetName = describeKey(dep.dependency);
942
+ const optional = dep.type === IOC.DependencyType.OPTIONAL;
943
+ dependencies.push({
944
+ id: targetId ?? `unbound:${targetName}`,
945
+ name: targetName,
946
+ property: dep.propertyName,
947
+ optional,
948
+ bound: targetId !== void 0
949
+ });
950
+ if (targetId !== void 0) {
951
+ edges.push({
952
+ from: id,
953
+ to: targetId,
954
+ property: dep.propertyName,
955
+ optional
956
+ });
957
+ dependentCounts.set(targetId, (dependentCounts.get(targetId) ?? 0) + 1);
958
+ }
959
+ }
960
+ const node = {
961
+ id,
962
+ name,
963
+ kind: toServiceKind(def.type),
964
+ role: ctor ? "service" : "value",
965
+ implementation: describeImplementation(key, def.serviceValue),
966
+ instantiated: def.type === IOC.ServiceFactoryType.INSTANCE || container.hasInstance(key),
967
+ symbol: typeof key === "symbol",
968
+ dependencies,
969
+ dependents: 0
970
+ };
971
+ options.annotate?.(node, {
972
+ key,
973
+ def,
974
+ ctor
975
+ });
976
+ nodes.push(node);
977
+ }
978
+ for (const node of nodes) node.dependents = dependentCounts.get(node.id) ?? 0;
979
+ return {
980
+ context: container.context,
981
+ nodes,
982
+ edges,
983
+ cycles: findCycles(nodes, edges),
984
+ unusedCount: nodes.filter((node) => !node.instantiated).length
985
+ };
986
+ }
987
+ /**
988
+ * Assigns a stable, unique id to every service key.
989
+ *
990
+ * @param services - The container's service map
991
+ * @returns Map of service key to unique id
992
+ */
993
+ function buildIdMap(services) {
994
+ const ids = /* @__PURE__ */ new Map();
995
+ const used = /* @__PURE__ */ new Map();
996
+ for (const key of services.keys()) {
997
+ const base = describeKey(key);
998
+ const seen = used.get(base) ?? 0;
999
+ used.set(base, seen + 1);
1000
+ ids.set(key, seen === 0 ? base : `${base}#${seen}`);
1001
+ }
1002
+ return ids;
1003
+ }
1004
+ /**
1005
+ * Finds dependency cycles by depth-first search with colour marking.
1006
+ *
1007
+ * Each cycle is reported once, rotated to start at its lexicographically
1008
+ * smallest member so the same loop always serialises identically.
1009
+ *
1010
+ * @param nodes - Graph nodes
1011
+ * @param edges - Graph edges
1012
+ * @returns One ordered id list per cycle
1013
+ */
1014
+ function findCycles(nodes, edges) {
1015
+ const adjacency = /* @__PURE__ */ new Map();
1016
+ for (const node of nodes) adjacency.set(node.id, []);
1017
+ for (const edge of edges) adjacency.get(edge.from)?.push(edge.to);
1018
+ const state = /* @__PURE__ */ new Map();
1019
+ const path = [];
1020
+ const found = /* @__PURE__ */ new Map();
1021
+ const visit = (id) => {
1022
+ state.set(id, "visiting");
1023
+ path.push(id);
1024
+ for (const next of adjacency.get(id) ?? []) {
1025
+ const nextState = state.get(next);
1026
+ if (nextState === "visiting") {
1027
+ const cycle = path.slice(path.indexOf(next));
1028
+ const smallest = cycle.indexOf([...cycle].sort()[0]);
1029
+ const normalised = [...cycle.slice(smallest), ...cycle.slice(0, smallest)];
1030
+ found.set(normalised.join(" "), normalised);
1031
+ continue;
1032
+ }
1033
+ if (nextState === void 0) visit(next);
1034
+ }
1035
+ path.pop();
1036
+ state.set(id, "done");
1037
+ };
1038
+ for (const node of nodes) if (!state.has(node.id)) visit(node.id);
1039
+ return [...found.values()];
1040
+ }
1041
+ //#endregion
1042
+ export { BaseDecorator, Container, ContainerEvents, Destroy, IOC, IOCEngine, IOC_DEVTOOLS_HOOK_KEY, Identity, Init, Inject, InjectOptional, Injectable, addIOCDevtoolsHook, collectClassDeps, createDecorator, describeContainer, describeImplementation, describeKey, destroyContainer, destroyDecorators, getIOCDevtoolsHook, initializeContainer, initializeDecorators, resolveConstructor, setIOCDevtoolsHook, toServiceKind };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vercube/di",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "description": "Dependency Injection module for Vercube framework",
5
5
  "repository": {
6
6
  "type": "git",