Skip to content
This repository was archived by the owner on Apr 21, 2024. It is now read-only.

Commit 8970650

Browse files
committed
feat: registerAsyncConstructor
1 parent 0eddbde commit 8970650

File tree

10 files changed

+311
-63
lines changed

10 files changed

+311
-63
lines changed

README.md

+24-26
Original file line numberDiff line numberDiff line change
@@ -33,28 +33,6 @@ import { ... } from 'https://denopkg.com/Coder-Spirit/lambda-ioc@[VERSION]/lambd
3333
import { ... } from 'https://deno.land/x/lambda_ioc@[VERSION]/lambda-ioc/deno/index.ts'
3434
```
3535

36-
## Benefits
37-
38-
- 100% type safe:
39-
- The type checker will complain if we try to resolve unregistered
40-
dependencies.
41-
- The type checker will complain if we try to register new dependencies that
42-
depend on unregistered dependencies, or if there is any kind of type
43-
mismatch.
44-
- Purely functional
45-
- Immutable
46-
- Circular dependencies are impossible
47-
48-
## Drawbacks
49-
50-
- All dependencies must be declared "in order".
51-
- This implies that this IoC container cannot be used in combination with some
52-
auto-wiring solutions, such as IoC decorators.
53-
- The involved types are a bit convoluted:
54-
- They might cause the type checker to be slow.
55-
- In some situations, the type checker might be unable to infer the involved
56-
types due to excessive "nested types" depth.
57-
5836
## Example
5937

6038
```ts
@@ -105,13 +83,33 @@ container.resolveGroup('group2') // ~ [3, 4], not necessarily in the same order
10583
```
10684

10785
It is also possible to register and resolve asynchronous factories and
108-
dependencies. They are not documented yet because some "helpers" are missing,
109-
and therefore it's a bit more annoying to take advantage of that feature.
110-
111-
If you are curious, just try out:
86+
dependencies. If you are curious, just try out:
11287
- `registerAsync`
88+
- `registerAsyncConstructor`
11389
- `resolveAsync`
11490

91+
## Benefits
92+
93+
- 100% type safe:
94+
- The type checker will complain if we try to resolve unregistered
95+
dependencies.
96+
- The type checker will complain if we try to register new dependencies that
97+
depend on unregistered dependencies, or if there is any kind of type
98+
mismatch.
99+
- Purely functional
100+
- Immutable
101+
- Circular dependencies are impossible
102+
103+
## Drawbacks
104+
105+
- All dependencies must be declared "in order".
106+
- This implies that this IoC container cannot be used in combination with some
107+
auto-wiring solutions, such as IoC decorators.
108+
- The involved types are a bit convoluted:
109+
- They might cause the type checker to be slow.
110+
- In some situations, the type checker might be unable to infer the involved
111+
types due to excessive "nested types" depth.
112+
115113
## Differences respect to Diddly
116114

117115
- First-class support for Deno.

lambda-ioc/README.md

+24-26
Original file line numberDiff line numberDiff line change
@@ -33,28 +33,6 @@ import { ... } from 'https://denopkg.com/Coder-Spirit/lambda-ioc@[VERSION]/lambd
3333
import { ... } from 'https://deno.land/x/lambda_ioc@[VERSION]/lambda-ioc/deno/index.ts'
3434
```
3535

36-
## Benefits
37-
38-
- 100% type safe:
39-
- The type checker will complain if we try to resolve unregistered
40-
dependencies.
41-
- The type checker will complain if we try to register new dependencies that
42-
depend on unregistered dependencies, or if there is any kind of type
43-
mismatch.
44-
- Purely functional
45-
- Immutable
46-
- Circular dependencies are impossible
47-
48-
## Drawbacks
49-
50-
- All dependencies must be declared "in order".
51-
- This implies that this IoC container cannot be used in combination with some
52-
auto-wiring solutions, such as IoC decorators.
53-
- The involved types are a bit convoluted:
54-
- They might cause the type checker to be slow.
55-
- In some situations, the type checker might be unable to infer the involved
56-
types due to excessive "nested types" depth.
57-
5836
## Example
5937

6038
```ts
@@ -105,13 +83,33 @@ container.resolveGroup('group2') // ~ [3, 4], not necessarily in the same order
10583
```
10684

10785
It is also possible to register and resolve asynchronous factories and
108-
dependencies. They are not documented yet because some "helpers" are missing,
109-
and therefore it's a bit more annoying to take advantage of that feature.
110-
111-
If you are curious, just try out:
86+
dependencies. If you are curious, just try out:
11287
- `registerAsync`
88+
- `registerAsyncConstructor`
11389
- `resolveAsync`
11490

91+
## Benefits
92+
93+
- 100% type safe:
94+
- The type checker will complain if we try to resolve unregistered
95+
dependencies.
96+
- The type checker will complain if we try to register new dependencies that
97+
depend on unregistered dependencies, or if there is any kind of type
98+
mismatch.
99+
- Purely functional
100+
- Immutable
101+
- Circular dependencies are impossible
102+
103+
## Drawbacks
104+
105+
- All dependencies must be declared "in order".
106+
- This implies that this IoC container cannot be used in combination with some
107+
auto-wiring solutions, such as IoC decorators.
108+
- The involved types are a bit convoluted:
109+
- They might cause the type checker to be slow.
110+
- In some situations, the type checker might be unable to infer the involved
111+
types due to excessive "nested types" depth.
112+
115113
## Differences respect to Diddly
116114

117115
- First-class support for Deno.

lambda-ioc/deno/combinators.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import {
22
AsyncDependencyFactory,
3-
ContainerKey,
43
ReadableContainer,
54
ReadableSyncContainer,
65
SyncDependencyFactory,
76
} from './container.ts';
8-
import { ParamsToResolverKeys, TupleO, Zip } from './util.ts';
7+
import { ContainerKey, ParamsToResolverKeys, TupleO, Zip } from './util.ts';
98

109
/**
1110
* Given a dependency factory, returns a new factory that will always resolve

lambda-ioc/deno/container.ts

+94-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint-disable @typescript-eslint/ban-types */
22

3-
export type ContainerKey = string | symbol
3+
import { ContainerKey, ContextualParamsToResolverKeys } from './util.ts';
44

55
type ExtractPrefix<S extends ContainerKey> =
66
S extends `${infer Prefix}:${string}` ? Prefix : never
@@ -176,6 +176,51 @@ export interface WritableContainer<
176176
}
177177
>
178178

179+
/**
180+
* Registers a new constructor that might have asynchronous-resolvable
181+
* dependencies. This method is helpful when the constructor combinator is
182+
* not powerful enough (as it's only able to resolve synchronously).
183+
*
184+
* @param name The "name" of the dependency (can be a symbol).
185+
* @param constructor A class constructor, that will be use to resolve the
186+
* registered dependency.
187+
* @param args A list of dependency names that will be passed to the
188+
* registered constructor at construction time, when we try to
189+
* resolve the dependency.
190+
*/
191+
registerAsyncConstructor<
192+
TName extends ContainerKey,
193+
TParams extends readonly (
194+
| TSyncDependencies[keyof TSyncDependencies]
195+
| TAsyncDependencies[keyof TAsyncDependencies]
196+
)[],
197+
TClass extends TName extends keyof TSyncDependencies
198+
? never
199+
: TName extends keyof TAsyncDependencies
200+
? TAsyncDependencies[TName]
201+
: unknown,
202+
TDependencies extends ContextualParamsToResolverKeys<
203+
TSyncDependencies,
204+
TAsyncDependencies,
205+
TParams
206+
>,
207+
>(
208+
name: TName,
209+
constructor: new (...args: TParams) => TClass,
210+
...args: TDependencies
211+
): Container<
212+
TSyncDependencies,
213+
{
214+
[TK in
215+
| keyof TAsyncDependencies
216+
| TName]: TK extends keyof TAsyncDependencies
217+
? TName extends TK
218+
? TClass
219+
: TAsyncDependencies[TK]
220+
: TClass
221+
}
222+
>
223+
179224
/**
180225
* Register an already instantiated dependency.
181226
*
@@ -367,6 +412,54 @@ function __createContainer<
367412
}
368413
},
369414

415+
registerAsyncConstructor<
416+
TName extends ContainerKey,
417+
TParams extends readonly (
418+
| TSyncDependencies[keyof TSyncDependencies]
419+
| TAsyncDependencies[keyof TAsyncDependencies]
420+
)[],
421+
TClass extends TName extends keyof TSyncDependencies
422+
? never
423+
: TName extends keyof TAsyncDependencies
424+
? TAsyncDependencies[TName]
425+
: unknown,
426+
TDependencies extends ContextualParamsToResolverKeys<
427+
TSyncDependencies,
428+
TAsyncDependencies,
429+
TParams
430+
>,
431+
>(
432+
name: TName,
433+
constructor: new (...args: TParams) => TClass,
434+
...args: TDependencies
435+
): ContainerWithNewAsyncDep<TName, TClass> {
436+
const factory = async (container: typeof this) => {
437+
const argPromises = args.map((arg) => {
438+
return (arg as string) in syncDependencies
439+
? container.resolve(arg as keyof TSyncDependencies)
440+
: container.resolveAsync(arg as keyof TAsyncDependencies)
441+
})
442+
const resolvedParams = (await Promise.all(
443+
argPromises,
444+
)) as unknown as TParams
445+
446+
return new constructor(...resolvedParams)
447+
}
448+
449+
if (name in asyncDependencies) {
450+
return __createContainer(syncDependencies, {
451+
...asyncDependencies,
452+
[name]: factory,
453+
}) as ContainerWithNewAsyncDep<TName, TClass>
454+
} else {
455+
;(asyncDependencies as Record<TName, unknown>)[name] = factory
456+
return __createContainer(
457+
syncDependencies,
458+
asyncDependencies,
459+
) as ContainerWithNewAsyncDep<TName, TClass>
460+
}
461+
},
462+
370463
registerValue<TName extends ContainerKey, TDependency>(
371464
name: TName,
372465
dependency: TDependency,

lambda-ioc/deno/util.ts

+23-2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,34 @@
11
/* eslint-disable @typescript-eslint/ban-types */
22
/* eslint-disable @typescript-eslint/no-explicit-any */
33

4-
import { ContainerKey } from './container.ts';
4+
export type ContainerKey = string | symbol
55

66
export type Zip<A extends readonly unknown[], B extends readonly unknown[]> = {
77
[K in keyof A]: K extends keyof B ? [A[K], B[K]] : never
88
}
99

10-
export type ParamsToResolverKeys<T extends readonly unknown[] | []> = { [K in keyof T]: ContainerKey }
10+
export type ParamsToResolverKeys<T extends readonly unknown[] | []> = {
11+
[K in keyof T]: ContainerKey
12+
}
13+
14+
export type ContextualParamsToResolverKeys<
15+
TSyncDependencies extends Record<ContainerKey, unknown>,
16+
TAsyncDependencies extends Record<ContainerKey, unknown>,
17+
TParams extends
18+
| readonly (
19+
| TSyncDependencies[keyof TSyncDependencies]
20+
| TAsyncDependencies[keyof TAsyncDependencies]
21+
)[]
22+
| [],
23+
> = {
24+
[K in keyof TParams]:
25+
| KeysMatching<TSyncDependencies, TParams[K]>
26+
| KeysMatching<TAsyncDependencies, TParams[K]>
27+
}
28+
29+
type KeysMatching<Collection, Value> = {
30+
[K in keyof Collection]-?: Collection[K] extends Value ? K : never
31+
}[keyof Collection]
1132

1233
export type MergeNoDuplicates<A extends {}, B extends {}> = {
1334
[K in keyof A | keyof B]: K extends keyof B

lambda-ioc/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@coderspirit/lambda-ioc",
3-
"version": "0.4.1",
3+
"version": "0.5.0",
44
"main": "./dist/cjs/index.js",
55
"module": "./dist/esm/index.js",
66
"types": "./dist/cjs/index.d.ts",

lambda-ioc/src/__tests__/container.test.ts

+26
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,32 @@ describe('container', () => {
119119
expect(g2).toContain(50)
120120
expect(g2).toContain(60)
121121
})
122+
123+
it('can register constructors with a mix of sync & async dependencies', async () => {
124+
class C {
125+
constructor(public readonly a: number, public readonly b: string) {}
126+
}
127+
128+
const container = createContainer()
129+
.registerValue('numeric', 10)
130+
.registerAsync('text', () => Promise.resolve('hello'))
131+
.registerAsyncConstructor('C', C, 'numeric', 'text')
132+
133+
const c1 = await container.resolveAsync('C')
134+
expect(c1.a).toBe(10)
135+
expect(c1.b).toBe('hello')
136+
137+
// Re-registering a new dependency with the same name should work, as long
138+
// as we preserve its type.
139+
const secondContainer = container
140+
.registerAsync('float', () => Promise.resolve(34.5))
141+
.registerValue('name', 'Bob')
142+
.registerAsyncConstructor('C', C, 'float', 'name')
143+
144+
const c2 = await secondContainer.resolveAsync('C')
145+
expect(c2.a).toBe(34.5)
146+
expect(c2.b).toBe('Bob')
147+
})
122148
})
123149

124150
// Type tests

lambda-ioc/src/combinators.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import {
22
AsyncDependencyFactory,
3-
ContainerKey,
43
ReadableContainer,
54
ReadableSyncContainer,
65
SyncDependencyFactory,
76
} from './container'
8-
import { ParamsToResolverKeys, TupleO, Zip } from './util'
7+
import { ContainerKey, ParamsToResolverKeys, TupleO, Zip } from './util'
98

109
/**
1110
* Given a dependency factory, returns a new factory that will always resolve

0 commit comments

Comments
 (0)