@decohere/crucible 0.0.1-dev.112 → 0.0.1-dev.114
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 +25 -22
- package/commonjs/src/decorators/inject.decorator.js +26 -11
- package/commonjs/src/models/service-key.js +20 -0
- package/commonjs/src/services/crucible-builder.js +3 -3
- package/commonjs/src/services/crucible.js +66 -46
- package/commonjs/src/services/service-registry.js +11 -21
- package/es6/src/decorators/inject.decorator.js +22 -11
- package/es6/src/models/service-key.js +10 -0
- package/es6/src/services/crucible-builder.js +3 -3
- package/es6/src/services/crucible.js +66 -46
- package/es6/src/services/service-registry.js +16 -22
- package/package.json +2 -3
- package/types/src/decorators/inject.decorator.d.ts +15 -6
- package/types/src/models/service-key.d.ts +9 -0
- package/types/src/models/service-registration.model.d.ts +3 -3
- package/types/src/services/crucible.d.ts +11 -2
- package/types/src/services/service-registry.d.ts +9 -5
- package/commonjs/src/models/registration-key.model.js +0 -29
- package/es6/src/models/registration-key.model.js +0 -19
- package/types/src/models/registration-key.model.d.ts +0 -7
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 |
|
|
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
|
|
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
|
|
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 |
|
|
102
|
-
| Array of implementations (`Transformer[]`) | Paramtypes is `Array
|
|
103
|
-
| Paramtypes collapsed to `Object` (
|
|
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`.
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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` → `
|
|
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. **
|
|
332
|
-
6. **
|
|
333
|
-
7. **
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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 (
|
|
15
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
return
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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 (
|
|
51
|
-
throw new _errors.ResolveError(`There are multiple services registered with key ${
|
|
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
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
|
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
|
|
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.
|
|
24
|
+
this.#registrations.set(registration.key, [
|
|
24
25
|
registration
|
|
25
26
|
]);
|
|
26
27
|
return registration;
|
|
27
28
|
}
|
|
28
|
-
const existing = this.
|
|
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.
|
|
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.
|
|
42
|
+
this.#registrations.set(registration.key, [
|
|
42
43
|
registration
|
|
43
44
|
]);
|
|
44
45
|
return registration;
|
|
45
46
|
}
|
|
46
47
|
getRegistration(key) {
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
6
|
-
*
|
|
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 (
|
|
9
|
-
|
|
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
|
-
}
|
|
@@ -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
|
|
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
|
|
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
|
|
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 {
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
return
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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 (
|
|
41
|
-
throw new ResolveError(`There are multiple services registered with key ${key
|
|
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
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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 {
|
|
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
|
-
|
|
5
|
-
|
|
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
|
|
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.
|
|
18
|
+
this.#registrations.set(registration.key, [
|
|
14
19
|
registration
|
|
15
20
|
]);
|
|
16
21
|
return registration;
|
|
17
22
|
}
|
|
18
|
-
const existing = this.
|
|
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.
|
|
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.
|
|
36
|
+
this.#registrations.set(registration.key, [
|
|
32
37
|
registration
|
|
33
38
|
]);
|
|
34
39
|
return registration;
|
|
35
40
|
}
|
|
36
41
|
getRegistration(key) {
|
|
37
|
-
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "0.0.1-dev.114",
|
|
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.
|
|
34
|
-
"@decohere/scribe": "0.0.1-dev.112"
|
|
33
|
+
"@decohere/nexus": "0.0.1-dev.114"
|
|
35
34
|
},
|
|
36
35
|
"devDependencies": {}
|
|
37
36
|
}
|
|
@@ -1,11 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ServiceKey } from "../models/service-key.js";
|
|
2
2
|
export type InjectionMetadata = Map<number, InjectionToken>;
|
|
3
3
|
export type InjectionToken = {
|
|
4
|
-
key
|
|
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
|
-
*
|
|
9
|
-
*
|
|
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:
|
|
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 {
|
|
3
|
+
import type { ServiceKey } from "./service-key.js";
|
|
4
4
|
export declare class ServiceRegistration {
|
|
5
|
-
key:
|
|
5
|
+
key: ServiceKey;
|
|
6
6
|
scope: Scope;
|
|
7
7
|
type: Constructable | FunctionType | object;
|
|
8
8
|
isInstance: boolean;
|
|
9
|
-
constructor(key:
|
|
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:
|
|
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
|
|
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:
|
|
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
|
-
}
|