@resolid/di 0.5.0 → 0.6.0

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 CHANGED
@@ -8,17 +8,19 @@
8
8
  ## TypeScript Dependency Injection Container
9
9
 
10
10
  A lightweight, fully-typed Dependency Injection (DI) container for TypeScript.
11
- Supports singleton & transient scopes, lazy resolution, and disposable resources. Fully functional with async factories
11
+ Supports singleton & transient scopes, lazy resolution, optional dependencies, and disposable resources. Fully functional with async factories
12
12
  and avoids circular dependency issues.
13
13
 
14
14
  ### Features
15
15
 
16
16
  - Fully typed with TypeScript, no any.
17
17
  - Supports singleton and transient scopes.
18
- - Lazy resolution for async dependencies.
18
+ - Lazy resolution for dependencies.
19
+ - Optional dependency resolution.
19
20
  - Detects circular dependencies.
20
21
  - Handles disposable instances with dispose() support.
21
- - Fully async-compatible.
22
+ - Fully async-compatible with inject() and injectAsync() functions.
23
+ - Context-aware injection with InjectionContext.
22
24
 
23
25
  ### Installation
24
26
 
@@ -34,123 +36,233 @@ bun add @resolid/di
34
36
 
35
37
  ### Basic Usage
36
38
 
37
- ```js
38
- import { createContainer } from "@resolid/di";
39
+ ```typescript
40
+ import { Container, inject } from "@resolid/di";
39
41
 
40
- const VALUE = "VALUE";
41
- const FUNCTION = "FUNCTION";
42
- const FACTORY = "FACTORY";
42
+ class LogService {
43
+ log(message: string): void {
44
+ console.log(`[LOG]: ${message}`);
45
+ }
46
+ }
43
47
 
44
- const container = createContainer([
45
- { name: VALUE, value: "Hello World" },
46
- {
47
- name: FUNCTION,
48
- callable: () => {
49
- return "Hello World from callable";
50
- },
51
- },
52
- {
53
- name: FACTORY,
54
- factory: async ({ resolver, config }) => {
55
- const value = await resolver.resolve(VALUE);
56
-
57
- return value + " " + config.message || "from factory!";
58
- },
59
- /**
60
- * Scope of a binding.
61
- * singleton (default): Only one instance is created and shared for all resolves.
62
- * transient: A new instance is created on each resolve.
63
- */
64
- scope: "singleton",
65
- /**
66
- * Optional configuration object passed to a factory.
67
- * Type is inferred from the factory definition.
68
- * Used to provide parameters for instance creation.
69
- */
70
- config: { message: " from factory with config" },
71
- },
72
- ]);
48
+ class UserService {
49
+ private logger: LogService;
50
+
51
+ constructor(logger: LogService) {
52
+ this.logger = logger;
53
+ }
54
+
55
+ createUser(name: string): void {
56
+ this.logger.log(`Creating user: ${name}`);
57
+ }
58
+ }
59
+
60
+ const container = new Container();
61
+
62
+ container.add({
63
+ token: LogService,
64
+ factory: () => new LogService(),
65
+ });
66
+
67
+ container.add({
68
+ token: UserService,
69
+ factory: () => new UserService(inject(LogService)),
70
+ });
71
+
72
+ const userService = container.get(USER_SERVICE);
73
+
74
+ userService.createUser("John Doe"); // Output: [LOG]: Creating user: John Doe
75
+ ```
73
76
 
74
- const valueResult = await container.resolve(VALUE);
75
- console.log(valueResult); // Output: "Hello World"
77
+ ### Scopes
76
78
 
77
- const callable = await container.resolve(FUNCTION);
78
- const callableResult = callable();
79
- console.log(callableResult); // Output: "Hello World from callable"
79
+ The container supports two scopes:
80
80
 
81
- const factoryResult = await container.resolve(FACTORY);
82
- console.log(factoryResult); // Output: "Hello World from factory with config"
81
+ - **singleton** (default): Only one instance is created and shared for all resolutions.
82
+ - **transient**: A new instance is created on each resolution.
83
+
84
+ ```typescript
85
+ container.add({
86
+ token: LogService,
87
+ factory: () => new LogService(),
88
+ scope: "singleton", // Default scope
89
+ });
90
+
91
+ container.add({
92
+ token: Symbol("REQUEST_ID"),
93
+ factory: () => Math.random(),
94
+ scope: "transient", // New instance each time
95
+ });
83
96
  ```
84
97
 
85
- ### Lazy Resolve
98
+ ### Asynchronous Dependencies
99
+
100
+ ```typescript
101
+ import { Container, injectAsync } from "@resolid/di";
102
+
103
+ class AsyncDatabaseService {
104
+ async connect(): Promise<void> {
105
+ await new Promise(resolve => setTimeout(resolve, 100));
106
+ console.log("Database connected");
107
+ }
108
+ }
86
109
 
87
- You can create bindings that are resolved lazily, useful for circular dependencies or heavy computations:
110
+ class AsyncUserService {
111
+ private db : AsyncDatabaseService
88
112
 
89
- ```js
90
- const LAZY_A = "LAZY_A";
91
- const LAZY_B = "LAZY_B";
113
+ constructor(db: AsyncDatabaseService) {
114
+ this.db = db;
115
+ }
92
116
 
93
- const container = createContainer([
94
- {
95
- name: LAZY_A,
96
- factory: ({ resolver }) => {
97
- const b = resolver.lazyResolve(LAZY_B);
98
- return `A depends on ${b.value}`;
99
- },
117
+ async getUser(id: number): Promise<string> {
118
+ return `User ${id}`;
119
+ }
120
+ }
121
+
122
+ const container = new Container();
123
+
124
+ container.add({
125
+ token: AsyncDatabaseService,
126
+ factory: async () => {
127
+ const db = new AsyncDatabaseService();
128
+ await db.connect();
129
+ return db;
100
130
  },
101
- {
102
- name: LAZY_B,
103
- factory: ({ resolver }) => {
104
- const a = resolver.lazyResolve(LAZY_A);
105
- return `B depends on ${a.value}`;
106
- },
131
+ async: true,
132
+ });
133
+
134
+ container.add({
135
+ token: AsyncUserService,
136
+ factory: async () => {
137
+ const db = await injectAsync(AsyncDatabaseService);
138
+ return new AsyncUserService(db)
107
139
  },
108
- ]);
140
+ async: true,
141
+ });
142
+
143
+ // Resolve async services
144
+ const userService = await container.getAsync(AsyncUserService);
145
+ const user = await userService.getUser(1);
146
+
147
+ console.log(user); // Output: User 1
148
+ ```
149
+
150
+ ### Optional Dependencies
109
151
 
110
- // Accessing value after construction is safe
111
- await container.resolve(LAZY_A);
112
- await container.resolve(LAZY_B);
152
+ ```typescript
153
+ import { Container, inject } from "@resolid/di";
154
+
155
+ class AnalyticsService {
156
+ private logger?: LogService
157
+
158
+ constructor(logger?: LogService) {
159
+ this.logger = logger;
160
+ }
161
+
162
+ track(event: string): void {
163
+ if (this.logger) {
164
+ this.logger.log(`Analytics: ${event}`);
165
+ } else {
166
+ console.log(`Analytics (no logger): ${event}`);
167
+ }
168
+ }
169
+ }
170
+
171
+ const container = new Container();
172
+
173
+ // Logger is optional
174
+ container.add({
175
+ token: AnalyticsService,
176
+ factory: () => new AnalyticsService(inject(LOG_SERVICE, { optional: true })),
177
+ });
178
+
179
+ const analytics = container.get(AnalyticsService);
180
+ analytics.track("page_view"); // Output: Analytics (no logger): page_view
113
181
  ```
114
182
 
115
- > Note: Access `lazyResolve.value` **only after all bindings are constructed**, otherwise it will throw an error.
183
+ ### Lazy Resolution
184
+
185
+ ```typescript
186
+ import { Container, inject } from "@resolid/di";
187
+
188
+ class ReportService {
189
+ private getLogger : () => LogService;
190
+
191
+ constructor(private getLogger: () => LogService) {
192
+ this.getLogger = getLogger;
193
+ }
194
+
195
+ generateReport(): void {
196
+ const logger = this.getLogger();
197
+ logger.log("Report generated");
198
+ }
199
+ }
200
+
201
+ const container = new Container();
202
+
203
+ container.add({
204
+ token: LogService,
205
+ factory: () => new LogService(),
206
+ });
207
+
208
+ container.add({
209
+ token: ReportService,
210
+ factory: () => new ReportService(inject(LogService, { lazy: true })),
211
+ });
212
+
213
+ const reportService = container.get(ReportService);
214
+ reportService.generateReport(); // Output: [LOG]: Report generated
215
+ ```
116
216
 
117
217
  ### Circular Dependency Detection
118
218
 
119
- ```js
120
- const A = "A";
121
- const B = "B";
219
+ ```typescript
220
+ class ApiService {
221
+ constructor(private auth: AuthService) {}
222
+ }
122
223
 
123
- const container = createContainer([
124
- {
125
- name: A,
126
- factory: ({ resolver }) => resolver.resolve(B),
127
- },
128
- {
129
- name: B,
130
- factory: ({ resolver }) => resolver.resolve(A),
131
- },
132
- ]);
224
+ class AuthService {
225
+ constructor(private api: ApiService) {}
226
+ }
227
+
228
+ const container = new Container();
229
+
230
+ container.add({
231
+ token: ApiService,
232
+ factory: () => new ApiService(inject(AuthService)),
233
+ });
133
234
 
134
- await container.resolve(A);
135
- // Throws Error: Circular dependency detected: A -> B -> A
235
+ container.add({
236
+ token: AuthService,
237
+ factory: () => new AuthService(inject(ApiService)),
238
+ });
239
+
240
+ container.get(ApiService); // Throws: Circular dependency detected ApiService -> AuthService -> ApiService
136
241
  ```
137
242
 
138
- ### Dispose
243
+ ### Disposable Resources
244
+
245
+ ```typescript
246
+ import { Container } from "@resolid/di";
139
247
 
140
- ```js
141
- class Resource {
142
- async dispose() {
143
- console.log("Resource disposed");
248
+ class DatabaseConnection {
249
+ async dispose(): Promise<void> {
250
+ console.log("Database connection closed");
144
251
  }
145
252
  }
146
253
 
147
- const RESOURCE = "RESOURCE";
254
+ const container = new Container();
255
+
256
+ container.add({
257
+ token: DatabaseConnection,
258
+ factory: async () => new DatabaseConnection(),
259
+ async: true,
260
+ });
148
261
 
149
- const container = createContainer([{ name: RESOURCE, value: new Resource() }]);
262
+ const db = await container.getAsync(DatabaseConnection);
150
263
 
151
264
  // Dispose all singleton instances
152
- await container.dispose();
153
- // Output: "Resource disposed"
265
+ await container.dispose(); // Output: Database connection closed
154
266
  ```
155
267
 
156
268
  ## License
package/dist/index.d.ts CHANGED
@@ -1,45 +1,119 @@
1
- //#region src/index.d.ts
2
- type Resolve = <T>(name: string) => Promise<T>;
3
- type LazyResolve = <T = unknown>(name: string) => Readonly<{
4
- value: T;
5
- }>;
1
+ //#region src/shared/index.d.ts
2
+ type Class<T> = new (...args: any[]) => T;
3
+ type Token<T = unknown> = symbol | Class<T>;
4
+ interface Disposable {
5
+ dispose: () => Promise<void> | void;
6
+ }
7
+ interface Resolver {
8
+ get: (<T>(token: Token<T>) => T) & (<T>(token: Token<T>, options: {
9
+ optional: true;
10
+ }) => T | undefined) & (<T>(token: Token<T>, options: {
11
+ lazy: true;
12
+ }) => () => T) & (<T>(token: Token<T>, options: {
13
+ lazy: true;
14
+ optional: true;
15
+ }) => () => T | undefined) & (<T>(token: Token<T>, options: {
16
+ lazy?: false;
17
+ optional?: boolean;
18
+ }) => T | undefined) & (<T>(token: Token<T>, options?: {
19
+ lazy?: boolean;
20
+ optional?: boolean;
21
+ }) => T | undefined | (() => T | undefined));
22
+ getAsync: (<T>(token: Token<T>) => Promise<T>) & (<T>(token: Token<T>, options: {
23
+ optional: true;
24
+ }) => Promise<T | undefined>) & (<T>(token: Token<T>, options: {
25
+ lazy: true;
26
+ }) => () => Promise<T>) & (<T>(token: Token<T>, options: {
27
+ lazy: true;
28
+ optional: true;
29
+ }) => () => Promise<T | undefined>) & (<T>(token: Token<T>, options: {
30
+ lazy?: false;
31
+ optional?: boolean;
32
+ }) => Promise<T | undefined>) & (<T>(token: Token<T>, options?: {
33
+ lazy?: boolean;
34
+ optional?: boolean;
35
+ }) => Promise<T | undefined> | (() => Promise<T | undefined>));
36
+ }
37
+ //#endregion
38
+ //#region src/container/index.d.ts
6
39
  type Scope = "singleton" | "transient";
7
- type Resolver = {
8
- resolve: Resolve;
9
- lazyResolve: LazyResolve;
10
- };
11
- type FactoryConfig = Record<string, unknown>;
12
- type BindingDefinition<T$1 = unknown, C extends FactoryConfig = FactoryConfig> = {
13
- name: string;
14
- value: T$1;
15
- callable?: never;
16
- factory?: never;
17
- scope?: never;
18
- config?: never;
19
- } | {
20
- name: string;
21
- callable: (...args: unknown[]) => T$1;
22
- value?: never;
23
- factory?: never;
24
- scope?: never;
25
- config?: never;
26
- } | {
27
- name: string;
28
- factory: (options: {
29
- resolver: Resolver;
30
- config?: C;
31
- }) => T$1 | Promise<T$1>;
40
+ type SyncProvider<T = unknown> = {
41
+ token: Token<T>;
42
+ factory: (resolver: Resolver) => T;
43
+ async?: false;
32
44
  scope?: Scope;
33
- config?: C;
34
- value?: never;
35
- callable?: never;
36
45
  };
37
- type Disposable = {
38
- dispose: () => Promise<void> | void;
46
+ type AsyncProvider<T = unknown> = {
47
+ token: Token<T>;
48
+ factory: (resolver: Resolver) => Promise<T>;
49
+ async?: true;
50
+ scope?: Scope;
39
51
  };
40
- type Container = {
41
- resolve: Resolve;
42
- } & Disposable;
43
- declare const createContainer: (definitions: readonly BindingDefinition[]) => Container;
52
+ type Provider<T = unknown> = SyncProvider<T> | AsyncProvider<T>;
53
+ declare class Container implements Resolver, Disposable {
54
+ private readonly _providers;
55
+ private readonly _singletons;
56
+ private readonly _constructing;
57
+ private _checkCircularDependency;
58
+ private _resolveProvide;
59
+ private _resolve;
60
+ private _resolveAsync;
61
+ add(provider: Provider): void;
62
+ get<T>(token: Token<T>): T;
63
+ get<T>(token: Token<T>, options: {
64
+ optional: true;
65
+ }): T | undefined;
66
+ get<T>(token: Token<T>, options: {
67
+ lazy: true;
68
+ }): () => T;
69
+ get<T>(token: Token<T>, options: {
70
+ lazy: true;
71
+ optional: true;
72
+ }): () => T | undefined;
73
+ get<T>(token: Token<T>, options: {
74
+ lazy?: false;
75
+ optional?: boolean;
76
+ }): T | undefined;
77
+ getAsync<T>(token: Token<T>): Promise<T>;
78
+ getAsync<T>(token: Token<T>, options: {
79
+ optional: true;
80
+ }): Promise<T | undefined>;
81
+ getAsync<T>(token: Token<T>, options: {
82
+ lazy: true;
83
+ }): () => Promise<T>;
84
+ getAsync<T>(token: Token<T>, options: {
85
+ lazy: true;
86
+ optional: true;
87
+ }): () => Promise<T | undefined>;
88
+ getAsync<T>(token: Token<T>, options: {
89
+ lazy?: false;
90
+ optional?: boolean;
91
+ }): Promise<T | undefined>;
92
+ dispose(): Promise<void>;
93
+ }
94
+ //#endregion
95
+ //#region src/inject/index.d.ts
96
+ declare function inject<T>(token: Token<T>): T;
97
+ declare function inject<T>(token: Token<T>, options: {
98
+ optional: true;
99
+ }): T | undefined;
100
+ declare function inject<T>(token: Token<T>, options: {
101
+ lazy: true;
102
+ }): () => T;
103
+ declare function inject<T>(token: Token<T>, options: {
104
+ lazy: true;
105
+ optional: true;
106
+ }): () => T | undefined;
107
+ declare function injectAsync<T>(token: Token<T>): Promise<T>;
108
+ declare function injectAsync<T>(token: Token<T>, options: {
109
+ optional: true;
110
+ }): Promise<T | undefined>;
111
+ declare function injectAsync<T>(token: Token<T>, options: {
112
+ lazy: true;
113
+ }): () => Promise<T>;
114
+ declare function injectAsync<T>(token: Token<T>, options: {
115
+ lazy: true;
116
+ optional: true;
117
+ }): () => Promise<T | undefined>;
44
118
  //#endregion
45
- export { BindingDefinition, Container, FactoryConfig, Resolver, Scope, createContainer };
119
+ export { Container, Provider, type Resolver, Scope, type Token, inject, injectAsync };
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- const e=process.env.NODE_ENV===`development`,t=e=>{let t=n(`__R_DI`,()=>{let t=new r;return t.register(e),t});return{resolve:e=>t.resolve(e),dispose:()=>t.dispose()}},n=(t,n)=>{if(!e)return n();let r=globalThis;if(t in r)return r[t];let i=n();return r[t]=i,i};var r=class e{_bindings;_singletons;_constructing;_lazyResolveQueue=[];constructor(e,t,n){this._bindings=e?._bindings??new Map,this._singletons=e?._singletons??new Map,this._constructing=t?[...n??e?._constructing??[],t]:[]}register(e){for(let t of e){let e=t.callable?()=>t.callable:t.factory?(e,n)=>t.factory({resolver:{resolve:e,lazyResolve:n},config:t.config}):()=>t.value;this._bindings.set(t.name,{factory:e,scope:t.scope??`singleton`,config:t.config})}}async _resolveBinding(t,n){let r=this._bindings.get(t);if(!r)throw Error(`No binding found for ${t}.`);let i=r.scope===`singleton`;if(i&&this._singletons.has(t))return this._singletons.get(t);let a=new e(this,t,n),o=r.factory(e=>a.resolve(e),e=>a.lazyResolve(e)),s=o instanceof Promise?await o:o;return i&&this._singletons.set(t,s),await a.dequeueLazyResolves(),s}async dequeueLazyResolves(){for(let e of this._lazyResolveQueue)try{e.resolve(await this._resolveBinding(e.name,[]))}catch(t){throw Error(`Failed to resolve lazy binding ${e.name}: ${t instanceof Error?t.message:String(t)}`)}}async resolve(e){if(this._constructing.includes(e))throw Error(`Circular dependency detected: ${[...this._constructing,e].join(` -> `)}`);return this._resolveBinding(e)}lazyResolve(e){let t={resolved:!1};return this._lazyResolveQueue.push({name:e,resolve:e=>{t.value=e,t.resolved=!0}}),{get value(){if(!t.resolved)throw Error(`Lazy binding is not yet resolved. Avoid accessing it before container construction finishes.`);return t.value}}}async dispose(){let e=0,t=``;for(let[n,r]of this._singletons)if(typeof r.dispose==`function`)try{await r.dispose()}catch(r){e++,t+=`${n}: ${r instanceof Error?r.message:r}; `}if(this._singletons.clear(),e>0)throw Error(`Failed to dispose ${e} binding(s):\n${t.slice(0,-2)}`)}};export{t as createContainer};
1
+ var e=class extends Error{constructor(){super(`inject() / injectAsync() must be called within a injection context`)}},t=class t{static contextStack=[];resolver;constructor(e){this.resolver=e}static current(){let t=this.contextStack[this.contextStack.length-1];if(t===void 0)throw new e;return t}run(e){t.contextStack.push(this);try{return e(this.resolver)}finally{t.contextStack.pop()}}async runAsync(e){t.contextStack.push(this);try{return await e(this.resolver)}finally{t.contextStack.pop()}}};function n(e){return typeof e==`symbol`?e.description??String(e):e.name}var r=class{_providers=new Map;_singletons=new Map;_constructing=[];_checkCircularDependency(e){if(this._constructing.includes(e))throw Error(`Circular dependency detected ${[...this._constructing,e].map(n).join(` -> `)}`)}_resolveProvide(e,t){let r=this._providers.get(e);if(r===void 0&&!t)throw Error(`No provider found for ${n(e)}`);return r}_resolve(e,r){this._checkCircularDependency(e);let i=this._resolveProvide(e,r);if(i===void 0)return;if(i.async)throw Error(`Provider for ${n(e)} is async, please use injectAsync()`);let a=i.scope!==`transient`;if(a&&this._singletons.has(e))return this._singletons.get(e);this._constructing.push(e);try{let n=new t(this).run(()=>i.factory(this));return a&&this._singletons.set(e,n),n}finally{this._constructing.pop()}}async _resolveAsync(e,n){this._checkCircularDependency(e);let r=this._resolveProvide(e,n);if(r===void 0)return;let i=r.scope!==`transient`;if(i&&this._singletons.has(e))return this._singletons.get(e);this._constructing.push(e);try{let n=await new t(this).runAsync(async()=>await r.factory(this));return i&&this._singletons.set(e,n),n}finally{this._constructing.pop()}}add(e){this._providers.set(e.token,e)}get(e,t){return t?.lazy??!1?()=>this.get(e,{...t,lazy:!1}):this._resolve(e,t?.optional??!1)}getAsync(e,t){return t?.lazy??!1?()=>this.getAsync(e,{...t,lazy:!1}):this._resolveAsync(e,t?.optional??!1)}async dispose(){let e=0,t=``;for(let[r,i]of this._singletons)if(typeof i.dispose==`function`)try{await i.dispose()}catch(i){e++,t+=`${n(r)}: ${i instanceof Error?i.message:i}; `}if(this._singletons.clear(),e>0)throw Error(`Failed to dispose ${e} provider(s):\n${t.slice(0,-2)}`)}};function i(n,r){try{return t.current().run(e=>e.get(n,r))}catch(t){if(t instanceof e&&r?.optional==1)return;throw t}}function a(n,r){try{return r?.lazy?t.current().run(e=>e.getAsync(n,{...r,lazy:!0})):t.current().runAsync(e=>e.getAsync(n,{...r,lazy:!1}))}catch(t){return t instanceof e&&r?.optional===!0?r?.lazy?()=>Promise.resolve(void 0):Promise.resolve(void 0):Promise.reject(t)}}export{r as Container,i as inject,a as injectAsync};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@resolid/di",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "private": false,
5
5
  "description": "The Resolid DI Container package.",
6
6
  "keywords": [
@@ -36,10 +36,10 @@
36
36
  "dist"
37
37
  ],
38
38
  "devDependencies": {
39
- "@vitest/coverage-v8": "beta",
40
- "tsdown": "^0.15.8",
39
+ "@vitest/coverage-v8": "^4.0.3",
40
+ "tsdown": "^0.15.10",
41
41
  "typescript": "^5.9.3",
42
- "vitest": "beta"
42
+ "vitest": "^4.0.3"
43
43
  },
44
44
  "engines": {
45
45
  "node": "^20.19.0 || ^22.13.0 || >=24"