├── .editorconfig ├── .github └── workflows │ ├── build.yml │ └── publish-egg.yml ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── LICENSE ├── README.md ├── decorators.ts ├── egg.yml ├── metadata.ts ├── mod.ts ├── resolution.ts ├── scripts.yml ├── service.ts ├── service_collection.ts ├── service_multi_collection.ts ├── service_multi_collection_test.ts ├── test.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_size = 2 5 | indent_style = space 6 | insert_final_newline = true 7 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build & Test 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v1 11 | - uses: denolib/setup-deno@master 12 | with: 13 | deno-version: 1.x 14 | - name: Install Velociraptor 15 | run: | 16 | deno install -qAn vr https://x.nest.land/velociraptor@1.0.1/cli.ts 17 | echo "$HOME/.deno/bin" >> $GITHUB_PATH 18 | - name: Build 19 | run: vr build 20 | - name: Test 21 | run: vr test 22 | -------------------------------------------------------------------------------- /.github/workflows/publish-egg.yml: -------------------------------------------------------------------------------- 1 | name: Publish Egg 2 | 3 | on: 4 | release: 5 | types: 6 | - created 7 | 8 | jobs: 9 | publish-egg: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: denolib/setup-deno@master 14 | with: 15 | deno-version: 1.x 16 | - name: Setup eggs CLI 17 | run: | 18 | deno install -Afq --unstable https://x.nest.land/eggs@0.3.8/eggs.ts 19 | echo "$HOME/.deno/bin" >> $GITHUB_PATH 20 | - name: Publish Egg to Nest.land 21 | run: | 22 | eggs link --key ${NEST_LAND_API_KEY} 23 | eggs publish 24 | env: 25 | NEST_LAND_API_KEY: ${{secrets.NEST_LAND_API_KEY}} 26 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | // { 5 | // "name": "Launch Deno", 6 | // "type": "node", 7 | // "request": "launch", 8 | // "cwd": "${workspaceFolder}", 9 | // "runtimeExecutable": "deno", 10 | // "runtimeArgs": ["run", "--inspect-brk", "-A", "main.ts"], 11 | // "port": 9229, 12 | // "outputCapture": "std" 13 | // } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.codeActionsOnSave": { 4 | "source.organizeImports": true 5 | }, 6 | "deno.enable": true, 7 | "[typescript]": { 8 | "editor.defaultFormatter": "denoland.vscode-deno" 9 | }, 10 | "deno.suggest.imports.hosts": { 11 | "https://cdn.skypack.dev": false, 12 | "https://deno.land": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "Run File", 8 | "type": "shell", 9 | "command": "${file}", 10 | "problemMatcher": [], 11 | "group": { 12 | "kind": "build", 13 | "isDefault": true 14 | } 15 | }, 16 | { 17 | "label": "Test", 18 | "type": "shell", 19 | "command": "deno test -c ./tsconfig.json", 20 | "options": { 21 | "cwd": "${workspaceFolder}" 22 | }, 23 | "problemMatcher": [], 24 | "group": { 25 | "kind": "test", 26 | "isDefault": true 27 | } 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 luvies 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dependency Injection 2 | 3 | ![Build & Test](https://github.com/luvies/deno_di/workflows/Build%20&%20Test/badge.svg) [![nest badge](https://nest.land/badge.svg)](https://nest.land/package/di) 4 | 5 | A dependency injection (inversion of control) module for Deno. 6 | 7 | This is a work in-progress, and breaking changes may be made without warning. 8 | 9 | Available at: 10 | 11 | ``` 12 | https://deno.land/x/di 13 | ``` 14 | 15 | ``` 16 | https://nest.land/package/di 17 | ``` 18 | 19 | ## Contents 20 | 21 | - [API](#api) 22 | - [Services](#services) 23 | - [Newable Services](#newable-services) 24 | - [Abstract Classes](#abstract-classes) 25 | - [Dynamic Services](#dynamic-services) 26 | - [Static Services](#static-services) 27 | - [Lifetimes](#lifetimes) 28 | - [Service Multi-Collection](#service-multi-collection) 29 | - [Setup](#setup) 30 | 31 | ## API 32 | 33 | You can view the complete API using [docs.deno.land](https://doc.deno.land/https/deno.land/x/di/mod.ts). 34 | 35 | Before you can use this module, follow the [setup](#setup) section, as this module relies on decorators and TypeScript's metadata system. 36 | 37 | ### Services 38 | 39 | The main feature of this module is the `ServiceCollection`. This is a class which hold all your app's services in, and will resolve the dependencies of any requested service for you. Services can be one of the following: 40 | 41 | - Newable 42 | - Dynamic 43 | - Static 44 | 45 | To use the service collection, use some setup code like this near the start of your app: 46 | 47 | ```ts 48 | const serviceCollection = new ServiceCollection(); 49 | 50 | // This is also showing the different lifetimes of a service and how each one 51 | // may be bound to a name. 52 | serviceCollection.addTransient(A); 53 | serviceCollection.addScoped(IB, B); 54 | serviceCollection.addSingleton(types.IC, C); 55 | ``` 56 | 57 | #### Newable Services 58 | 59 | Newable services are just classes that have the `@Service()` decorator applied. These classes can delare their dependencies as constructor parameters or as properties: 60 | 61 | ```ts 62 | // Service with no dependencies. 63 | @Service() 64 | class A {} 65 | 66 | // Constructor injection 67 | @Service() 68 | class B { 69 | constructor(private a: A) {} 70 | } 71 | 72 | // Property injection. 73 | @Service() 74 | class C { 75 | @Inject() 76 | private a!: A; 77 | } 78 | 79 | // Service collection setup. 80 | const serviceCollection = new ServiceCollection(); 81 | serviceCollection.addTransient(A); 82 | serviceCollection.addTransient(B); 83 | serviceCollection.addTransient(C); 84 | 85 | const b = serviceCollection.get(B); 86 | assert(b instanceof B); 87 | assert(b.a instanceof A); 88 | 89 | const c = serviceCollection.get(C); 90 | assert(c instanceof C); 91 | assert(c.a instanceof A); 92 | ``` 93 | 94 | For property injection, notice that it needed the `@Inject()` decorator. This decorator is used to either make TypeScript emit the design types for that property, and optionally to allow you to manually specify the type to be injected. This second feature is useful for interface injection (as interfaces do no exist at runtime): 95 | 96 | ```ts 97 | // Type identifiers (these can also be strings). 98 | const types = { 99 | IA: Symbol("IA"), 100 | IB: Symbol("IB"), 101 | }; 102 | 103 | // Interfaces 104 | interface IA { 105 | foo(): void; 106 | } 107 | 108 | interface IB { 109 | bar(): void; 110 | } 111 | 112 | // Implementations 113 | @Service() 114 | class A implements IA { 115 | public foo(): void { 116 | console.log("foo"); 117 | } 118 | } 119 | 120 | @Service() 121 | class B implements IB { 122 | // Using property injection. 123 | @Inject(types.IA) 124 | private propA!: IA; 125 | 126 | // Using constructor injection. 127 | constructor( 128 | @Inject(types.IA) 129 | private constructA: IA 130 | ) {} 131 | 132 | public bar(): void { 133 | console.log("bar"); 134 | } 135 | } 136 | 137 | // Service collection setup. 138 | const serviceCollection = new ServiceCollection(); 139 | serviceCollection.addTransient(types.IA, A); 140 | serviceCollection.addTransient(types.IB, B); 141 | ``` 142 | 143 | ##### Abstract Classes 144 | 145 | Since interfaces do not exist at runtime, you have to manually specify the identifiers when declaring a dependency. This can become quite obtuse, and prevents good typing from being used. A better alternative is to use abstract classes. These classes cannot be instantiated (TypeScript prevents you from doing it), but can be used entirely as a replacement for interfaces that work with dependency type inference. 146 | 147 | ```ts 148 | // Delcare the abstract classes 149 | abstract class IA { 150 | foo(): void; 151 | } 152 | 153 | abstract class IB { 154 | bar(): void; 155 | } 156 | 157 | // Provide implementations. 158 | class A implements IA { 159 | public foo() { 160 | console.log("foo"); 161 | } 162 | } 163 | 164 | class B implements IB { 165 | constructor( 166 | // Notice how we can now infer the type, as it will exist at runtime. 167 | // This means we don't need to manually add @Inject decorators 168 | // (you still do with property injection, but you don't need the identifier argument). 169 | private a: IA 170 | ) {} 171 | 172 | public bar() { 173 | console.log("bar"); 174 | } 175 | } 176 | 177 | // Service collection setup. 178 | const serviceCollection = new ServiceCollection(); 179 | serviceCollection.addTransient(IA, A); 180 | serviceCollection.addTransient(IB, B); 181 | 182 | // We can then use the abstract class to instantiate the implementation without 183 | // needing to do anything else. This also provides type inference, i.e. 184 | // ib will be of type `IB` 185 | const ib = serviceCollection.get(IB); 186 | ``` 187 | 188 | #### Dynamic Services 189 | 190 | Dynamic services are just functions that are called when resolving the dependency tree, and the values of which are injected into the services. 191 | 192 | ```ts 193 | const types = { 194 | data: Symbol("data"), 195 | }; 196 | 197 | function getData(): string[] { 198 | return ["data"]; 199 | } 200 | 201 | @Service() 202 | class A { 203 | constructor( 204 | @Inject(types.data) 205 | private data: string[] 206 | ) {} 207 | } 208 | 209 | // Service collection setup. 210 | const serviceCollection = new ServiceCollection(); 211 | serviceCollection.addTransientDynamic(types.data, getData); 212 | serviceCollection.addTransient(A); 213 | 214 | const a = serviceCollection.get(A); 215 | assert(a instanceof A); 216 | assertEquals(a.data, ["data"]); 217 | ``` 218 | 219 | #### Static Services 220 | 221 | Static services are just pure values that are bound directly into the collection, and then can be used as a dependency by other services. 222 | 223 | ```ts 224 | const types = { 225 | value: Symbol("value"), 226 | }; 227 | 228 | const value = "static value"; 229 | 230 | @Service() 231 | class A { 232 | constructor( 233 | @Inject(types.value) 234 | private value: string 235 | ) {} 236 | } 237 | 238 | // Service collection setup. 239 | const serviceCollection = new ServiceCollection(); 240 | serviceCollection.addStatic(types.value, value); 241 | serviceCollection.addTransient(A); 242 | 243 | const a = serviceCollection.get(A); 244 | assert(a instanceof A); 245 | assertEquals(a.value, value); 246 | ``` 247 | 248 | ### Lifetimes 249 | 250 | Newable and dynamic services have 'lifetimes', which refers to how often it is instantiated or called when making a request. 251 | 252 | #### Transient 253 | 254 | For transient services, every single time it exists in the dependency tree, it is created again. This means that if you request service `A` from a collection, and it depends on `B` and `C`, which both depends on `D` (which is transient), then `D` will be created as new for both `B` and `C`. 255 | 256 | #### Scoped 257 | 258 | Scoped services are reused for the duration of the request. This means, using the example before, `D` (which is scoped now) would be the same instance between `B` and `C`, but if you requested `A` again, a new instance would be created. 259 | 260 | #### Singleton 261 | 262 | Singleton service are only ever created once, and this instance is reused throughout the entire lifetime of the `ServiceCollection`. Using the previous example again, but with `D` being a singleton, everytime you requested `A`, the instance of `D` is reused within and across requests. 263 | 264 | ### Service Multi-Collection 265 | 266 | If you have multiple `ServiceCollection`s that you want to resolve a service from where dependencies may only exist in one or a subset, then you can use the `ServiceMultiCollection` class to treat all the `ServiceCollection`s as a single `ServiceCollection`. 267 | 268 | A use-case for this is for something like a web-framework. When the framework loads, it would add all the controllers, middleware, and data service to a main service collection. Some of these services may depend on other services that contain request information. As multiple requests may be handled in parallel, it is not ideal to add the request-only services to the main collection, so you can instead create a second, request-only, collection, and use the `ServiceMultiCollection` to allow the main services and request-only services to resolve dependencies from each other. 269 | 270 | Dependency resolution in this class can happen bi-directionally, meaning that each container may depend on services from the other (as long as it does not cause a circular dependency) without issue. However, services are resolved from collections in the order the collections were added to the multi-collection. This means that if you add collection `X` and _then_ collection `Y`, which both have service `A`, then `A` will be resolved from collection `X`, not `Y`, as it was added first. This allows a form a priority between collections. 271 | 272 | Example: 273 | 274 | ```ts 275 | // Services. 276 | @Service() 277 | class A {} 278 | 279 | @Service() 280 | class B { 281 | constructor(private a: A) {} 282 | } 283 | 284 | @Service() 285 | class C { 286 | constructor(private b: B) {} 287 | } 288 | 289 | // Service collection setup 290 | const collection1 = new ServiceCollection(); 291 | collection1.addTransient(A); 292 | collection1.addTransient(C); 293 | 294 | const collection2 = new ServiceCollection(); 295 | collection2.addTransient(B); 296 | 297 | // Multi-collection setup 298 | const multiCollection = new ServiceMultiCollection(collection1, collection2); 299 | 300 | // Resolve services across collections. 301 | const c = multiCollection.get(C); 302 | assert(c instanceof C); 303 | assert(c.b instanceof B); 304 | assert(c.b.a instanceof A); 305 | ``` 306 | 307 | ## Setup 308 | 309 | This modules requires the following options to be set in the tsconfig: 310 | 311 | ```json 312 | { 313 | "compilerOptions": { 314 | "experimentalDecorators": true, 315 | "emitDecoratorMetadata": true 316 | } 317 | } 318 | ``` 319 | 320 | You will also need to polyfill Reflect metadata. The recommended way is: 321 | 322 | ```ts 323 | import "https://cdn.skypack.dev/@abraham/reflection@0.8.0"; 324 | ``` 325 | -------------------------------------------------------------------------------- /decorators.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DiParamTypes, 3 | getClassParamTypes, 4 | getDiClassParamTypes, 5 | getMemberType, 6 | isClassTagged, 7 | setDiClassParamTypes, 8 | tagClass, 9 | } from "./metadata.ts"; 10 | import { isServiceIdent, ServiceIdent } from "./service.ts"; 11 | 12 | /** 13 | * Declares a class as a service that can be used with dependency injection. 14 | */ 15 | export function Service(): ClassDecorator { 16 | return (target: Function) => { 17 | if (isClassTagged(target)) { 18 | throw new Error("Cannot decorate class multiple times"); 19 | } 20 | 21 | tagClass(target); 22 | }; 23 | } 24 | 25 | /** 26 | * Used to declare a propery for injection, or to specify the service identifier 27 | * for a property or constructor parameter. 28 | * 29 | * @remarks 30 | * All injection properties need this decorator with or without the identifier. 31 | * 32 | * If an identifier is not given, then the type of the property or parameter 33 | * must exist at runtime (i.e. it must be a class or abstract class). 34 | */ 35 | export function Inject( 36 | ident?: ServiceIdent, 37 | ) { 38 | return ( 39 | target: object | Function, 40 | propKey: string | symbol | undefined, 41 | paramIndex?: number, 42 | ) => { 43 | const tgt = typeof target === "function" ? target : target.constructor; 44 | 45 | const metadata: DiParamTypes = getDiClassParamTypes(tgt) ?? 46 | { prop: new Map(), param: new Map() }; 47 | 48 | let cident: ServiceIdent | undefined = ident; 49 | if (typeof paramIndex === "number") { 50 | if (typeof cident === "undefined") { 51 | const designParams = getClassParamTypes(tgt); 52 | const designType = designParams?.[paramIndex]; 53 | 54 | if (!isServiceIdent(designType)) { 55 | throw new Error( 56 | `Cannot determine type of parameter ${paramIndex} for class ${tgt.name}, either change the type or use Inject(type)`, 57 | ); 58 | } 59 | 60 | cident = designType as ServiceIdent; 61 | } 62 | 63 | metadata.param.set(paramIndex, cident); 64 | } else { 65 | if (typeof propKey === "undefined") { 66 | throw new Error( 67 | "Inject can only be used on properties and construct parameters", 68 | ); 69 | } 70 | 71 | if (typeof cident === "undefined") { 72 | const designType = getMemberType(tgt.prototype, propKey); 73 | 74 | if (!isServiceIdent(designType)) { 75 | throw new Error( 76 | `Cannot determine type of property ${ 77 | String(propKey) 78 | } for class ${tgt.name}, either chagen the type of use Inject(type)`, 79 | ); 80 | } 81 | 82 | cident = designType as ServiceIdent; 83 | } 84 | 85 | metadata.prop.set(propKey, cident); 86 | } 87 | 88 | setDiClassParamTypes(tgt, metadata); 89 | }; 90 | } 91 | -------------------------------------------------------------------------------- /egg.yml: -------------------------------------------------------------------------------- 1 | name: di 2 | description: A dependency injection (DI/IoC/Inversion of Control) module 3 | version: 0.1.1 4 | entry: ./mod.ts 5 | stable: false 6 | unlisted: false 7 | fmt: false 8 | repository: https://github.com/luvies/deno_di 9 | files: 10 | - ./*.ts 11 | - ./README.md 12 | -------------------------------------------------------------------------------- /metadata.ts: -------------------------------------------------------------------------------- 1 | import { ServiceIdent } from "./service.ts"; 2 | 3 | // Alias Reflect to prevent type errors 4 | const Ref = Reflect as any; 5 | 6 | const classTag = Symbol("class-tag"); 7 | 8 | enum MetadataTags { 9 | // Metadata tags used by TS. 10 | TsParamTypes = "design:paramtypes", 11 | TsType = "design:type", 12 | 13 | // Metadata tags used by this module. 14 | DiParamTypes = "di:paramtypes", 15 | DiClassTag = "di:classtag", 16 | } 17 | 18 | export type DesignType = object | undefined; 19 | 20 | export interface DiParamTypes { 21 | prop: Map>; 22 | param: Map>; 23 | } 24 | 25 | export function getClassParamTypes( 26 | target: Function, 27 | ): DesignType[] | undefined { 28 | return Ref.getOwnMetadata(MetadataTags.TsParamTypes, target); 29 | } 30 | 31 | export function getMemberType( 32 | target: object, 33 | propKey: string | symbol, 34 | ): DesignType | undefined { 35 | return Ref.getMetadata(MetadataTags.TsType, target, propKey); 36 | } 37 | 38 | export function getDiClassParamTypes( 39 | target: Function, 40 | ): DiParamTypes | undefined { 41 | return Ref.getOwnMetadata(MetadataTags.DiParamTypes, target); 42 | } 43 | 44 | export function setDiClassParamTypes( 45 | target: Function, 46 | paramTypes: DiParamTypes, 47 | ): void { 48 | Ref.defineMetadata(MetadataTags.DiParamTypes, paramTypes, target); 49 | } 50 | 51 | export function isClassTagged(target: Function): boolean { 52 | const metadata = Ref.getOwnMetadata(MetadataTags.DiClassTag, target); 53 | 54 | return typeof metadata !== "undefined"; 55 | } 56 | 57 | export function tagClass(target: Function) { 58 | Ref.defineMetadata(MetadataTags.DiClassTag, classTag, target); 59 | } 60 | -------------------------------------------------------------------------------- /mod.ts: -------------------------------------------------------------------------------- 1 | export * from "./decorators.ts"; 2 | export * from "./service_collection.ts"; 3 | export * from "./service_multi_collection.ts"; 4 | -------------------------------------------------------------------------------- /resolution.ts: -------------------------------------------------------------------------------- 1 | import { getClassParamTypes, getDiClassParamTypes } from "./metadata.ts"; 2 | import { 3 | Cacheable, 4 | isServiceIdent, 5 | Kind, 6 | Lifetime, 7 | LifetimedService, 8 | Service, 9 | ServiceIdent, 10 | ServiceStore, 11 | } from "./service.ts"; 12 | 13 | function tryGet( 14 | ident: ServiceIdent, 15 | serviceStores: ServiceStore[], 16 | ): Service | undefined { 17 | for (const serviceMap of serviceStores) { 18 | const service = serviceMap.get(ident); 19 | 20 | if (service) { 21 | return service; 22 | } 23 | } 24 | 25 | return undefined; 26 | } 27 | 28 | interface Context { 29 | services: ServiceStore; 30 | cache: Map, any | undefined>; 31 | } 32 | 33 | type ResolveParents = Set>; 34 | 35 | function loadFromCache( 36 | ident: ServiceIdent, 37 | service: Cacheable & LifetimedService, 38 | context: Context, 39 | ): T | undefined { 40 | switch (service.lifetime) { 41 | case Lifetime.Transient: 42 | return undefined; 43 | case Lifetime.Scoped: 44 | return context.cache.get(ident); 45 | case Lifetime.Singleton: 46 | return service.cache; 47 | } 48 | } 49 | 50 | function createUsingCache( 51 | ident: ServiceIdent, 52 | creator: () => T, 53 | service: Cacheable & LifetimedService, 54 | context: Context, 55 | ): T { 56 | const val = creator(); 57 | 58 | switch (service.lifetime) { 59 | case Lifetime.Transient: 60 | break; 61 | case Lifetime.Scoped: 62 | context.cache.set(ident, val); 63 | break; 64 | case Lifetime.Singleton: 65 | service.cache = val; 66 | } 67 | 68 | return val; 69 | } 70 | 71 | function preventCircularGraph( 72 | ident: ServiceIdent, 73 | parents: ResolveParents, 74 | ) { 75 | if (parents.has(ident)) { 76 | throw new Error( 77 | `Circular dependency detected: ${ 78 | [...parents, ident].map((i) => String(i)).join(" -> ") 79 | }`, 80 | ); 81 | } 82 | } 83 | 84 | function _resolve( 85 | ident: ServiceIdent, 86 | parents: ResolveParents, 87 | serviceStores: ServiceStore[], 88 | context: Context, 89 | ): T { 90 | const service = tryGet(ident, serviceStores); 91 | 92 | switch (service?.kind) { 93 | case Kind.Newable: { 94 | const cached = loadFromCache(ident, service as any, context); 95 | if (typeof cached !== "undefined") { 96 | return cached; 97 | } 98 | 99 | const manualDeps = getDiClassParamTypes(service.impl); 100 | const autoDeps = getClassParamTypes(service.impl); 101 | 102 | // Gather constructor arguments 103 | const args: any[] = []; 104 | if (autoDeps) { 105 | for (const [designType, i] of autoDeps.map((d, i) => [d, i] as const)) { 106 | const argIdent = manualDeps?.param.get(i) ?? designType; 107 | if (!isServiceIdent(argIdent)) { 108 | throw new Error( 109 | `Constructor parameter ${i} of class ${service.impl.name} is not a valid identifier`, 110 | ); 111 | } 112 | 113 | preventCircularGraph(argIdent, parents); 114 | args.push( 115 | _resolve( 116 | argIdent, 117 | new Set([...parents, argIdent]), 118 | serviceStores, 119 | context, 120 | ), 121 | ); 122 | } 123 | } 124 | 125 | // Gather prop values 126 | const props = new Map(); 127 | if (manualDeps) { 128 | for (const [propKey, propIdent] of manualDeps.prop) { 129 | preventCircularGraph(propIdent, parents); 130 | props.set( 131 | propKey, 132 | _resolve( 133 | propIdent, 134 | new Set([...parents, propIdent]), 135 | serviceStores, 136 | context, 137 | ), 138 | ); 139 | } 140 | } 141 | 142 | return createUsingCache( 143 | ident, 144 | () => { 145 | const serv = new service.impl(...args); 146 | 147 | for (const [propKey, propVal] of props) { 148 | serv[propKey] = propVal; 149 | } 150 | 151 | return serv; 152 | }, 153 | service, 154 | context, 155 | ); 156 | } 157 | case Kind.Dynamic: { 158 | const val = loadFromCache(ident, service, context); 159 | 160 | return val ?? createUsingCache(ident, service.fn, service, context); 161 | } 162 | case Kind.Static: { 163 | return service.value; 164 | } 165 | case undefined: 166 | throw new Error(`Service ${String(ident)} does not exist in container`); 167 | } 168 | } 169 | 170 | export function resolve( 171 | ident: ServiceIdent, 172 | serviceStores: ServiceStore[], 173 | ): T { 174 | return _resolve( 175 | ident, 176 | new Set(), 177 | serviceStores, 178 | { services: new Map(), cache: new Map() }, 179 | ); 180 | } 181 | -------------------------------------------------------------------------------- /scripts.yml: -------------------------------------------------------------------------------- 1 | scripts: 2 | test: deno test --config ./tsconfig.json 3 | build: deno cache --reload -c ./tsconfig.json ./mod.ts 4 | -------------------------------------------------------------------------------- /service.ts: -------------------------------------------------------------------------------- 1 | export enum Lifetime { 2 | Transient, 3 | Scoped, 4 | Singleton, 5 | } 6 | 7 | export enum Kind { 8 | Newable, 9 | Dynamic, 10 | Static, 11 | } 12 | 13 | export interface Newable { 14 | new (...args: any[]): T; 15 | } 16 | 17 | export interface Abstract { 18 | prototype: T; 19 | } 20 | 21 | export type ServiceIdent = 22 | | string 23 | | symbol 24 | | Newable 25 | | Abstract; 26 | 27 | export function isServiceIdent(ident: unknown): ident is ServiceIdent { 28 | if (typeof ident === "string" || typeof ident === "symbol") { 29 | return true; 30 | } 31 | 32 | if (typeof ident === "function") { 33 | return ident !== Object; 34 | } 35 | 36 | return false; 37 | } 38 | 39 | export interface GenericService { 40 | kind: T; 41 | ident: ServiceIdent; 42 | } 43 | 44 | export interface LifetimedService extends GenericService { 45 | lifetime: Lifetime; 46 | } 47 | 48 | export interface Cacheable { 49 | cache?: T; 50 | } 51 | 52 | export interface NewableService 53 | extends LifetimedService, Cacheable> { 54 | impl: Newable; 55 | } 56 | 57 | export type DynamicValue = () => any; 58 | 59 | export interface DynamicService 60 | extends LifetimedService, Cacheable { 61 | fn: DynamicValue; 62 | } 63 | 64 | export type StaticValue = any; 65 | 66 | export interface StaticService extends GenericService { 67 | value: StaticValue; 68 | } 69 | 70 | export type Service = NewableService | DynamicService | StaticService; 71 | 72 | export type ServiceStore = Map, Service>; 73 | -------------------------------------------------------------------------------- /service_collection.ts: -------------------------------------------------------------------------------- 1 | import { isClassTagged } from "./metadata.ts"; 2 | import { resolve } from "./resolution.ts"; 3 | import { 4 | DynamicValue, 5 | Kind, 6 | Lifetime, 7 | Newable, 8 | Service, 9 | ServiceIdent, 10 | ServiceStore, 11 | StaticValue, 12 | } from "./service.ts"; 13 | 14 | export interface IServiceCollection { 15 | /** 16 | * Gets a service from the collection using the rest of the services in the 17 | * collection to resolve the dependencies. 18 | */ 19 | get(ident: ServiceIdent): T; 20 | } 21 | 22 | /** 23 | * A collection of services. 24 | */ 25 | export class ServiceCollection implements IServiceCollection { 26 | private _services: ServiceStore = new Map(); 27 | 28 | /** 29 | * Gets a service from the collection using the rest of the services in the 30 | * collection to resolve the dependencies. 31 | */ 32 | public get(ident: ServiceIdent): T { 33 | return resolve(ident, [this._services]); 34 | } 35 | 36 | /** 37 | * Adds a class to the collection using itself as the identifier. 38 | * The service will be added with a transient lifetime. 39 | */ 40 | public addTransient(impl: Newable): void; 41 | /** 42 | * Adds a class to the collection using a custom identifier. 43 | * The service will be added with a transient lifetime. 44 | */ 45 | public addTransient(ident: ServiceIdent, impl: Newable): void; 46 | /** 47 | * @ignore 48 | */ 49 | public addTransient(ident: ServiceIdent, impl?: Newable) { 50 | this._addNewable(ident, newableImpl(ident, impl), Lifetime.Transient); 51 | } 52 | 53 | /** 54 | * Adds a class to the collection using itself as the identifier. 55 | * The service will be added with a scoped lifetime. 56 | */ 57 | public addScoped(impl: Newable): void; 58 | /** 59 | * Adds a class to the collection using a custom identifier. 60 | * The service will be added with a scoped lifetime. 61 | */ 62 | public addScoped(ident: ServiceIdent, impl: Newable): void; 63 | /** 64 | * @ignore 65 | */ 66 | public addScoped(ident: ServiceIdent, impl?: Newable) { 67 | this._addNewable(ident, newableImpl(ident, impl), Lifetime.Scoped); 68 | } 69 | 70 | /** 71 | * Adds a class to the collection using itself as the identifier. 72 | * The service will be added with a singleton lifetime. 73 | */ 74 | public addSingleton(impl: Newable): void; 75 | /** 76 | * Adds a class to the collection using a custom identifier. 77 | * The service will be added with a singleton lifetime. 78 | */ 79 | public addSingleton(ident: ServiceIdent, impl: Newable): void; 80 | /** 81 | * @ignore 82 | */ 83 | public addSingleton(ident: ServiceIdent, impl?: Newable) { 84 | this._addNewable(ident, newableImpl(ident, impl), Lifetime.Singleton); 85 | } 86 | 87 | /** 88 | * Adds a function to the collection using a custom identifier. 89 | * The service will be added with a transient lifetime. 90 | */ 91 | public addTransientDynamic(ident: ServiceIdent, fn: DynamicValue) { 92 | this._addDynamic(ident, fn, Lifetime.Transient); 93 | } 94 | 95 | /** 96 | * Adds a function to the collection using a custom identifier. 97 | * The service will be added with a scoped lifetime. 98 | */ 99 | public addScopedDynamic(ident: ServiceIdent, fn: DynamicValue) { 100 | this._addDynamic(ident, fn, Lifetime.Scoped); 101 | } 102 | 103 | /** 104 | * Adds a function to the collection using a custom identifier. 105 | * The service will be added with a singleton lifetime. 106 | */ 107 | public addSingletonDynamic(ident: ServiceIdent, fn: DynamicValue) { 108 | this._addDynamic(ident, fn, Lifetime.Singleton); 109 | } 110 | 111 | /** 112 | * Adds a static to the collection using a custom identifier. 113 | */ 114 | public addStatic(ident: ServiceIdent, value: StaticValue) { 115 | this._add({ 116 | kind: Kind.Static, 117 | ident, 118 | value, 119 | }); 120 | } 121 | 122 | private _addNewable( 123 | ident: ServiceIdent, 124 | impl: Newable, 125 | lifetime: Lifetime, 126 | ) { 127 | if (!isClassTagged(impl)) { 128 | throw new Error( 129 | `Class ${impl.name} has not been decorated with @Service`, 130 | ); 131 | } 132 | 133 | this._add({ 134 | kind: Kind.Newable, 135 | ident, 136 | lifetime, 137 | impl, 138 | }); 139 | } 140 | 141 | private _addDynamic( 142 | ident: ServiceIdent, 143 | fn: DynamicValue, 144 | lifetime: Lifetime, 145 | ) { 146 | this._add({ 147 | kind: Kind.Dynamic, 148 | ident, 149 | lifetime, 150 | fn, 151 | }); 152 | } 153 | 154 | private _add(service: Service) { 155 | if (this._services.has(service.ident)) { 156 | throw new Error("Service already added to container"); 157 | } 158 | 159 | this._services.set(service.ident, service); 160 | } 161 | } 162 | 163 | function newableImpl( 164 | ident: ServiceIdent, 165 | impl: Newable | undefined, 166 | ): Newable { 167 | if (!impl) { 168 | if (typeof ident !== "function") { 169 | throw new Error("Cannot self bind to a non-class"); 170 | } 171 | 172 | return ident; 173 | } 174 | 175 | return impl; 176 | } 177 | -------------------------------------------------------------------------------- /service_multi_collection.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from "./resolution.ts"; 2 | import { ServiceIdent, ServiceStore } from "./service.ts"; 3 | import { IServiceCollection, ServiceCollection } from "./service_collection.ts"; 4 | 5 | /** 6 | * Provides a mechanism to allow resolution of services from multiple 7 | * collections as-if they were one. 8 | */ 9 | export class ServiceMultiCollection implements IServiceCollection { 10 | private _serviceCollections: Set; 11 | 12 | public constructor(...serviceCollections: ServiceCollection[]) { 13 | this._serviceCollections = new Set(serviceCollections); 14 | } 15 | 16 | /** 17 | * Resolves the service uses all the service collections currently 18 | * held. 19 | * 20 | * @remarks 21 | * Resolution is done in priority order based on collection 22 | * insertion time, which means if 2 collections have the same service, 23 | * the collection that was added first will be the one that the service 24 | * will be resolved from. 25 | */ 26 | public get(ident: ServiceIdent): T { 27 | const serviceStores = Array.from(this._serviceCollections).map((sc) => 28 | (sc as any)._services as ServiceStore 29 | ); 30 | return resolve(ident, serviceStores); 31 | } 32 | 33 | /** 34 | * Adds the given collections to the multi-collection in order. 35 | */ 36 | public addCollections(...collections: ServiceCollection[]): void { 37 | for (const collection of collections) { 38 | this._serviceCollections.add(collection); 39 | } 40 | } 41 | 42 | /** 43 | * Removes the given collections from the multi-collection. 44 | */ 45 | public removeCollections(...collections: ServiceCollection[]): void { 46 | for (const collection of collections) { 47 | this._serviceCollections.delete(collection); 48 | } 49 | } 50 | 51 | /** 52 | * Clears all held collections. 53 | */ 54 | public clearCollections(): void { 55 | this._serviceCollections.clear(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /service_multi_collection_test.ts: -------------------------------------------------------------------------------- 1 | import "https://cdn.pika.dev/@abraham/reflection@^0.7.0"; 2 | import { assert, assertThrows } from "https://deno.land/std/testing/asserts.ts"; 3 | import { Service, ServiceCollection } from "./mod.ts"; 4 | import { ServiceMultiCollection } from "./service_multi_collection.ts"; 5 | 6 | Deno.test({ 7 | name: "multi-collection can resolve from collections in order", 8 | fn() { 9 | @Service() 10 | class A {} 11 | 12 | @Service() 13 | class B { 14 | constructor(public a: A) {} 15 | } 16 | 17 | const c1 = new ServiceCollection(); 18 | c1.addTransient(A); 19 | 20 | const c2 = new ServiceCollection(); 21 | c2.addTransient(B); 22 | 23 | assertThrows(() => { 24 | c2.get(B); 25 | }); 26 | 27 | const mc = new ServiceMultiCollection(c1, c2); 28 | 29 | const b = mc.get(B); 30 | assert(b instanceof B); 31 | assert(b.a instanceof A); 32 | }, 33 | }); 34 | 35 | Deno.test({ 36 | name: "multi-collection can resolve bi-directionally", 37 | fn() { 38 | @Service() 39 | class A {} 40 | 41 | @Service() 42 | class B { 43 | constructor(public a: A) {} 44 | } 45 | 46 | @Service() 47 | class C { 48 | constructor(public b: B) {} 49 | } 50 | 51 | // Service collection setup 52 | const c1 = new ServiceCollection(); 53 | c1.addTransient(A); 54 | c1.addTransient(C); 55 | 56 | const c2 = new ServiceCollection(); 57 | c2.addTransient(B); 58 | 59 | // Multi-collection setup 60 | const multiCollection = new ServiceMultiCollection(c1, c2); 61 | 62 | // Resolve services across collections. 63 | const c = multiCollection.get(C); 64 | assert(c instanceof C); 65 | assert(c.b instanceof B); 66 | assert(c.b.a instanceof A); 67 | }, 68 | }); 69 | -------------------------------------------------------------------------------- /test.ts: -------------------------------------------------------------------------------- 1 | import "https://cdn.skypack.dev/@abraham/reflection@0.8.0"; 2 | import { 3 | assert, 4 | assertThrows, 5 | } from "https://deno.land/std@0.101.0/testing/asserts.ts"; 6 | import { Inject, Service, ServiceCollection } from "./mod.ts"; 7 | 8 | function initServices( 9 | initFn: (services: ServiceCollection) => void, 10 | ): ServiceCollection { 11 | const services = new ServiceCollection(); 12 | initFn(services); 13 | return services; 14 | } 15 | 16 | function singleDirectClasses() { 17 | @Service() 18 | class A {} 19 | 20 | @Service() 21 | class B { 22 | constructor(public a: A) {} 23 | } 24 | 25 | return [A, B] as const; 26 | } 27 | 28 | Deno.test({ 29 | name: "transient direct classes", 30 | fn() { 31 | const [A, B] = singleDirectClasses(); 32 | 33 | const svs = initServices((s) => { 34 | s.addTransient(A); 35 | s.addTransient(B); 36 | }); 37 | 38 | const b1 = svs.get(B); 39 | assert(b1 instanceof B); 40 | assert(b1.a instanceof A); 41 | 42 | const b2 = svs.get(B); 43 | assert(b2 instanceof B); 44 | assert(b2.a instanceof A); 45 | assert(b1 !== b2); 46 | assert(b1.a !== b2.a); 47 | }, 48 | }); 49 | 50 | Deno.test({ 51 | name: "scoped direct classes", 52 | fn() { 53 | const [A, B] = singleDirectClasses(); 54 | 55 | const svs = initServices((s) => { 56 | s.addScoped(A); 57 | s.addScoped(B); 58 | }); 59 | 60 | const b1 = svs.get(B); 61 | assert(b1 instanceof B); 62 | assert(b1.a instanceof A); 63 | 64 | const b2 = svs.get(B); 65 | assert(b2 instanceof B); 66 | assert(b2.a instanceof A); 67 | assert(b1 !== b2); 68 | assert(b1.a !== b2.a); 69 | }, 70 | }); 71 | 72 | Deno.test({ 73 | name: "singleton direct classes", 74 | fn() { 75 | const [A, B] = singleDirectClasses(); 76 | 77 | const svs = initServices((s) => { 78 | s.addSingleton(A); 79 | s.addSingleton(B); 80 | }); 81 | 82 | const b1 = svs.get(B); 83 | assert(b1 instanceof B); 84 | assert(b1.a instanceof A); 85 | 86 | const b2 = svs.get(B); 87 | assert(b2 instanceof B); 88 | assert(b2.a instanceof A); 89 | assert(b1 === b2); 90 | assert(b1.a === b2.a); 91 | }, 92 | }); 93 | 94 | function multipleDirectClasses() { 95 | @Service() 96 | class A {} 97 | 98 | @Service() 99 | class B { 100 | constructor(public a1: A, public a2: A) {} 101 | } 102 | 103 | return [A, B] as const; 104 | } 105 | 106 | Deno.test({ 107 | name: "transient multiple direct", 108 | fn() { 109 | const [A, B] = multipleDirectClasses(); 110 | 111 | const svs = initServices((s) => { 112 | s.addTransient(A); 113 | s.addTransient(B); 114 | }); 115 | 116 | const b1 = svs.get(B); 117 | assert(b1 instanceof B); 118 | assert(b1.a1 instanceof A); 119 | assert(b1.a2 instanceof A); 120 | assert(b1.a1 !== b1.a2); 121 | 122 | const b2 = svs.get(B); 123 | assert(b2 instanceof B); 124 | assert(b2.a1 instanceof A); 125 | assert(b2.a2 instanceof A); 126 | assert(b2.a1 !== b2.a2); 127 | 128 | assert(b1 !== b2); 129 | }, 130 | }); 131 | 132 | Deno.test({ 133 | name: "scoped multiple direct", 134 | fn() { 135 | const [A, B] = multipleDirectClasses(); 136 | 137 | const svs = initServices((s) => { 138 | s.addScoped(A); 139 | s.addScoped(B); 140 | }); 141 | 142 | const b1 = svs.get(B); 143 | assert(b1 instanceof B); 144 | assert(b1.a1 instanceof A); 145 | assert(b1.a2 instanceof A); 146 | assert(b1.a1 === b1.a2); 147 | 148 | const b2 = svs.get(B); 149 | assert(b2 instanceof B); 150 | assert(b2.a1 instanceof A); 151 | assert(b2.a2 instanceof A); 152 | assert(b2.a1 === b2.a2); 153 | 154 | assert(b1 !== b2); 155 | }, 156 | }); 157 | 158 | Deno.test({ 159 | name: "singleton multiple direct", 160 | fn() { 161 | const [A, B] = multipleDirectClasses(); 162 | 163 | const svs = initServices((s) => { 164 | s.addSingleton(A); 165 | s.addSingleton(B); 166 | }); 167 | 168 | const b1 = svs.get(B); 169 | assert(b1 instanceof B); 170 | assert(b1.a1 instanceof A); 171 | assert(b1.a2 instanceof A); 172 | assert(b1.a1 === b1.a2); 173 | 174 | const b2 = svs.get(B); 175 | assert(b2 instanceof B); 176 | assert(b2.a1 instanceof A); 177 | assert(b2.a2 instanceof A); 178 | assert(b2.a1 === b2.a2); 179 | 180 | assert(b1 === b2); 181 | }, 182 | }); 183 | 184 | Deno.test({ 185 | name: "property injection", 186 | fn() { 187 | @Service() 188 | class A {} 189 | 190 | @Service() 191 | class B { 192 | @Inject() 193 | public a!: A; 194 | } 195 | 196 | const svs = initServices((s) => { 197 | s.addTransient(A); 198 | s.addTransient(B); 199 | }); 200 | 201 | const b1 = svs.get(B); 202 | assert(b1 instanceof B); 203 | assert(b1.a instanceof A); 204 | 205 | const b2 = svs.get(B); 206 | assert(b2 instanceof B); 207 | assert(b2.a instanceof A); 208 | 209 | assert(b1 !== b2); 210 | assert(b1.a !== b2.a); 211 | }, 212 | }); 213 | 214 | Deno.test({ 215 | name: "explict types functionality", 216 | fn() { 217 | const aName = "a"; 218 | const bSym = Symbol("b"); 219 | const cSym = Symbol("c"); 220 | 221 | interface IA { 222 | propA: string; 223 | } 224 | 225 | interface IB { 226 | propB: string; 227 | } 228 | 229 | interface IC { 230 | propC: string; 231 | } 232 | 233 | @Service() 234 | class A implements IA { 235 | public propA = "prop A"; 236 | } 237 | 238 | @Service() 239 | class B implements IB { 240 | public propB = "prop B"; 241 | } 242 | 243 | @Service() 244 | class C implements IC { 245 | @Inject(aName) 246 | public a!: A; 247 | 248 | public propC = "prop C"; 249 | 250 | constructor( 251 | @Inject(bSym) public b: B, 252 | ) {} 253 | } 254 | 255 | const svs = initServices((s) => { 256 | s.addTransient(aName, A); 257 | s.addTransient(bSym, B); 258 | s.addTransient(cSym, C); 259 | }); 260 | 261 | const c = svs.get(cSym); 262 | assert(c instanceof C); 263 | assert(c.propC === "prop C"); 264 | assert(c.a instanceof A); 265 | assert(c.a.propA === "prop A"); 266 | assert(c.b instanceof B); 267 | assert(c.b.propB === "prop B"); 268 | }, 269 | }); 270 | 271 | Deno.test({ 272 | name: "validation - double Service decorator", 273 | fn() { 274 | assertThrows( 275 | () => { 276 | @Service() 277 | @Service() 278 | class A {} 279 | }, 280 | undefined, 281 | "multiple times", 282 | ); 283 | }, 284 | }); 285 | 286 | Deno.test({ 287 | name: "validation - bad constructor type", 288 | fn() { 289 | assertThrows( 290 | () => { 291 | @Service() 292 | class A { 293 | constructor(@Inject() a: object) {} 294 | } 295 | }, 296 | undefined, 297 | "Cannot determine type of parameter", 298 | ); 299 | }, 300 | }); 301 | 302 | Deno.test({ 303 | name: "validation - bad property type", 304 | fn() { 305 | assertThrows( 306 | () => { 307 | @Service() 308 | class A { 309 | @Inject() 310 | public a!: object; 311 | } 312 | }, 313 | undefined, 314 | "Cannot determine type of property", 315 | ); 316 | }, 317 | }); 318 | 319 | Deno.test({ 320 | name: "validation - unknown service", 321 | fn() { 322 | assertThrows( 323 | () => { 324 | const svs = new ServiceCollection(); 325 | 326 | svs.get("unknown"); 327 | }, 328 | undefined, 329 | "does not exist in container", 330 | ); 331 | }, 332 | }); 333 | 334 | Deno.test({ 335 | name: "validation - missing dependency", 336 | fn() { 337 | assertThrows( 338 | () => { 339 | @Service() 340 | class A {} 341 | 342 | @Service() 343 | class B { 344 | constructor(public a: A) {} 345 | } 346 | 347 | const svs = initServices((s) => { 348 | s.addTransient(B); 349 | }); 350 | 351 | svs.get(B); 352 | }, 353 | undefined, 354 | "does not exist in container", 355 | ); 356 | }, 357 | }); 358 | 359 | Deno.test({ 360 | name: "validation - missing Service decorator", 361 | fn() { 362 | assertThrows( 363 | () => { 364 | class A {} 365 | 366 | initServices((s) => s.addTransient(A)); 367 | }, 368 | undefined, 369 | "has not been decorated with @Service", 370 | ); 371 | }, 372 | }); 373 | 374 | Deno.test({ 375 | name: "validation - cyclic dependency", 376 | fn() { 377 | assertThrows( 378 | () => { 379 | const types = { 380 | a: Symbol("a"), 381 | b: Symbol("b"), 382 | c: Symbol("c"), 383 | }; 384 | 385 | @Service() 386 | class A { 387 | constructor( 388 | @Inject(types.c) public c: any, 389 | ) {} 390 | } 391 | 392 | @Service() 393 | class B { 394 | constructor( 395 | @Inject(types.a) public a: A, 396 | ) {} 397 | } 398 | 399 | @Service() 400 | class C { 401 | constructor( 402 | @Inject(types.b) public b: B, 403 | ) {} 404 | } 405 | 406 | const svs = initServices((s) => { 407 | s.addTransient(types.a, A); 408 | s.addTransient(types.b, B); 409 | s.addTransient(types.c, C); 410 | }); 411 | 412 | svs.get(types.c); 413 | }, 414 | undefined, 415 | "Circular dependency detected", 416 | ); 417 | }, 418 | }); 419 | 420 | Deno.test({ 421 | name: "dynamic transient binding", 422 | fn() { 423 | let i = 0; 424 | const fn = () => i++; 425 | 426 | @Service() 427 | class A { 428 | constructor(@Inject("fn") public count: number) {} 429 | } 430 | 431 | const svs = initServices((s) => { 432 | s.addTransientDynamic("fn", fn); 433 | s.addTransient(A); 434 | }); 435 | 436 | const a1 = svs.get(A); 437 | assert(a1 instanceof A); 438 | assert(a1.count === 0); 439 | assert(i === 1); 440 | 441 | const a2 = svs.get(A); 442 | assert(a2 instanceof A); 443 | assert(a2.count === 1); 444 | assert(i === 2 as any); 445 | }, 446 | }); 447 | 448 | Deno.test({ 449 | name: "dynamic singleton binding", 450 | fn() { 451 | let i = 0; 452 | const fn = () => i++; 453 | 454 | @Service() 455 | class A { 456 | constructor(@Inject("fn") public count: number) {} 457 | } 458 | 459 | const svs = initServices((s) => { 460 | s.addSingletonDynamic("fn", fn); 461 | s.addTransient(A); 462 | }); 463 | 464 | const a1 = svs.get(A); 465 | assert(a1 instanceof A); 466 | assert(a1.count === 0); 467 | assert(i === 1); 468 | 469 | const a2 = svs.get(A); 470 | assert(a2 instanceof A); 471 | assert(a2.count === 0); 472 | assert(i === 1); 473 | }, 474 | }); 475 | 476 | Deno.test({ 477 | name: "static binding", 478 | fn() { 479 | const val = "test value"; 480 | 481 | @Service() 482 | class A { 483 | constructor(@Inject("val") public v: string) {} 484 | } 485 | 486 | const svs = initServices((s) => { 487 | s.addStatic("val", val); 488 | s.addTransient(A); 489 | }); 490 | 491 | const a = svs.get(A); 492 | assert(a instanceof A); 493 | assert(a.v === val); 494 | }, 495 | }); 496 | 497 | Deno.test({ 498 | name: "deep complex nested", 499 | fn() { 500 | // Interfaces 501 | abstract class IA {} 502 | abstract class IB {} 503 | abstract class IC {} 504 | abstract class ID {} 505 | abstract class IE {} 506 | 507 | // Impls 508 | @Service() // transient 509 | class A implements IA {} 510 | 511 | @Service() // singleton 512 | class B implements IB { 513 | constructor(public a: IA) {} 514 | } 515 | 516 | @Service() // scoped 517 | class C implements IC { 518 | constructor(public b: IB, public a: IA) {} 519 | } 520 | 521 | @Service() // transient 522 | class D implements ID { 523 | constructor(public c: IC, public b: IB) {} 524 | } 525 | 526 | @Service() // transient 527 | class E implements IE { 528 | constructor(public d1: ID, public d2: ID) {} 529 | } 530 | 531 | // Container setup 532 | const svs = initServices((s) => { 533 | s.addTransient(IA, A); 534 | s.addSingleton(IB, B); 535 | s.addScoped(IC, C); 536 | s.addTransient(ID, D); 537 | s.addTransient(IE, E); 538 | }); 539 | 540 | const e1 = svs.get(IE); 541 | assert(e1 instanceof E); 542 | assert(e1.d1 instanceof D); 543 | assert(e1.d1.b instanceof B); 544 | assert(e1.d1.b.a instanceof A); 545 | assert(e1.d1.c instanceof C); 546 | assert(e1.d1.c.a instanceof A); 547 | assert(e1.d1.c.b instanceof B); 548 | assert(e1.d1.c.b.a instanceof A); 549 | 550 | assert(e1.d2 instanceof D); 551 | assert(e1.d2.b instanceof B); 552 | assert(e1.d2.b.a instanceof A); 553 | assert(e1.d2.c instanceof C); 554 | assert(e1.d2.c.a instanceof A); 555 | assert(e1.d2.c.b instanceof B); 556 | assert(e1.d2.c.b.a instanceof A); 557 | 558 | assert(e1.d1 !== e1.d2); 559 | assert(e1.d1.b === e1.d2.b); 560 | assert(e1.d1.b.a === e1.d2.b.a); 561 | assert(e1.d1.c === e1.d2.c); 562 | assert(e1.d1.c.a === e1.d2.c.a); 563 | assert(e1.d1.c.b === e1.d2.c.b); 564 | assert(e1.d1.c.b === e1.d1.b); 565 | assert(e1.d1.c.b === e1.d2.b); 566 | assert(e1.d2.c.b === e1.d1.b); 567 | assert(e1.d2.c.b === e1.d2.b); 568 | assert(e1.d1.c.b.a === e1.d2.c.b.a); 569 | 570 | const e2 = svs.get(IE); 571 | assert(e2 instanceof E); 572 | assert(e2.d1 instanceof D); 573 | assert(e2.d1.b instanceof B); 574 | assert(e2.d1.b.a instanceof A); 575 | assert(e2.d1.c instanceof C); 576 | assert(e2.d1.c.a instanceof A); 577 | assert(e2.d1.c.b instanceof B); 578 | assert(e2.d1.c.b.a instanceof A); 579 | 580 | assert(e2.d2 instanceof D); 581 | assert(e2.d2.b instanceof B); 582 | assert(e2.d2.b.a instanceof A); 583 | assert(e2.d2.c instanceof C); 584 | assert(e2.d2.c.a instanceof A); 585 | assert(e2.d2.c.b instanceof B); 586 | assert(e2.d2.c.b.a instanceof A); 587 | 588 | assert(e2.d1 !== e2.d2); 589 | assert(e2.d1.b === e2.d2.b); 590 | assert(e2.d1.b.a === e2.d2.b.a); 591 | assert(e2.d1.c === e2.d2.c); 592 | assert(e2.d1.c.a === e2.d2.c.a); 593 | assert(e2.d1.c.b === e2.d2.c.b); 594 | assert(e2.d1.c.b === e2.d1.b); 595 | assert(e2.d1.c.b === e2.d2.b); 596 | assert(e2.d2.c.b === e2.d1.b); 597 | assert(e2.d2.c.b === e2.d2.b); 598 | assert(e2.d1.c.b.a === e2.d2.c.b.a); 599 | 600 | assert(e1 !== e2); 601 | assert(e1.d1 !== e2.d1); 602 | assert(e1.d2 !== e2.d2); 603 | assert(e1.d1 !== e2.d2); 604 | assert(e1.d1.b === e2.d1.b); 605 | assert(e1.d1.c !== e2.d1.c); 606 | assert(e1.d1.c.a !== e2.d1.c.a); 607 | assert(e1.d1.c.b === e2.d1.c.b); 608 | }, 609 | }); 610 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "emitDecoratorMetadata": true 5 | } 6 | } 7 | --------------------------------------------------------------------------------