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

Commit da5f212

Browse files
authored
Merge pull request #102 from denis-ilchishin/optional_deps
feat(application): add optional dependencies
2 parents 319016d + 728efb7 commit da5f212

4 files changed

Lines changed: 65 additions & 94 deletions

File tree

packages/application/lib/api.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ describe.sequential('Api', () => {
179179

180180
const call = (
181181
options: Pick<ProcedureCallOptions, 'procedure'> &
182-
Partial<ProcedureCallOptions>,
182+
Partial<Omit<ProcedureCallOptions, 'procedure'>>,
183183
) =>
184184
api.call({
185185
container,

packages/application/lib/container.spec.ts

Lines changed: 5 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,6 @@ describe.sequential('Provider', () => {
4242
expect(newProvider).not.toBe(provider)
4343
})
4444

45-
it('should clone with a options', () => {
46-
let newProvider = provider.withOptions({ some: 'option' })
47-
expect(newProvider.options).to.deep.eq({ some: 'option' })
48-
expect(newProvider).not.toBe(provider)
49-
newProvider = provider.withOptions({ some: 'option', other: 'option' })
50-
expect(newProvider.options).to.deep.eq({ some: 'option', other: 'option' })
51-
expect(newProvider.scope).not.toBe(Scope.Transient)
52-
})
53-
5445
it('should clone with a dependencies', () => {
5546
const dep1 = new Provider().withValue('dep1')
5647
const dep2 = new Provider().withValue('dep2')
@@ -70,11 +61,6 @@ describe.sequential('Provider', () => {
7061
expect(newProvider.description).toBe('description')
7162
expect(newProvider).not.toBe(provider)
7263
})
73-
74-
it('should clone with options type', () => {
75-
const newProvider = provider.withOptionsType<number>().withOptions(1)
76-
expect(newProvider).not.toBe(provider)
77-
})
7864
})
7965

8066
describe.sequential('Container', () => {
@@ -118,16 +104,16 @@ describe.sequential('Container', () => {
118104
const dep1 = new Provider().withValue('dep1')
119105
const dep2 = new Provider()
120106
.withDependencies({ dep1 })
121-
.withFactory(({ dep1 }) => ({ dep1 }))
107+
.withFactory((deps) => deps)
122108
const dep3 = new Provider().withFactory(() => 'dep3')
123109
const provider = new Provider()
124-
.withDependencies({ dep2, dep3 })
125-
.withFactory(({ ...deps }) => deps)
110+
.withDependencies({ dep2, dep3, dep4: dep3.optional() })
111+
.withFactory((deps) => deps)
126112
const deps = await container.resolve(provider)
127113
expect(deps).toHaveProperty('dep2')
128114
expect(deps).toHaveProperty('dep3')
129-
expect(deps).toHaveProperty('dep2.dep1')
130-
expect(deps.dep2.dep1).toBe('dep1')
115+
expect(deps).toHaveProperty('dep4', deps.dep3)
116+
expect(deps).toHaveProperty('dep2.dep1', 'dep1')
131117
})
132118

133119
it('should dispose', async () => {
@@ -140,12 +126,6 @@ describe.sequential('Container', () => {
140126
expect(spy).toHaveBeenCalledOnce()
141127
})
142128

143-
it('should provide with options', async () => {
144-
const options = {}
145-
const provider = new Provider().withFactory((ctx, options) => options)
146-
expect(await container.resolve(provider.withOptions(options))).toBe(options)
147-
})
148-
149129
it('should be cached', async () => {
150130
const provider = new Provider().withFactory(() => ({}))
151131
const val = await container.resolve(provider)

packages/application/lib/container.ts

Lines changed: 57 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ export function getProviderScope(provider: AnyProvider) {
2929
return scope
3030
}
3131

32-
export type Dependencies = Record<string, AnyProvider>
32+
export type Dependencies = Record<
33+
string,
34+
AnyProvider | { isOptional: true; provider: AnyProvider }
35+
>
3336

3437
export type ResolveProviderType<T extends Provider> = Awaited<T['value']>
3538

@@ -38,31 +41,38 @@ export interface Depender<Deps extends Dependencies = {}> {
3841
}
3942

4043
export type DependencyContext<Deps extends Dependencies> = {
41-
[K in keyof Deps]: ResolveProviderType<Deps[K]>
44+
[K in keyof Deps as Deps[K] extends AnyProvider
45+
? K
46+
: never]: Deps[K] extends AnyProvider ? ResolveProviderType<Deps[K]> : never
47+
} & {
48+
[K in keyof Deps as Deps[K] extends {
49+
isOptional: true
50+
provider: AnyProvider
51+
}
52+
? K
53+
: never]?: Deps[K] extends {
54+
isOptional: true
55+
provider: AnyProvider
56+
}
57+
? ResolveProviderType<Deps[K]['provider']>
58+
: never
4259
}
4360

4461
export type ProviderFactoryType<
4562
ProviderType,
46-
ProviderOptions,
4763
ProviderDeps extends Dependencies,
48-
> = (
49-
injections: DependencyContext<ProviderDeps>,
50-
options: ProviderOptions,
51-
) => ProviderType
64+
> = (injections: DependencyContext<ProviderDeps>) => ProviderType
5265

5366
export type ProviderDisposeType<
5467
ProviderType,
55-
ProviderOptions,
5668
ProviderDeps extends Dependencies,
5769
> = (
5870
instance: Awaited<ProviderType>,
5971
ctx: DependencyContext<ProviderDeps>,
60-
options: ProviderOptions,
6172
) => any
6273

6374
export class Provider<
6475
ProviderValue = any,
65-
ProviderOptions = unknown,
6676
ProviderDeps extends Dependencies = {},
6777
> implements Depender<ProviderDeps>
6878
{
@@ -79,56 +89,34 @@ export class Provider<
7989
readonly value!: ProviderValue
8090
readonly dependencies: ProviderDeps = {} as ProviderDeps
8191
readonly scope: Scope = Scope.Global
82-
readonly factory!: ProviderFactoryType<
83-
ProviderValue,
84-
ProviderOptions,
85-
ProviderDeps
86-
>
87-
readonly dispose?: ProviderDisposeType<
88-
ProviderValue,
89-
ProviderOptions,
90-
ProviderDeps
91-
>
92-
readonly options!: ProviderOptions
92+
readonly factory!: ProviderFactoryType<ProviderValue, ProviderDeps>
93+
readonly dispose?: ProviderDisposeType<ProviderValue, ProviderDeps>
9394
readonly description!: string
9495

9596
withDependencies<Deps extends Dependencies>(dependencies: Deps) {
96-
const provider = new Provider<
97-
ProviderValue,
98-
ProviderOptions,
99-
Merge<ProviderDeps, Deps>
100-
>()
97+
const provider = new Provider<ProviderValue, Merge<ProviderDeps, Deps>>()
10198
return Provider.override(provider, this, {
10299
dependencies: merge(this.dependencies, dependencies),
103100
})
104101
}
105102

106103
withScope<S extends Scope>(scope: S) {
107-
const provider = new Provider<
108-
ProviderValue,
109-
ProviderOptions,
110-
ProviderDeps
111-
>()
104+
const provider = new Provider<ProviderValue, ProviderDeps>()
112105
return Provider.override(provider, this, { scope })
113106
}
114107

115-
withOptionsType<Options>() {
116-
const provider = new Provider<ProviderValue, Options, ProviderDeps>()
117-
return Provider.override(provider, this)
118-
}
119-
120108
withFactory<
121-
F extends ProviderFactoryType<ProviderValue, ProviderOptions, ProviderDeps>,
109+
F extends ProviderFactoryType<ProviderValue, ProviderDeps>,
122110
T extends Awaited<ReturnType<F>>,
123111
>(factory: F) {
124-
const provider = new Provider<T, ProviderOptions, ProviderDeps>()
112+
const provider = new Provider<T, ProviderDeps>()
125113
return Provider.override(provider, this, { factory, value: undefined })
126114
}
127115

128116
withValue<T extends ProviderValue extends never ? any : ProviderValue>(
129117
value: T,
130118
) {
131-
const provider = new Provider<T, ProviderOptions, ProviderDeps>()
119+
const provider = new Provider<T, ProviderDeps>()
132120
return Provider.override(provider, this, {
133121
value,
134122
factory: undefined,
@@ -137,31 +125,31 @@ export class Provider<
137125
}
138126

139127
withDisposal(dispose: this['dispose']) {
140-
const provider = new Provider<
141-
ProviderValue,
142-
ProviderOptions,
143-
ProviderDeps
144-
>()
128+
const provider = new Provider<ProviderValue, ProviderDeps>()
145129
return Provider.override(provider, this, { dispose })
146130
}
147131

148-
withOptions(options: ProviderOptions) {
149-
const provider = new Provider<
150-
ProviderValue,
151-
ProviderOptions,
152-
ProviderDeps
153-
>()
154-
return Provider.override(provider, this, { options })
155-
}
156-
157132
withDescription(description: string) {
158-
const provider = new Provider<
159-
ProviderValue,
160-
ProviderOptions,
161-
ProviderDeps
162-
>()
133+
const provider = new Provider<ProviderValue, ProviderDeps>()
163134
return Provider.override(provider, this, { description })
164135
}
136+
137+
optional() {
138+
return {
139+
isOptional: true as const,
140+
provider: this as Provider<ProviderValue, ProviderDeps>,
141+
}
142+
}
143+
144+
async resolve(
145+
...args: keyof ProviderDeps extends never
146+
? []
147+
: [DependencyContext<ProviderDeps>]
148+
) {
149+
if (this.value) return this.value
150+
const [ctx = {}] = args
151+
return await this.factory(ctx as any)
152+
}
165153
}
166154

167155
export class Container {
@@ -182,8 +170,10 @@ export class Container {
182170
const traverse = (dependencies: Dependencies) => {
183171
for (const key in dependencies) {
184172
const depender = dependencies[key]
185-
this.providers.add(depender)
186-
traverse(depender.dependencies)
173+
const provider =
174+
depender instanceof Provider ? depender : depender.provider
175+
this.providers.add(provider)
176+
traverse(provider.dependencies)
187177
}
188178
}
189179

@@ -204,11 +194,11 @@ export class Container {
204194
// to prevent first disposal of a provider
205195
// that other disposing provider depends on
206196
this.application.logger.trace('Disposing [%s] scope context...', this.scope)
207-
for (const [{ dispose, options, dependencies }, value] of this.instances) {
197+
for (const [{ dispose, dependencies }, value] of this.instances) {
208198
if (dispose) {
209199
try {
210200
const ctx = await this.createContext(dependencies)
211-
await dispose(value, ctx, options)
201+
await dispose(value, ctx)
212202
} catch (cause) {
213203
this.application.logger.error(
214204
new Error('Context disposal error. Potential memory leak', {
@@ -237,13 +227,12 @@ export class Container {
237227
} else if (this.resolvers.has(provider)) {
238228
return this.resolvers.get(provider)!
239229
} else {
240-
const { value, factory, scope, dependencies, options } = provider
230+
const { value, factory, scope, dependencies } = provider
241231
if (typeof value !== 'undefined') return Promise.resolve(value)
242232
if (this.parent?.isResolved(provider))
243233
return this.parent.resolve(provider)
244-
// if (typeof factory !== 'function') console.log(provider)
245234
const resolution = this.createContext(dependencies)
246-
.then((ctx) => factory(ctx, options))
235+
.then((ctx) => factory(ctx))
247236
.then((instance) => {
248237
if (ScopeStrictness[this.scope] >= ScopeStrictness[scope])
249238
this.instances.set(provider, instance)
@@ -271,7 +260,9 @@ export class Container {
271260
const injections: any = {}
272261
const resolvers: Promise<any>[] = []
273262
for (const [key, dependency] of Object.entries(dependencies)) {
274-
const resolver = this.resolve(dependency)
263+
const provider =
264+
dependency instanceof Provider ? dependency : dependency.provider
265+
const resolver = this.resolve(provider)
275266
resolvers.push(resolver.then((value) => (injections[key] = value)))
276267
}
277268
await Promise.all(resolvers)

packages/application/lib/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,8 @@ export type ConnectionProvider<T, C> = Provider<ConnectionFn<T, C>>
8080

8181
export type AnyApplication = Application<any, any, any, any>
8282

83-
export type AnyProvider = Provider<any, any, any>
84-
export type AnyProcedure = Procedure<any, any, any, any>
83+
export type AnyProvider = Provider<any, any>
84+
export type AnyProcedure = Procedure<any, any, any, any, any>
8585
export type AnyTask = Task<any, any, any>
8686
export type AnyEvent = Event<any, any, any>
8787

0 commit comments

Comments
 (0)