@nmtjs/core 0.7.3 → 0.7.5

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.
@@ -1,7 +1,440 @@
1
- import { createLazyInjectable } from './container.ts'
1
+ import {
2
+ type Async,
3
+ type ClassConstructor,
4
+ type ClassConstructorArgs,
5
+ type ClassInstance,
6
+ tryCaptureStackTrace,
7
+ } from '@nmtjs/common'
8
+ import {
9
+ kClassInjectable,
10
+ kFactoryInjectable,
11
+ kInjectable,
12
+ kLazyInjectable,
13
+ kOptionalDependency,
14
+ kValueInjectable,
15
+ } from './constants.ts'
16
+ import type { InjectFn } from './container.ts'
2
17
  import { Scope } from './enums.ts'
3
18
  import type { Logger } from './logger.ts'
4
19
 
5
- const logger = createLazyInjectable<Logger>(Scope.Global, 'Logger')
20
+ const ScopeStrictness = {
21
+ [Scope.Global]: 0,
22
+ [Scope.Connection]: 1,
23
+ [Scope.Call]: 2,
24
+ [Scope.Transient]: 3,
25
+ }
6
26
 
7
- export const CoreInjectables = { logger }
27
+ export type DependencyOptional<T extends AnyInjectable = AnyInjectable> = {
28
+ [kOptionalDependency]: any
29
+ injectable: T
30
+ }
31
+
32
+ export type Depedency = DependencyOptional | AnyInjectable
33
+
34
+ export type Dependencies = Record<string, Depedency>
35
+
36
+ export type ResolveInjectableType<T extends AnyInjectable> =
37
+ T extends Injectable<infer Type, any, any> ? Type : never
38
+
39
+ export interface Dependant<Deps extends Dependencies = Dependencies> {
40
+ dependencies: Deps
41
+ label?: string
42
+ stack?: string
43
+ }
44
+
45
+ export type DependencyInjectable<T extends Depedency> = T extends AnyInjectable
46
+ ? T
47
+ : T extends DependencyOptional
48
+ ? T['injectable']
49
+ : never
50
+
51
+ export type DependencyContext<Deps extends Dependencies> = {
52
+ readonly [K in keyof Deps as Deps[K] extends AnyInjectable
53
+ ? K
54
+ : never]: Deps[K] extends AnyInjectable
55
+ ? ResolveInjectableType<Deps[K]>
56
+ : never
57
+ } & {
58
+ readonly [K in keyof Deps as Deps[K] extends DependencyOptional
59
+ ? K
60
+ : never]?: Deps[K] extends DependencyOptional
61
+ ? ResolveInjectableType<Deps[K]['injectable']>
62
+ : never
63
+ }
64
+
65
+ export type InjectableFactoryType<
66
+ InjectableType,
67
+ InjectableDeps extends Dependencies,
68
+ > = (context: DependencyContext<InjectableDeps>) => Async<InjectableType>
69
+
70
+ export type InjectablePickType<Input, Output> = (injectable: Input) => Output
71
+
72
+ export type InjectableDisposeType<
73
+ InjectableType,
74
+ InjectableDeps extends Dependencies,
75
+ > = (
76
+ instance: InjectableType,
77
+ context: DependencyContext<InjectableDeps>,
78
+ ) => any
79
+
80
+ export interface LazyInjectable<T, S extends Scope = Scope.Global>
81
+ extends Dependant<{}> {
82
+ scope: S
83
+ [kInjectable]: any
84
+ [kLazyInjectable]: T
85
+ }
86
+
87
+ export interface ValueInjectable<T> extends Dependant<{}> {
88
+ scope: Scope.Global
89
+ value: T
90
+ [kInjectable]: any
91
+ [kValueInjectable]: any
92
+ }
93
+
94
+ export interface FactoryInjectable<
95
+ T,
96
+ D extends Dependencies = {},
97
+ S extends Scope = Scope.Global,
98
+ P = T,
99
+ > extends Dependant<D> {
100
+ scope: S
101
+ factory: InjectableFactoryType<P, D>
102
+ pick: InjectablePickType<P, T>
103
+ dispose?: InjectableDisposeType<P, D>
104
+ [kInjectable]: any
105
+ [kFactoryInjectable]: any
106
+ }
107
+
108
+ export interface BaseClassInjectable<
109
+ T,
110
+ D extends Dependencies = {},
111
+ S extends Scope = Scope.Global,
112
+ > extends Dependant<D> {
113
+ new (...args: any[]): T
114
+ scope: S
115
+ [kInjectable]: any
116
+ [kClassInjectable]: any
117
+ }
118
+
119
+ export interface ClassInjectable<
120
+ T,
121
+ D extends Dependencies = {},
122
+ S extends Scope = Scope.Global,
123
+ A extends any[] = [],
124
+ > extends Dependant<D> {
125
+ new (
126
+ $context: DependencyContext<D>,
127
+ ...args: A
128
+ ): T & {
129
+ $context: DependencyContext<D>
130
+ }
131
+ scope: S
132
+ [kInjectable]: any
133
+ [kClassInjectable]: any
134
+ }
135
+
136
+ export type Injectable<
137
+ V = any,
138
+ D extends Dependencies = {},
139
+ S extends Scope = Scope,
140
+ > =
141
+ | LazyInjectable<V, S>
142
+ | ValueInjectable<V>
143
+ | FactoryInjectable<V, D, S, any>
144
+ | BaseClassInjectable<V, D, S>
145
+
146
+ export type AnyInjectable<T = any, S extends Scope = Scope> = Injectable<
147
+ T,
148
+ any,
149
+ S
150
+ >
151
+
152
+ export const isLazyInjectable = (
153
+ injectable: any,
154
+ ): injectable is LazyInjectable<any> => kLazyInjectable in injectable
155
+ export const isFactoryInjectable = (
156
+ injectable: any,
157
+ ): injectable is FactoryInjectable<any> => kFactoryInjectable in injectable
158
+ export const isClassInjectable = (
159
+ injectable: any,
160
+ ): injectable is ClassInjectable<any> => kClassInjectable in injectable
161
+ export const isValueInjectable = (
162
+ injectable: any,
163
+ ): injectable is ValueInjectable<any> => kValueInjectable in injectable
164
+ export const isInjectable = (
165
+ injectable: any,
166
+ ): injectable is AnyInjectable<any> => kInjectable in injectable
167
+ export const isOptionalInjectable = (
168
+ injectable: any,
169
+ ): injectable is DependencyOptional<any> => kOptionalDependency in injectable
170
+
171
+ export function getInjectableScope(injectable: AnyInjectable) {
172
+ let scope = injectable.scope
173
+ const deps = Object.values(injectable.dependencies as Dependencies)
174
+ for (const dependency of deps) {
175
+ const injectable = getDepedencencyInjectable(dependency)
176
+ const dependencyScope = getInjectableScope(injectable)
177
+ if (compareScope(dependencyScope, '>', scope)) {
178
+ scope = dependencyScope
179
+ }
180
+ }
181
+ return scope
182
+ }
183
+
184
+ export function getDepedencencyInjectable(
185
+ dependency: Depedency,
186
+ ): AnyInjectable {
187
+ if (kOptionalDependency in dependency) {
188
+ return dependency.injectable
189
+ }
190
+ return dependency
191
+ }
192
+
193
+ export function createOptionalInjectable<T extends AnyInjectable>(
194
+ injectable: T,
195
+ ) {
196
+ return {
197
+ [kOptionalDependency]: true,
198
+ injectable,
199
+ } as DependencyOptional<T>
200
+ }
201
+
202
+ export function createLazyInjectable<T, S extends Scope = Scope.Global>(
203
+ scope = Scope.Global as S,
204
+ label?: string,
205
+ stackTraceDepth = 0,
206
+ ): LazyInjectable<T, S> {
207
+ return Object.freeze({
208
+ scope,
209
+ dependencies: {},
210
+ label,
211
+ stack: tryCaptureStackTrace(stackTraceDepth),
212
+ [kInjectable]: true,
213
+ [kLazyInjectable]: true as unknown as T,
214
+ })
215
+ }
216
+
217
+ export function createValueInjectable<T>(
218
+ value: T,
219
+ label?: string,
220
+ stackTraceDepth = 0,
221
+ ): ValueInjectable<T> {
222
+ return Object.freeze({
223
+ value,
224
+ scope: Scope.Global,
225
+ dependencies: {},
226
+ label,
227
+ stack: tryCaptureStackTrace(stackTraceDepth),
228
+ [kInjectable]: true,
229
+ [kValueInjectable]: true,
230
+ })
231
+ }
232
+
233
+ export function createFactoryInjectable<
234
+ T,
235
+ D extends Dependencies = {},
236
+ S extends Scope = Scope.Global,
237
+ P = T,
238
+ >(
239
+ paramsOrFactory:
240
+ | {
241
+ dependencies?: D
242
+ scope?: S
243
+ pick?: InjectablePickType<P, T>
244
+ factory: InjectableFactoryType<P, D>
245
+ dispose?: InjectableDisposeType<P, D>
246
+ }
247
+ | InjectableFactoryType<P, D>,
248
+ label?: string,
249
+ stackTraceDepth = 0,
250
+ ): FactoryInjectable<null extends T ? P : T, D, S, P> {
251
+ const isFactory = typeof paramsOrFactory === 'function'
252
+ const params = isFactory ? { factory: paramsOrFactory } : paramsOrFactory
253
+ const injectable = {
254
+ dependencies: (params.dependencies ?? {}) as D,
255
+ scope: (params.scope ?? Scope.Global) as S,
256
+ factory: params.factory,
257
+ dispose: params.dispose,
258
+ pick: params.pick ?? ((instance: P) => instance as unknown as T),
259
+ label,
260
+ stack: tryCaptureStackTrace(stackTraceDepth),
261
+ [kInjectable]: true,
262
+ [kFactoryInjectable]: true,
263
+ }
264
+ const actualScope = getInjectableScope(injectable)
265
+ if (
266
+ !isFactory &&
267
+ params.scope &&
268
+ ScopeStrictness[actualScope] > ScopeStrictness[params.scope]
269
+ )
270
+ throw new Error(
271
+ `Invalid scope ${params.scope} for factory injectable: dependencies have stricter scope - ${actualScope}`,
272
+ )
273
+ injectable.scope = actualScope as unknown as S
274
+ return Object.freeze(injectable) as any
275
+ }
276
+
277
+ export const createClassInjectable = <
278
+ D extends Dependencies = {},
279
+ S extends Scope = Scope.Global,
280
+ >(
281
+ dependencies: D = {} as D,
282
+ scope: S = Scope.Global as S,
283
+ stackTraceDepth = 0,
284
+ ): ClassInjectable<ClassInstance<typeof klass>, D, S> => {
285
+ const klass = class {
286
+ static dependencies = dependencies
287
+ static scope = scope
288
+ static stack = tryCaptureStackTrace(stackTraceDepth + 2)
289
+ static [kInjectable] = true
290
+ static [kClassInjectable] = true
291
+
292
+ static get label() {
293
+ // biome-ignore lint/complexity/noThisInStatic:
294
+ return this.name
295
+ }
296
+
297
+ constructor(public $context: DependencyContext<D>) {}
298
+
299
+ protected async $onCreate() {}
300
+ protected async $onDispose() {}
301
+ }
302
+ return klass
303
+ }
304
+
305
+ export function createExtendableClassInjectable<
306
+ B extends ClassConstructor<any>,
307
+ D extends Dependencies = {},
308
+ S extends Scope = Scope.Global,
309
+ >(
310
+ baseClass: B,
311
+ dependencies: D = {} as D,
312
+ scope: S = Scope.Global as S,
313
+ stackTraceDepth = 0,
314
+ ): B extends ClassInjectable<any>
315
+ ? ClassInjectable<ClassInstance<B>, D, S>
316
+ : ClassInjectable<ClassInstance<B>, D, S, ClassConstructorArgs<B, []>> {
317
+ if (isClassInjectable(baseClass)) {
318
+ dependencies = Object.assign({}, baseClass.dependencies, dependencies)
319
+ if (compareScope(scope, '<', baseClass.scope)) {
320
+ throw new Error(
321
+ 'Invalid scope for injectable: base class has stricter scope',
322
+ )
323
+ }
324
+ }
325
+ // @ts-expect-error
326
+ return class extends baseClass {
327
+ static dependencies = dependencies
328
+ static scope = scope
329
+ static stack = tryCaptureStackTrace(stackTraceDepth)
330
+ static [kInjectable] = true
331
+ static [kClassInjectable] = true
332
+
333
+ static get label() {
334
+ // biome-ignore lint/complexity/noThisInStatic:
335
+ return this.name
336
+ }
337
+
338
+ $context!: DependencyContext<D>
339
+
340
+ constructor(...args: any[]) {
341
+ const [$context, ...baseClassArgs] = args
342
+ if (isClassInjectable(baseClass)) {
343
+ super($context)
344
+ } else {
345
+ super(...baseClassArgs)
346
+ this.$context = $context
347
+ }
348
+ }
349
+
350
+ protected async $onCreate() {
351
+ await super.$onCreate?.()
352
+ }
353
+
354
+ protected async $onDispose() {
355
+ await super.$onDispose?.()
356
+ }
357
+ }
358
+ }
359
+
360
+ export type DependenciesSubstitution<T extends Dependencies> = {
361
+ [K in keyof T]?: T[K] extends AnyInjectable<infer Type>
362
+ ? AnyInjectable<Type> | DependenciesSubstitution<T[K]['dependencies']>
363
+ : never
364
+ }
365
+
366
+ export function substitute<
367
+ T extends
368
+ | FactoryInjectable<any, any, Scope>
369
+ | BaseClassInjectable<any, any, Scope>,
370
+ >(
371
+ injectable: T,
372
+ substitution: DependenciesSubstitution<T['dependencies']>,
373
+ stackTraceDepth = 0,
374
+ ): T {
375
+ const dependencies = { ...injectable.dependencies }
376
+ const depth = stackTraceDepth + 1
377
+ for (const key in substitution) {
378
+ const value = substitution[key]!
379
+ if (key in dependencies) {
380
+ const original = dependencies[key]
381
+ if (isInjectable(value)) {
382
+ dependencies[key] = value
383
+ } else if (isClassInjectable(original) || isFactoryInjectable(original)) {
384
+ dependencies[key] = substitute(original, value, depth)
385
+ }
386
+ }
387
+ }
388
+
389
+ if (isClassInjectable(injectable)) {
390
+ // @ts-expect-error
391
+ return createExtendableClassInjectable(
392
+ injectable,
393
+ dependencies,
394
+ injectable.scope,
395
+ depth,
396
+ )
397
+ } else if (isFactoryInjectable(injectable)) {
398
+ // @ts-expect-error
399
+ return createFactoryInjectable(
400
+ {
401
+ ...injectable,
402
+ dependencies,
403
+ },
404
+ injectable.label,
405
+ depth,
406
+ )
407
+ }
408
+
409
+ throw new Error('Invalid injectable type')
410
+ }
411
+
412
+ export function compareScope(
413
+ left: Scope,
414
+ operator: '>' | '<' | '>=' | '<=' | '=' | '!=',
415
+ right: Scope,
416
+ ) {
417
+ const leftScope = ScopeStrictness[left]
418
+ const rightScope = ScopeStrictness[right]
419
+ switch (operator) {
420
+ case '=':
421
+ return leftScope === rightScope
422
+ case '!=':
423
+ return leftScope !== rightScope
424
+ case '>':
425
+ return leftScope > rightScope
426
+ case '<':
427
+ return leftScope < rightScope
428
+ case '>=':
429
+ return leftScope >= rightScope
430
+ case '<=':
431
+ return leftScope <= rightScope
432
+ default:
433
+ throw new Error('Invalid operator')
434
+ }
435
+ }
436
+
437
+ export const CoreInjectables = {
438
+ logger: createLazyInjectable<Logger>(Scope.Global, 'Logger'),
439
+ inject: createLazyInjectable<InjectFn>(Scope.Global, 'Inject function'),
440
+ }
package/src/registry.ts CHANGED
@@ -1,10 +1,10 @@
1
+ import { type Hook, Scope } from './enums.ts'
2
+ import { Hooks, type HookType } from './hooks.ts'
1
3
  import {
2
4
  type AnyInjectable,
3
5
  type Dependant,
4
6
  getInjectableScope,
5
- } from './container.ts'
6
- import { type Hook, Scope } from './enums.ts'
7
- import { Hooks, type HookType } from './hooks.ts'
7
+ } from './injectables.ts'
8
8
  import type { Logger } from './logger.ts'
9
9
 
10
10
  export class Registry {