@decohere/crucible 0.0.1-dev.112 → 0.0.1-dev.113

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/README.md CHANGED
@@ -10,7 +10,7 @@ Higher layers (for example Cortex `Builder.withServices`) register on a `Crucibl
10
10
  |---|---|
11
11
  | Constructor-injected services and controllers | Replacing Cortex / Protocol / Codex — those compose *on top of* Crucible |
12
12
  | Tokens for interfaces, config objects, and “many of this kind” | Auto-scanning folders or Nest-style modules (not in this package) |
13
- | Request / singleton / transient lifetimes and `using` disposal | Throwing on a missing dependency (unregistered keys resolve to `null`) |
13
+ | Request / singleton / transient lifetimes and `using` disposal | Compile-time graph checking (the graph is checked when a service is first resolved) |
14
14
 
15
15
  ## Install
16
16
 
@@ -40,7 +40,7 @@ Decorator metadata is required. Enable it in `tsconfig.json` (the Decohere scaff
40
40
 
41
41
  ## Constructor injection — `@inject` is usually unnecessary
42
42
 
43
- If the constructor parameter is a **concrete class**, Crucible reads `design:paramtypes` and resolves by the class name. Mark the *consumer* `@injectable()` so TypeScript/SWC emit that metadata. Do **not** add `@inject` for that parameter.
43
+ If the constructor parameter is a **concrete class**, Crucible reads `design:paramtypes` and resolves that class. Mark the *consumer* `@injectable()` so TypeScript/SWC emit that metadata. Do **not** add `@inject` for that parameter.
44
44
 
45
45
  ```ts
46
46
  import { injectable } from "@decohere/crucible/decorators";
@@ -68,7 +68,7 @@ const hello = scope.resolve(HelloController);
68
68
 
69
69
  ### Do not `import type` injected classes
70
70
 
71
- `import type` is erased at compile time. `emitDecoratorMetadata` then records `Object` (or nothing) instead of the constructor. Crucible looks up `"Object"`, finds no registration, and **returns `null`**. The first method call on that dependency throws (`Cannot read properties of null`).
71
+ `import type` is erased at compile time. `emitDecoratorMetadata` then records `Object` (or nothing) instead of the constructor. Crucible cannot tell what to inject and **throws `ResolveError`** when the consumer is resolved, naming the parameter and telling you to add `@inject`.
72
72
 
73
73
  ```ts
74
74
  // Wrong — SampleService does not exist at runtime, paramtypes collapse to Object
@@ -98,9 +98,9 @@ That pattern is used throughout Forge (`Build`, `Clean`, …). Same idea applies
98
98
  | Situation | Why `@inject` |
99
99
  |---|---|
100
100
  | Interface or type alias (`ISampleStore`) | No runtime class; paramtypes is `Object` |
101
- | String / symbol token | Registration key is not the class name |
102
- | Array of implementations (`Transformer[]`) | Paramtypes is `Array`; without a token, Crucible would `resolveMany("Array")` |
103
- | Paramtypes collapsed to `Object` (circular import, decorate-before-init) | Explicit token still resolves |
101
+ | String / symbol token | The service is registered under the token, not the class |
102
+ | Array of implementations (`Transformer[]`) | Paramtypes is `Array`, which says nothing about the element type |
103
+ | Paramtypes collapsed to `Object` (the class's import was dropped) | `@inject(TheClass)` uses the class as a value, so its import is kept |
104
104
 
105
105
  ```ts
106
106
  import { inject, injectable } from "@decohere/crucible/decorators";
@@ -127,13 +127,13 @@ builder.registerSingleton(SampleStoreToken, PostgresSampleStore);
127
127
  builder.registerSingleton(SampleStoreToken, store);
128
128
  ```
129
129
 
130
- `@inject` takes `string | symbol | Constructable`. Prefer `@inject(SampleService)` over `@inject("SampleService")` — the class tracks renames. There is no `@inject({ many, optional })` yet — arrays use `resolveMany` automatically when the reflected type is `Array`, and missing keys are `null` (see [Optional / missing](#optional--missing-dependencies)).
130
+ `@inject` takes `string | symbol | Constructable`. To inject a class, pass the class: `@inject("SampleService")` does **not** find `SampleService` (see [Keys](#registration-and-lifetimes)). Arrays use `resolveMany` automatically when the reflected type is `Array`. For a dependency that may be absent, add `@optional()` (see [Optional / missing](#optional--missing-dependencies)).
131
131
 
132
132
  ---
133
133
 
134
134
  ## Registration and lifetimes
135
135
 
136
- Keys are the class **name**, a string, or a symbol’s **description**. `resolve(SampleService)`, `resolve("SampleService")`, `@inject(SampleService)`, and `@inject("SampleService")` hit the same registration.
136
+ Keys match by **identity** ([ADR 0013](../../docs/adr/0013-crucible-keys-and-lifetimes.md)): a class matches only itself, a symbol only itself, a string only an equal string. So `resolve("SampleService")` does not find `registerSingleton(SampleService)`, `Symbol("TokenService")` does not collide with a class `TokenService`, two classes with the same name in different modules stay apart, and minified class names still work.
137
137
 
138
138
  Registering the same class (or the same type on a token) twice **throws** `RegistrationError` — silent last-wins hides accidental double-registers. Replace it explicitly:
139
139
 
@@ -150,6 +150,10 @@ builder.override.registerTransient(SampleService);
150
150
  | Request | On the current scope — new instance per `beginScope` |
151
151
  | Transient | Never — new instance every `resolve` |
152
152
 
153
+ A singleton may not depend on a request-scoped service, directly or through transients: it would keep the first request's instance forever (a "captive dependency", e.g. one database connection for the life of the process). Resolving one throws `ResolveError` naming the chain. Make the consumer request-scoped, or inject `LifetimeScopeToken` and resolve the request-scoped service per request. `LifetimeScopeToken` itself is allowed: a singleton gets the root container.
154
+
155
+ A dependency cycle (`A → B → A`) throws `ResolveError` listing the cycle.
156
+
153
157
  ```ts
154
158
  builder.registerSingleton(SampleService);
155
159
  builder.registerSingleton(Token, SampleService);
@@ -167,7 +171,7 @@ builder.registerTransient(Token, Helper);
167
171
 
168
172
  Cortex HTTP: controllers are **request-scoped** (one per request). Services are usually **singletons**.
169
173
 
170
- `registerSingleton(Class, instance)` binds an existing object under the class name. Terminal does this for the workspace:
174
+ `registerSingleton(Class, instance)` binds an existing object under the class. The CLI does this for the workspace:
171
175
 
172
176
  ```ts
173
177
  builder.registerSingleton(workspace.constructor, workspace);
@@ -216,7 +220,7 @@ await using scope = container.beginScope("req-2");
216
220
  // async teardown via Symbol.asyncDispose if the instance implements it
217
221
  ```
218
222
 
219
- - Child scopes copy the parent **registry** when created; they do not share the request-instance cache.
223
+ - Child scopes look registrations up in their parent (nothing is copied, so `beginScope` costs the same however many services are registered); they do not share the request-instance cache.
220
224
  - Nested `beginScope` is allowed; each level has its own request instances.
221
225
  - Named scopes: a live duplicate name throws `DuplicateScopeError`. After dispose, the name can be reused. Unnamed scopes do not collide.
222
226
  - Disposing a parent does **not** dispose child scopes. Dispose the scope you created.
@@ -249,19 +253,19 @@ Arrow factories are called as functions (not `new`). Use them when construction
249
253
 
250
254
  ### Optional / missing dependencies
251
255
 
252
- Unregistered keys do **not** throw. `resolve` returns `null` and logs a warning. Optional constructor params (`dep?: Foo`) still go through resolution — you get `null` if nothing is registered.
256
+ A missing registration **throws** `ResolveError`, naming the chain that needed it (`App → Router → Symbol(AuthorizationService)`). `resolveMany` returns `[]` instead. A `?` on the parameter type does not make it optional to the container; add `@optional()`, and the container passes `undefined` (so a default value applies):
253
257
 
254
258
  ```ts
259
+ import { inject, injectable, optional } from "@decohere/crucible/decorators";
260
+
255
261
  @injectable()
256
262
  export class App {
257
263
  public constructor(
258
- @inject(HttpProviderToken) private readonly http?: HttpProvider,
264
+ @inject(HttpProviderToken) @optional() private readonly http?: HttpProvider,
259
265
  ) {}
260
266
  }
261
267
  ```
262
268
 
263
- If a concrete class parameter is `null` at runtime, the usual cause is metadata loss (`import type`, missing `@injectable()`, or paramtypes `Object`) — not an optional registration.
264
-
265
269
  ### Cortex HTTP apps
266
270
 
267
271
  ```ts
@@ -309,7 +313,7 @@ To assert constructor arguments under DI, use `spyOnConstructor` from `@decohere
309
313
  | `IContainer` / `LifetimeScope` / `ContainerBuilder` / `ServiceProvider` | Composition interfaces |
310
314
  | `Scopes` | `{ request, singleton: "single-instance", transient }` |
311
315
  | `LifetimeScopeToken` | Symbol for the current scope |
312
- | `ResolveError` | `resolve` when multiple registrations share a key |
316
+ | `ResolveError` | Nothing registered, several registrations for `resolve`, a cycle, a captive dependency, or a parameter with no usable type |
313
317
  | `DuplicateScopeError` | `beginScope` with a live duplicate name |
314
318
  | `RegistrationError` | Same type registered twice without `builder.override` |
315
319
 
@@ -318,19 +322,20 @@ To assert constructor arguments under DI, use `spyOnConstructor` from `@decohere
318
322
  | Export | Role |
319
323
  |---|---|
320
324
  | `injectable()` | Empty class decorator; emit `design:paramtypes` |
321
- | `inject(key)` | Parameter decorator; resolve by string or symbol instead of the reflected class name |
325
+ | `inject(key)` | Parameter decorator; resolve this key instead of the reflected type |
326
+ | `optional()` | Parameter decorator; pass `undefined` when nothing is registered instead of throwing |
322
327
 
323
328
  ---
324
329
 
325
330
  ## Pitfalls
326
331
 
327
- 1. **`import type` on an injected class** — paramtypes become `Object` → `resolve` returns `null`. Use a value import.
332
+ 1. **`import type` on an injected class** — paramtypes become `Object` → `ResolveError` asking for `@inject`. Use a value import, or `@inject(TheClass)`.
328
333
  2. **Missing `@injectable()` (or any decorator) on the consumer** — no paramtypes, constructor is called with zero arguments.
329
334
  3. **`resolve` of a multi-bound token** — throws; use `resolveMany` or inject `T[]` with `@inject(token)`.
330
335
  4. **Same type registered twice** — throws `RegistrationError`. Use `builder.override.registerSingleton(...)` (tests, plugin replacements).
331
- 5. **Missing registration** — `null`, not an exception. Null-deref on first use is the symptom.
332
- 6. **Symbol keys** — the lookup key is `symbol.description`. Keep descriptions unique and stable.
333
- 7. **Renaming a class** without a token — the registration key is `Class.name`. Tokens isolate you from renames.
336
+ 5. **A string key for a class** — `@inject("SampleService")` does not find `SampleService`. Pass the class.
337
+ 6. **Two copies of a package** — the class from one copy is not the class from the other, so a service registered with one is not found with the other. Install each package once.
338
+ 7. **A singleton depending on a request-scoped service** — throws; see [lifetimes](#registration-and-lifetimes).
334
339
 
335
340
  ---
336
341
 
@@ -338,7 +343,6 @@ To assert constructor arguments under DI, use `spyOnConstructor` from `@decohere
338
343
 
339
344
  - Folder scanning / “declare a module and everything is registered”
340
345
  - Property or setter injection (constructors only)
341
- - `@inject({ many, optional })` metadata (not implemented; arrays and `null` cover the current cases)
342
346
  - Compile-time graph checking
343
347
 
344
348
  ## Dependencies
@@ -346,6 +350,5 @@ To assert constructor arguments under DI, use `spyOnConstructor` from `@decohere
346
350
  | Package | Role |
347
351
  |---|---|
348
352
  | [`@decohere/nexus`](../nexus) | Reflection helpers for `design:paramtypes` (`getMeta`, `isConstructable`, …) |
349
- | [`@decohere/scribe`](../scribe/README.md) | Logger facade |
350
353
 
351
354
  Layering: [architecture](../../docs/architecture/).
@@ -2,24 +2,39 @@
2
2
  Object.defineProperty(exports, "__esModule", {
3
3
  value: true
4
4
  });
5
- Object.defineProperty(exports, "inject", {
6
- enumerable: true,
7
- get: function() {
5
+ function _export(target, all) {
6
+ for(var name in all)Object.defineProperty(target, name, {
7
+ enumerable: true,
8
+ get: Object.getOwnPropertyDescriptor(all, name).get
9
+ });
10
+ }
11
+ _export(exports, {
12
+ get inject () {
8
13
  return inject;
14
+ },
15
+ get optional () {
16
+ return optional;
9
17
  }
10
18
  });
11
19
  const _reflection = require("@decohere/nexus/reflection");
12
20
  const _const = require("../const.js");
13
21
  function inject(key) {
14
- return (target, _propertyKey, idx)=>{
15
- const metadata = getExistingMeta(target);
16
- metadata.set(idx, {
22
+ return updateToken((token)=>({
23
+ ...token,
17
24
  key
18
- });
25
+ }));
26
+ }
27
+ function optional() {
28
+ return updateToken((token)=>({
29
+ ...token,
30
+ optional: true
31
+ }));
32
+ }
33
+ function updateToken(update) {
34
+ return (target, _propertyKey, idx)=>{
35
+ // Copy: the metadata can be inherited from a base class, which must not change.
36
+ const metadata = new Map((0, _reflection.getMeta)(_const.CrucibleMeta.injecttoken, target));
37
+ metadata.set(idx, update(metadata.get(idx)));
19
38
  (0, _reflection.defineMeta)(_const.CrucibleMeta.injecttoken, metadata, target);
20
39
  };
21
40
  }
22
- function getExistingMeta(target) {
23
- const meta = (0, _reflection.getMeta)(_const.CrucibleMeta.injecttoken, target) ?? new Map();
24
- return meta;
25
- }
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", {
3
+ value: true
4
+ });
5
+ Object.defineProperty(exports, "describeKey", {
6
+ enumerable: true,
7
+ get: function() {
8
+ return describeKey;
9
+ }
10
+ });
11
+ function describeKey(key) {
12
+ switch(typeof key){
13
+ case "string":
14
+ return `"${key}"`;
15
+ case "symbol":
16
+ return key.toString();
17
+ default:
18
+ return key.name || "(anonymous factory)";
19
+ }
20
+ }
@@ -20,7 +20,7 @@ class CrucibleBuilder {
20
20
  return this.createRegistrar(true);
21
21
  }
22
22
  registerType(type, scope = _const.Scopes.transient) {
23
- return this.register(false)(scope)(type.name, type);
23
+ return this.register(false)(scope)(type, type);
24
24
  }
25
25
  registerTypes(scopeOrType, ...types) {
26
26
  return this.registerMany(false, scopeOrType, types);
@@ -52,12 +52,12 @@ class CrucibleBuilder {
52
52
  scopeOrType,
53
53
  ...types
54
54
  ];
55
- return allTypes.map((type)=>bind(scope)(type.name, type));
55
+ return allTypes.map((type)=>bind(scope)(type, type));
56
56
  }
57
57
  createRegistrar(override) {
58
58
  const bind = this.register(override);
59
59
  return {
60
- registerType: (type, scope = _const.Scopes.transient)=>bind(scope)(type.name, type),
60
+ registerType: (type, scope = _const.Scopes.transient)=>bind(scope)(type, type),
61
61
  registerTypes: (scopeOrType, ...types)=>this.registerMany(override, scopeOrType, types),
62
62
  registerTransient: (typeOrKey, type)=>bind(_const.Scopes.transient)(typeOrKey, type),
63
63
  registerSingleton: (typeOrKey, typeOrInstance)=>bind(_const.Scopes.singleton)(typeOrKey, typeOrInstance),
@@ -9,11 +9,19 @@ Object.defineProperty(exports, "Crucible", {
9
9
  }
10
10
  });
11
11
  const _reflection = require("@decohere/nexus/reflection");
12
- const _scribe = require("@decohere/scribe");
13
12
  const _const = require("../const.js");
14
- const _registrationkeymodel = require("../models/registration-key.model.js");
13
+ const _servicekey = require("../models/service-key.js");
15
14
  const _errors = require("../types/errors.js");
16
15
  const _serviceregistry = require("./service-registry.js");
16
+ function describePath(path, last) {
17
+ const keys = path.map((registration)=>registration.key);
18
+ return [
19
+ ...keys,
20
+ ...last == null ? [] : [
21
+ last
22
+ ]
23
+ ].map(_servicekey.describeKey).join(" → ");
24
+ }
17
25
  class Crucible {
18
26
  key;
19
27
  instances = new Map();
@@ -21,7 +29,6 @@ class Crucible {
21
29
  parent;
22
30
  rootScope;
23
31
  scopes = new Map();
24
- logger = _scribe.scribe.logger(Crucible.name);
25
32
  isDisposed = false;
26
33
  constructor(options){
27
34
  this.key = options?.key;
@@ -31,27 +38,26 @@ class Crucible {
31
38
  this.rootScope = options?.parent?.rootScope ?? this;
32
39
  }
33
40
  resolveMany(resolveRequest) {
34
- const key = new _registrationkeymodel.RegistrationKey(resolveRequest);
35
- const registrations = this.getRegistrationFrom(key) ?? [];
36
- const services = Array.of();
37
- for (const registration of registrations){
38
- const service = this.resolveRegistration(registration);
39
- services.push(service);
40
- }
41
- return services;
42
- }
43
- resolve(resolveRequest) {
44
- const key = new _registrationkeymodel.RegistrationKey(resolveRequest);
45
- const registration = this.getRegistrationFrom(key);
46
- if (registration == null) {
47
- this.logger.warn(`Cannot resolve dependency ${key.toString()}`);
48
- return null;
41
+ return this.resolveAll(resolveRequest, []);
42
+ }
43
+ /** Throws {@link ResolveError} when nothing is registered for the key. */ resolve(resolveRequest) {
44
+ return this.resolveOne(resolveRequest, [], false);
45
+ }
46
+ resolveAll(key, path) {
47
+ const registrations = this.serviceRegistry.getRegistration(key) ?? [];
48
+ return registrations.map((registration)=>this.resolveRegistration(registration, path));
49
+ }
50
+ resolveOne(key, path, optional) {
51
+ const registrations = this.serviceRegistry.getRegistration(key);
52
+ if (registrations == null || registrations.length === 0) {
53
+ if (optional) return undefined;
54
+ const requiredBy = path.length === 0 ? "" : ` (required by ${describePath(path, key)})`;
55
+ throw new _errors.ResolveError(`Nothing is registered for ${(0, _servicekey.describeKey)(key)}${requiredBy}. Register it, or mark the parameter @optional().`);
49
56
  }
50
- if (registration.length > 1) {
51
- throw new _errors.ResolveError(`There are multiple services registered with key ${key.toString()}. Use resolveMany to resolve all implementations.`);
57
+ if (registrations.length > 1) {
58
+ throw new _errors.ResolveError(`There are multiple services registered with key ${(0, _servicekey.describeKey)(key)}. Use resolveMany to resolve all implementations.`);
52
59
  }
53
- const service = this.resolveRegistration(registration[0]);
54
- return service;
60
+ return this.resolveRegistration(registrations[0], path);
55
61
  }
56
62
  beginScope(name) {
57
63
  if (this.isDisposed) {
@@ -120,22 +126,35 @@ class Crucible {
120
126
  }
121
127
  }
122
128
  }
123
- getRegistrationFrom(key) {
124
- const registration = this.serviceRegistry.getRegistration(key.value);
125
- return registration;
126
- }
127
- resolveRegistration(registration) {
129
+ resolveRegistration(registration, path) {
130
+ this.assertNoCycle(registration, path);
131
+ this.assertNotCaptive(registration, path);
128
132
  const { scope } = registration;
129
133
  switch(scope){
130
134
  case _const.Scopes.transient:
131
- return this.createInstance(registration);
135
+ return this.createInstance(registration, path);
132
136
  case _const.Scopes.singleton:
133
- return this.rootScope.getOrCreateInstance(registration);
137
+ return this.rootScope.getOrCreateInstance(registration, path);
134
138
  case _const.Scopes.request:
135
- return this.getOrCreateInstance(registration);
139
+ return this.getOrCreateInstance(registration, path);
136
140
  }
137
141
  }
138
- getOrCreateInstance(registration) {
142
+ assertNoCycle(registration, path) {
143
+ if (!path.includes(registration)) return;
144
+ throw new _errors.ResolveError(`Circular dependency: ${describePath(path, registration.key)}. Break the cycle, e.g. by resolving one side lazily from the LifetimeScope.`);
145
+ }
146
+ /**
147
+ * A singleton lives for the whole process, so a request-scoped service it depends on (directly
148
+ * or through transients) would be created once and then shared by every request, e.g. one
149
+ * QueryRunner for the life of the process. The container's own lifetime scope is exempt: a
150
+ * singleton asking for it gets the root, which is the scope it lives in.
151
+ */ assertNotCaptive(registration, path) {
152
+ if (registration.scope !== _const.Scopes.request || registration.key === _const.LifetimeScopeToken) return;
153
+ const singleton = path.find((owner)=>owner.scope === _const.Scopes.singleton);
154
+ if (singleton == null) return;
155
+ throw new _errors.ResolveError(`Singleton ${(0, _servicekey.describeKey)(singleton.key)} depends on request-scoped ${(0, _servicekey.describeKey)(registration.key)} (${describePath(path, registration.key)}). A singleton would keep the first request's instance forever: make ${(0, _servicekey.describeKey)(singleton.key)} request-scoped, or resolve ${(0, _servicekey.describeKey)(registration.key)} per request from the LifetimeScope.`);
156
+ }
157
+ getOrCreateInstance(registration, path) {
139
158
  if (this.isDisposed) {
140
159
  throw new Error("This container has been disposed");
141
160
  }
@@ -146,31 +165,32 @@ class Crucible {
146
165
  if (existingInstance != null) {
147
166
  return existingInstance;
148
167
  }
149
- const instance = isInstance ? type : this.createInstance(registration);
168
+ const instance = isInstance ? type : this.createInstance(registration, path);
150
169
  this.instances.set(cacheKey, instance);
151
170
  return instance;
152
171
  }
153
- createInstance(registration) {
172
+ createInstance(registration, path) {
154
173
  const type = registration.type;
155
174
  const canConstruct = (0, _reflection.isConstructable)(type);
156
- const deps = this.resolveDependencies(type);
175
+ const deps = this.resolveDependencies(type, [
176
+ ...path,
177
+ registration
178
+ ]);
157
179
  const inst = canConstruct ? new type(...deps) : type();
158
180
  return inst;
159
181
  }
160
- resolveDependencies(type) {
182
+ resolveDependencies(type, path) {
161
183
  const params = (0, _reflection.getMeta)(_reflection.ReflectionMeta.paramtypes, type) ?? [];
162
184
  const injectTokens = (0, _reflection.getMeta)(_const.CrucibleMeta.injecttoken, type) ?? new Map();
163
- //TODO: put a try catch here so we can throw the name of the service
164
- let resolved = [];
165
- for(let i = 0; i < params.length; i++){
166
- const token = injectTokens.get(i)?.key ?? params[i].name;
167
- const dep = (0, _reflection.isArrayType)(params[i]) ? this.resolveMany(token) : this.resolve(token);
168
- resolved = [
169
- ...resolved,
170
- dep
171
- ];
172
- }
173
- return resolved;
185
+ return params.map((param, index)=>{
186
+ const token = injectTokens.get(index);
187
+ const key = token?.key ?? param;
188
+ if (key == null || key === Object || token?.key == null && (0, _reflection.isArrayType)(param)) {
189
+ if (token?.optional) return undefined;
190
+ throw new _errors.ResolveError(`Cannot tell what to inject for constructor parameter ${index} of ${describePath(path)}: its type is not a class at run time (an interface, an array, or a type-only import). Add @inject(token) to the parameter.`);
191
+ }
192
+ return (0, _reflection.isArrayType)(param) ? this.resolveAll(key, path) : this.resolveOne(key, path, token?.optional === true);
193
+ });
174
194
  }
175
195
  releaseScope(name) {
176
196
  if (name == null) {
@@ -8,29 +8,30 @@ Object.defineProperty(exports, "ServiceRegistry", {
8
8
  return ServiceRegistry;
9
9
  }
10
10
  });
11
- const _registrationkeymodel = require("../models/registration-key.model.js");
11
+ const _servicekey = require("../models/service-key.js");
12
12
  const _serviceregistrationmodel = require("../models/service-registration.model.js");
13
13
  const _errors = require("../types/errors.js");
14
14
  class ServiceRegistry {
15
- registrations;
15
+ #registrations = new Map();
16
+ #parent;
16
17
  constructor(parent){
17
- this.registrations = new Map(parent?.registrations);
18
+ this.#parent = parent;
18
19
  }
19
20
  registerType(scope, options) {
20
21
  const registration = this.createRegistrationFrom(scope, options);
21
22
  // Override means "this key now resolves to this", e.g. a test fake: replace every binding.
22
23
  if (options.override) {
23
- this.setRegistration(registration.key, [
24
+ this.#registrations.set(registration.key, [
24
25
  registration
25
26
  ]);
26
27
  return registration;
27
28
  }
28
- const existing = this.getRegistrations(registration.key) ?? [];
29
+ const existing = this.getRegistration(registration.key) ?? [];
29
30
  const duplicate = existing.find((reg)=>reg.isEqual(registration));
30
31
  if (duplicate != null) {
31
- throw new _errors.RegistrationError(`A service is already registered for key ${registration.key}. Use builder.override.registerSingleton() (or the matching register* method) to replace it.`);
32
+ throw new _errors.RegistrationError(`A service is already registered for key ${(0, _servicekey.describeKey)(registration.key)}. Use builder.override.registerSingleton() (or the matching register* method) to replace it.`);
32
33
  }
33
- this.setRegistration(registration.key, [
34
+ this.#registrations.set(registration.key, [
34
35
  ...existing,
35
36
  registration
36
37
  ]);
@@ -38,30 +39,19 @@ class ServiceRegistry {
38
39
  }
39
40
  replaceType(scope, options) {
40
41
  const registration = this.createRegistrationFrom(scope, options);
41
- this.setRegistration(registration.key, [
42
+ this.#registrations.set(registration.key, [
42
43
  registration
43
44
  ]);
44
45
  return registration;
45
46
  }
46
47
  getRegistration(key) {
47
- const keyVal = new _registrationkeymodel.RegistrationKey(key).value;
48
- const registration = this.registrations.get(keyVal);
49
- return registration;
48
+ return this.#registrations.get(key) ?? this.#parent?.getRegistration(key);
50
49
  }
51
50
  createRegistrationFrom(scope, options) {
52
51
  const { typeOrKey, typeOrInstance } = options;
53
52
  const isKey = typeof typeOrKey === "string" || typeof typeOrKey === "symbol";
54
53
  const isInstance = typeof typeOrInstance === "object";
55
- const key = new _registrationkeymodel.RegistrationKey(!isKey ? typeOrKey.name : typeOrKey);
56
54
  const type = isInstance ? typeOrInstance : isKey ? typeOrInstance : typeOrKey;
57
- const registration = new _serviceregistrationmodel.ServiceRegistration(key, type, scope, isInstance);
58
- return registration;
59
- }
60
- getRegistrations(key) {
61
- const value = key.value;
62
- return this.registrations.get(value);
63
- }
64
- setRegistration(key, registrations) {
65
- this.registrations.set(key.value, registrations);
55
+ return new _serviceregistrationmodel.ServiceRegistration(typeOrKey, type, scope, isInstance);
66
56
  }
67
57
  }
@@ -1,19 +1,30 @@
1
1
  import { defineMeta, getMeta } from "@decohere/nexus/reflection";
2
2
  import { CrucibleMeta } from "../const.js";
3
3
  /**
4
- * Bind a constructor parameter to a registration key.
5
- * A class uses `Class.name` (same key as `registerSingleton(Class)`).
6
- * Prefer the class over a name string. Use a symbol or string when the key is not the class name.
4
+ * Bind a constructor parameter to a registration key. Keys match by identity: a class matches only
5
+ * itself (not a string of its name), and a symbol only itself. Prefer the class; use a symbol for an
6
+ * interface or several implementations. `@inject(SomeClass)` also keeps SomeClass's import, which
7
+ * a parameter used only as a type can lose (its run-time type then collapses to `Object`).
7
8
  */ export function inject(key) {
8
- return (target, _propertyKey, idx)=>{
9
- const metadata = getExistingMeta(target);
10
- metadata.set(idx, {
9
+ return updateToken((token)=>({
10
+ ...token,
11
11
  key
12
- });
12
+ }));
13
+ }
14
+ /**
15
+ * Mark a constructor parameter as optional: when nothing is registered for it, the container
16
+ * passes `undefined` (so a default value applies) instead of throwing.
17
+ */ export function optional() {
18
+ return updateToken((token)=>({
19
+ ...token,
20
+ optional: true
21
+ }));
22
+ }
23
+ function updateToken(update) {
24
+ return (target, _propertyKey, idx)=>{
25
+ // Copy: the metadata can be inherited from a base class, which must not change.
26
+ const metadata = new Map(getMeta(CrucibleMeta.injecttoken, target));
27
+ metadata.set(idx, update(metadata.get(idx)));
13
28
  defineMeta(CrucibleMeta.injecttoken, metadata, target);
14
29
  };
15
30
  }
16
- function getExistingMeta(target) {
17
- const meta = getMeta(CrucibleMeta.injecttoken, target) ?? new Map();
18
- return meta;
19
- }
@@ -0,0 +1,10 @@
1
+ /** A readable name for a key, for error messages. */ export function describeKey(key) {
2
+ switch(typeof key){
3
+ case "string":
4
+ return `"${key}"`;
5
+ case "symbol":
6
+ return key.toString();
7
+ default:
8
+ return key.name || "(anonymous factory)";
9
+ }
10
+ }
@@ -10,7 +10,7 @@ export class CrucibleBuilder {
10
10
  return this.createRegistrar(true);
11
11
  }
12
12
  registerType(type, scope = Scopes.transient) {
13
- return this.register(false)(scope)(type.name, type);
13
+ return this.register(false)(scope)(type, type);
14
14
  }
15
15
  registerTypes(scopeOrType, ...types) {
16
16
  return this.registerMany(false, scopeOrType, types);
@@ -42,12 +42,12 @@ export class CrucibleBuilder {
42
42
  scopeOrType,
43
43
  ...types
44
44
  ];
45
- return allTypes.map((type)=>bind(scope)(type.name, type));
45
+ return allTypes.map((type)=>bind(scope)(type, type));
46
46
  }
47
47
  createRegistrar(override) {
48
48
  const bind = this.register(override);
49
49
  return {
50
- registerType: (type, scope = Scopes.transient)=>bind(scope)(type.name, type),
50
+ registerType: (type, scope = Scopes.transient)=>bind(scope)(type, type),
51
51
  registerTypes: (scopeOrType, ...types)=>this.registerMany(override, scopeOrType, types),
52
52
  registerTransient: (typeOrKey, type)=>bind(Scopes.transient)(typeOrKey, type),
53
53
  registerSingleton: (typeOrKey, typeOrInstance)=>bind(Scopes.singleton)(typeOrKey, typeOrInstance),
@@ -1,9 +1,17 @@
1
1
  import { getMeta, isArrayType, isConstructable, ReflectionMeta } from "@decohere/nexus/reflection";
2
- import { scribe } from "@decohere/scribe";
3
2
  import { CrucibleMeta, LifetimeScopeToken, Scopes } from "../const.js";
4
- import { RegistrationKey } from "../models/registration-key.model.js";
3
+ import { describeKey } from "../models/service-key.js";
5
4
  import { DuplicateScopeError, ResolveError } from "../types/errors.js";
6
5
  import { ServiceRegistry } from "./service-registry.js";
6
+ function describePath(path, last) {
7
+ const keys = path.map((registration)=>registration.key);
8
+ return [
9
+ ...keys,
10
+ ...last == null ? [] : [
11
+ last
12
+ ]
13
+ ].map(describeKey).join(" → ");
14
+ }
7
15
  export class Crucible {
8
16
  key;
9
17
  instances = new Map();
@@ -11,7 +19,6 @@ export class Crucible {
11
19
  parent;
12
20
  rootScope;
13
21
  scopes = new Map();
14
- logger = scribe.logger(Crucible.name);
15
22
  isDisposed = false;
16
23
  constructor(options){
17
24
  this.key = options?.key;
@@ -21,27 +28,26 @@ export class Crucible {
21
28
  this.rootScope = options?.parent?.rootScope ?? this;
22
29
  }
23
30
  resolveMany(resolveRequest) {
24
- const key = new RegistrationKey(resolveRequest);
25
- const registrations = this.getRegistrationFrom(key) ?? [];
26
- const services = Array.of();
27
- for (const registration of registrations){
28
- const service = this.resolveRegistration(registration);
29
- services.push(service);
30
- }
31
- return services;
32
- }
33
- resolve(resolveRequest) {
34
- const key = new RegistrationKey(resolveRequest);
35
- const registration = this.getRegistrationFrom(key);
36
- if (registration == null) {
37
- this.logger.warn(`Cannot resolve dependency ${key.toString()}`);
38
- return null;
31
+ return this.resolveAll(resolveRequest, []);
32
+ }
33
+ /** Throws {@link ResolveError} when nothing is registered for the key. */ resolve(resolveRequest) {
34
+ return this.resolveOne(resolveRequest, [], false);
35
+ }
36
+ resolveAll(key, path) {
37
+ const registrations = this.serviceRegistry.getRegistration(key) ?? [];
38
+ return registrations.map((registration)=>this.resolveRegistration(registration, path));
39
+ }
40
+ resolveOne(key, path, optional) {
41
+ const registrations = this.serviceRegistry.getRegistration(key);
42
+ if (registrations == null || registrations.length === 0) {
43
+ if (optional) return undefined;
44
+ const requiredBy = path.length === 0 ? "" : ` (required by ${describePath(path, key)})`;
45
+ throw new ResolveError(`Nothing is registered for ${describeKey(key)}${requiredBy}. Register it, or mark the parameter @optional().`);
39
46
  }
40
- if (registration.length > 1) {
41
- throw new ResolveError(`There are multiple services registered with key ${key.toString()}. Use resolveMany to resolve all implementations.`);
47
+ if (registrations.length > 1) {
48
+ throw new ResolveError(`There are multiple services registered with key ${describeKey(key)}. Use resolveMany to resolve all implementations.`);
42
49
  }
43
- const service = this.resolveRegistration(registration[0]);
44
- return service;
50
+ return this.resolveRegistration(registrations[0], path);
45
51
  }
46
52
  beginScope(name) {
47
53
  if (this.isDisposed) {
@@ -110,22 +116,35 @@ export class Crucible {
110
116
  }
111
117
  }
112
118
  }
113
- getRegistrationFrom(key) {
114
- const registration = this.serviceRegistry.getRegistration(key.value);
115
- return registration;
116
- }
117
- resolveRegistration(registration) {
119
+ resolveRegistration(registration, path) {
120
+ this.assertNoCycle(registration, path);
121
+ this.assertNotCaptive(registration, path);
118
122
  const { scope } = registration;
119
123
  switch(scope){
120
124
  case Scopes.transient:
121
- return this.createInstance(registration);
125
+ return this.createInstance(registration, path);
122
126
  case Scopes.singleton:
123
- return this.rootScope.getOrCreateInstance(registration);
127
+ return this.rootScope.getOrCreateInstance(registration, path);
124
128
  case Scopes.request:
125
- return this.getOrCreateInstance(registration);
129
+ return this.getOrCreateInstance(registration, path);
126
130
  }
127
131
  }
128
- getOrCreateInstance(registration) {
132
+ assertNoCycle(registration, path) {
133
+ if (!path.includes(registration)) return;
134
+ throw new ResolveError(`Circular dependency: ${describePath(path, registration.key)}. Break the cycle, e.g. by resolving one side lazily from the LifetimeScope.`);
135
+ }
136
+ /**
137
+ * A singleton lives for the whole process, so a request-scoped service it depends on (directly
138
+ * or through transients) would be created once and then shared by every request, e.g. one
139
+ * QueryRunner for the life of the process. The container's own lifetime scope is exempt: a
140
+ * singleton asking for it gets the root, which is the scope it lives in.
141
+ */ assertNotCaptive(registration, path) {
142
+ if (registration.scope !== Scopes.request || registration.key === LifetimeScopeToken) return;
143
+ const singleton = path.find((owner)=>owner.scope === Scopes.singleton);
144
+ if (singleton == null) return;
145
+ throw new ResolveError(`Singleton ${describeKey(singleton.key)} depends on request-scoped ${describeKey(registration.key)} (${describePath(path, registration.key)}). A singleton would keep the first request's instance forever: make ${describeKey(singleton.key)} request-scoped, or resolve ${describeKey(registration.key)} per request from the LifetimeScope.`);
146
+ }
147
+ getOrCreateInstance(registration, path) {
129
148
  if (this.isDisposed) {
130
149
  throw new Error("This container has been disposed");
131
150
  }
@@ -136,31 +155,32 @@ export class Crucible {
136
155
  if (existingInstance != null) {
137
156
  return existingInstance;
138
157
  }
139
- const instance = isInstance ? type : this.createInstance(registration);
158
+ const instance = isInstance ? type : this.createInstance(registration, path);
140
159
  this.instances.set(cacheKey, instance);
141
160
  return instance;
142
161
  }
143
- createInstance(registration) {
162
+ createInstance(registration, path) {
144
163
  const type = registration.type;
145
164
  const canConstruct = isConstructable(type);
146
- const deps = this.resolveDependencies(type);
165
+ const deps = this.resolveDependencies(type, [
166
+ ...path,
167
+ registration
168
+ ]);
147
169
  const inst = canConstruct ? new type(...deps) : type();
148
170
  return inst;
149
171
  }
150
- resolveDependencies(type) {
172
+ resolveDependencies(type, path) {
151
173
  const params = getMeta(ReflectionMeta.paramtypes, type) ?? [];
152
174
  const injectTokens = getMeta(CrucibleMeta.injecttoken, type) ?? new Map();
153
- //TODO: put a try catch here so we can throw the name of the service
154
- let resolved = [];
155
- for(let i = 0; i < params.length; i++){
156
- const token = injectTokens.get(i)?.key ?? params[i].name;
157
- const dep = isArrayType(params[i]) ? this.resolveMany(token) : this.resolve(token);
158
- resolved = [
159
- ...resolved,
160
- dep
161
- ];
162
- }
163
- return resolved;
175
+ return params.map((param, index)=>{
176
+ const token = injectTokens.get(index);
177
+ const key = token?.key ?? param;
178
+ if (key == null || key === Object || token?.key == null && isArrayType(param)) {
179
+ if (token?.optional) return undefined;
180
+ throw new ResolveError(`Cannot tell what to inject for constructor parameter ${index} of ${describePath(path)}: its type is not a class at run time (an interface, an array, or a type-only import). Add @inject(token) to the parameter.`);
181
+ }
182
+ return isArrayType(param) ? this.resolveAll(key, path) : this.resolveOne(key, path, token?.optional === true);
183
+ });
164
184
  }
165
185
  releaseScope(name) {
166
186
  if (name == null) {
@@ -1,26 +1,31 @@
1
- import { RegistrationKey } from "../models/registration-key.model.js";
1
+ import { describeKey } from "../models/service-key.js";
2
2
  import { ServiceRegistration } from "../models/service-registration.model.js";
3
3
  import { RegistrationError } from "../types/errors.js";
4
- export class ServiceRegistry {
5
- registrations;
4
+ /**
5
+ * Registrations by key. A scope's registry holds only what the scope adds (its own lifetime
6
+ * scope) and looks everything else up in its parent, so beginning a scope costs the same however
7
+ * many services are registered.
8
+ */ export class ServiceRegistry {
9
+ #registrations = new Map();
10
+ #parent;
6
11
  constructor(parent){
7
- this.registrations = new Map(parent?.registrations);
12
+ this.#parent = parent;
8
13
  }
9
14
  registerType(scope, options) {
10
15
  const registration = this.createRegistrationFrom(scope, options);
11
16
  // Override means "this key now resolves to this", e.g. a test fake: replace every binding.
12
17
  if (options.override) {
13
- this.setRegistration(registration.key, [
18
+ this.#registrations.set(registration.key, [
14
19
  registration
15
20
  ]);
16
21
  return registration;
17
22
  }
18
- const existing = this.getRegistrations(registration.key) ?? [];
23
+ const existing = this.getRegistration(registration.key) ?? [];
19
24
  const duplicate = existing.find((reg)=>reg.isEqual(registration));
20
25
  if (duplicate != null) {
21
- throw new RegistrationError(`A service is already registered for key ${registration.key}. Use builder.override.registerSingleton() (or the matching register* method) to replace it.`);
26
+ throw new RegistrationError(`A service is already registered for key ${describeKey(registration.key)}. Use builder.override.registerSingleton() (or the matching register* method) to replace it.`);
22
27
  }
23
- this.setRegistration(registration.key, [
28
+ this.#registrations.set(registration.key, [
24
29
  ...existing,
25
30
  registration
26
31
  ]);
@@ -28,30 +33,19 @@ export class ServiceRegistry {
28
33
  }
29
34
  replaceType(scope, options) {
30
35
  const registration = this.createRegistrationFrom(scope, options);
31
- this.setRegistration(registration.key, [
36
+ this.#registrations.set(registration.key, [
32
37
  registration
33
38
  ]);
34
39
  return registration;
35
40
  }
36
41
  getRegistration(key) {
37
- const keyVal = new RegistrationKey(key).value;
38
- const registration = this.registrations.get(keyVal);
39
- return registration;
42
+ return this.#registrations.get(key) ?? this.#parent?.getRegistration(key);
40
43
  }
41
44
  createRegistrationFrom(scope, options) {
42
45
  const { typeOrKey, typeOrInstance } = options;
43
46
  const isKey = typeof typeOrKey === "string" || typeof typeOrKey === "symbol";
44
47
  const isInstance = typeof typeOrInstance === "object";
45
- const key = new RegistrationKey(!isKey ? typeOrKey.name : typeOrKey);
46
48
  const type = isInstance ? typeOrInstance : isKey ? typeOrInstance : typeOrKey;
47
- const registration = new ServiceRegistration(key, type, scope, isInstance);
48
- return registration;
49
- }
50
- getRegistrations(key) {
51
- const value = key.value;
52
- return this.registrations.get(value);
53
- }
54
- setRegistration(key, registrations) {
55
- this.registrations.set(key.value, registrations);
49
+ return new ServiceRegistration(typeOrKey, type, scope, isInstance);
56
50
  }
57
51
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decohere/crucible",
3
- "version": "0.0.1-dev.112",
3
+ "version": "0.0.1-dev.113",
4
4
  "description": "Lightweight dependency injection container with decorator-based wiring",
5
5
  "keywords": [
6
6
  "typescript",
@@ -30,8 +30,7 @@
30
30
  "test": "echo \"Error: no test specified\" && exit 1"
31
31
  },
32
32
  "dependencies": {
33
- "@decohere/nexus": "0.0.1-dev.112",
34
- "@decohere/scribe": "0.0.1-dev.112"
33
+ "@decohere/nexus": "0.0.1-dev.113"
35
34
  },
36
35
  "devDependencies": {}
37
36
  }
@@ -1,11 +1,20 @@
1
- import { type Constructable } from "@decohere/nexus/reflection";
1
+ import type { ServiceKey } from "../models/service-key.js";
2
2
  export type InjectionMetadata = Map<number, InjectionToken>;
3
3
  export type InjectionToken = {
4
- key: string | symbol | Constructable;
4
+ /** The key to resolve; without one, the parameter's type is the key. */
5
+ key?: ServiceKey;
6
+ /** Pass `undefined` instead of throwing when nothing is registered for the key. */
7
+ optional?: boolean;
5
8
  };
6
9
  /**
7
- * Bind a constructor parameter to a registration key.
8
- * A class uses `Class.name` (same key as `registerSingleton(Class)`).
9
- * Prefer the class over a name string. Use a symbol or string when the key is not the class name.
10
+ * Bind a constructor parameter to a registration key. Keys match by identity: a class matches only
11
+ * itself (not a string of its name), and a symbol only itself. Prefer the class; use a symbol for an
12
+ * interface or several implementations. `@inject(SomeClass)` also keeps SomeClass's import, which
13
+ * a parameter used only as a type can lose (its run-time type then collapses to `Object`).
10
14
  */
11
- export declare function inject(key: string | symbol | Constructable): (target: object, _propertyKey: string | symbol | undefined, idx: number) => void;
15
+ export declare function inject(key: ServiceKey): (target: object, _propertyKey: string | symbol | undefined, idx: number) => void;
16
+ /**
17
+ * Mark a constructor parameter as optional: when nothing is registered for it, the container
18
+ * passes `undefined` (so a default value applies) instead of throwing.
19
+ */
20
+ export declare function optional(): (target: object, _propertyKey: string | symbol | undefined, idx: number) => void;
@@ -0,0 +1,9 @@
1
+ import type { Constructable, FunctionType } from "@decohere/nexus/reflection";
2
+ /**
3
+ * What a service is registered and resolved under, compared by identity: the class (or factory)
4
+ * itself, a symbol, or a string. A string never matches a class with that name, and two symbols
5
+ * with the same description are different keys.
6
+ */
7
+ export type ServiceKey = string | symbol | Constructable | FunctionType;
8
+ /** A readable name for a key, for error messages. */
9
+ export declare function describeKey(key: ServiceKey): string;
@@ -1,12 +1,12 @@
1
1
  import type { Constructable, FunctionType } from "@decohere/nexus/reflection";
2
2
  import type { Scope } from "../types/types.js";
3
- import type { RegistrationKey } from "./registration-key.model.js";
3
+ import type { ServiceKey } from "./service-key.js";
4
4
  export declare class ServiceRegistration {
5
- key: RegistrationKey;
5
+ key: ServiceKey;
6
6
  scope: Scope;
7
7
  type: Constructable | FunctionType | object;
8
8
  isInstance: boolean;
9
- constructor(key: RegistrationKey, type: Constructable | FunctionType | object, scope: Scope, isInstance: boolean);
9
+ constructor(key: ServiceKey, type: Constructable | FunctionType | object, scope: Scope, isInstance: boolean);
10
10
  /** Same class, or the same instance object. */
11
11
  isEqual(other: ServiceRegistration): boolean;
12
12
  }
@@ -12,18 +12,27 @@ export declare class Crucible implements IContainer {
12
12
  private parent?;
13
13
  private rootScope;
14
14
  private scopes;
15
- private logger;
16
15
  private isDisposed;
17
16
  constructor(options?: CrucibleOptions);
18
17
  resolveMany<T extends object = object>(resolveRequest: ResolveRequest<T>): T[];
18
+ /** Throws {@link ResolveError} when nothing is registered for the key. */
19
19
  resolve<T extends object | object[]>(resolveRequest: ResolveRequest<T>): T;
20
+ private resolveAll;
21
+ private resolveOne;
20
22
  beginScope(name?: string): Crucible;
21
23
  [Symbol.dispose](): void;
22
24
  [Symbol.asyncDispose](): Promise<void>;
23
25
  private disposeSync;
24
26
  private disposeAsync;
25
- private getRegistrationFrom;
26
27
  private resolveRegistration;
28
+ private assertNoCycle;
29
+ /**
30
+ * A singleton lives for the whole process, so a request-scoped service it depends on (directly
31
+ * or through transients) would be created once and then shared by every request, e.g. one
32
+ * QueryRunner for the life of the process. The container's own lifetime scope is exempt: a
33
+ * singleton asking for it gets the root, which is the scope it lives in.
34
+ */
35
+ private assertNotCaptive;
27
36
  private getOrCreateInstance;
28
37
  private createInstance;
29
38
  private resolveDependencies;
@@ -1,18 +1,22 @@
1
1
  import type { Constructable, FunctionType } from "@decohere/nexus/reflection";
2
+ import { type ServiceKey } from "../models/service-key.js";
2
3
  import { ServiceRegistration } from "../models/service-registration.model.js";
3
4
  import type { Scope } from "../types/types.js";
4
5
  export type RegisterTypeOptions = {
5
- typeOrKey: string | symbol | Constructable | FunctionType;
6
+ typeOrKey: ServiceKey;
6
7
  typeOrInstance?: Constructable | FunctionType | object;
7
8
  override?: boolean;
8
9
  };
10
+ /**
11
+ * Registrations by key. A scope's registry holds only what the scope adds (its own lifetime
12
+ * scope) and looks everything else up in its parent, so beginning a scope costs the same however
13
+ * many services are registered.
14
+ */
9
15
  export declare class ServiceRegistry {
10
- private readonly registrations;
16
+ #private;
11
17
  constructor(parent?: ServiceRegistry);
12
18
  registerType(scope: Scope, options: RegisterTypeOptions): ServiceRegistration;
13
19
  replaceType(scope: Scope, options: RegisterTypeOptions): ServiceRegistration;
14
- getRegistration(key: string | symbol | Constructable): ServiceRegistration[];
20
+ getRegistration(key: ServiceKey): ServiceRegistration[] | undefined;
15
21
  private createRegistrationFrom;
16
- private getRegistrations;
17
- private setRegistration;
18
22
  }
@@ -1,29 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", {
3
- value: true
4
- });
5
- Object.defineProperty(exports, "RegistrationKey", {
6
- enumerable: true,
7
- get: function() {
8
- return RegistrationKey;
9
- }
10
- });
11
- class RegistrationKey {
12
- constructor(key){
13
- this.value = this.getValue(key);
14
- }
15
- value;
16
- getValue(key) {
17
- switch(typeof key){
18
- case "string":
19
- return key;
20
- case "symbol":
21
- return key.valueOf().description;
22
- default:
23
- return key.name;
24
- }
25
- }
26
- toString() {
27
- return this.value;
28
- }
29
- }
@@ -1,19 +0,0 @@
1
- export class RegistrationKey {
2
- constructor(key){
3
- this.value = this.getValue(key);
4
- }
5
- value;
6
- getValue(key) {
7
- switch(typeof key){
8
- case "string":
9
- return key;
10
- case "symbol":
11
- return key.valueOf().description;
12
- default:
13
- return key.name;
14
- }
15
- }
16
- toString() {
17
- return this.value;
18
- }
19
- }
@@ -1,7 +0,0 @@
1
- import type { Constructable } from "@decohere/nexus/reflection";
2
- export declare class RegistrationKey {
3
- constructor(key: string | symbol | Constructable);
4
- value: string;
5
- private getValue;
6
- toString(): string;
7
- }