mesh-ioc 2.0.1 → 2.1.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 +187 -10
- package/out/main/mesh.d.ts +2 -2
- package/out/main/mesh.js +4 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Powerful and lightweight alternative to Dependency Injection (DI) solutions like [Inversify](https://inversify.io/).
|
|
4
4
|
|
|
5
|
-
Mesh IoC solves the problem of dependency management of application services. It wires
|
|
5
|
+
Mesh IoC solves the problem of dependency management of application services. It wires application services togethert (i.e. singletons instantiated once and scoped to an entire application) and contextual services (e.g. services scoped to a particular HTTP request, WebSocket connection, etc.)
|
|
6
6
|
|
|
7
7
|
## Key features
|
|
8
8
|
|
|
@@ -13,7 +13,7 @@ Mesh IoC solves the problem of dependency management of application services. It
|
|
|
13
13
|
- 🐓 🥚 Tolerates circular dependencies
|
|
14
14
|
- 🕵️♀️ Provides APIs for dependency analysis
|
|
15
15
|
|
|
16
|
-
##
|
|
16
|
+
## API Cheatsheet
|
|
17
17
|
|
|
18
18
|
```ts
|
|
19
19
|
// Mesh is an IoC container that stores service bindings and instantiated objects
|
|
@@ -142,16 +142,20 @@ class Redis {
|
|
|
142
142
|
|
|
143
143
|
// app.ts
|
|
144
144
|
|
|
145
|
-
class
|
|
146
|
-
//
|
|
147
|
-
|
|
148
|
-
|
|
145
|
+
class App {
|
|
146
|
+
mesh = new Mesh('App'); // Optional string identifier helps with debugging resolution problems
|
|
147
|
+
|
|
148
|
+
constructor() {
|
|
149
|
+
// List all services so that mesh connects them together
|
|
150
|
+
this.mesh.service(Redis);
|
|
151
|
+
this.mesh.service(Logger, ConsoleLogger);
|
|
152
|
+
}
|
|
149
153
|
}
|
|
150
154
|
```
|
|
151
155
|
|
|
152
156
|
There are several aspects that differentiate Mesh IoC from the rest of the DI libraries.
|
|
153
157
|
|
|
154
|
-
-
|
|
158
|
+
- Service instances are cached in mesh. If you only have a single mesh instance, the services will effectively be singletons. However, if multiple mesh instances are created, then each mesh will track its own service instances.
|
|
155
159
|
|
|
156
160
|
- If you're familiar with Inversify, this effectively makes all bindings `inSingletonScope`.
|
|
157
161
|
|
|
@@ -167,11 +171,176 @@ There are several aspects that differentiate Mesh IoC from the rest of the DI li
|
|
|
167
171
|
|
|
168
172
|
- Mesh can handle circular dependencies, all thanks to on-demand resolution. However, due to how Node.js module loading works, `@dep` will be unable to infer _one_ of the service keys (depending on which module happened to load first). It's a good practice to use explicit service keys on both sides of circular dependencies.
|
|
169
173
|
|
|
170
|
-
- Constant values can be bound to mesh. Those could be instances of other classes.
|
|
174
|
+
- Constant values can be bound to mesh. Those could be instances of other classes or just arbitrary values bound by string keys.
|
|
175
|
+
|
|
176
|
+
**Important!** Mesh should be used to track _services_. We define services as classes with **zero-argument constructors** (this is also enforced by TypeScript). However, there are multiple patterns to support constructor arguments, read on!
|
|
177
|
+
|
|
178
|
+
## Application Architecture Guide
|
|
179
|
+
|
|
180
|
+
This short guide briefly explains the basic concepts of a good application architecture where the components are loosely coupled, dependencies are easy to reason about and are not mixed with the actual data arguments.
|
|
181
|
+
|
|
182
|
+
### Scopes
|
|
183
|
+
|
|
184
|
+
Oftentimes different application components have different lifespans or scopes.
|
|
185
|
+
|
|
186
|
+
For example:
|
|
187
|
+
|
|
188
|
+
- **Application scope**: things like database connection pools, servers and other global components are scoped to entire application; their instances are effectively singletons (i.e. you don't want to establish a new database connection each time you query it).
|
|
189
|
+
|
|
190
|
+
- **Request/session scope**: things like traditional HTTP routers will depend on request and response objects; the same reasoning can be applied to other scenarios, for example, web socket server may need functionality per each connected client — such components will depend on client socket.
|
|
191
|
+
|
|
192
|
+
- **Short-lived per-instance scope**: if you use "fat" classes (e.g. Active Record pattern) then each entity instances should be conceptually "connected" to the rest of the application (e.g. `instance.save()` should somehow know about the database).
|
|
193
|
+
|
|
194
|
+
In the following sections we'll explore the best practices to organise the application components and to achieve a clear demarcation of scopes.
|
|
195
|
+
|
|
196
|
+
### Composition Root and Entrypoint
|
|
197
|
+
|
|
198
|
+
The term "app" is often very ambiguous when it comes to module organisation. For example, most http backend frameworks consider an "app" to be component responsible for routing and listening to HTTP requests, whereas most frontend frameworks would consider an "app" to be the root component with some setup.
|
|
199
|
+
|
|
200
|
+
It is also a common practice to mix the "app" with the actual entrypoint, i.e. the app module would be an equivalent of the "main" function that gets executed as soon as the script is parsed. This later results in problems with unit testing (i.e. the test runtime cannot use the "app" part separately from the "entrypoint" which typically results in the decisions like "unit test applications by sending HTTP requests to them").
|
|
201
|
+
|
|
202
|
+
With IoC the "app" term gets a well-defined meaning: `App` is a composition root, a "centralized registry" of application-scoped components.
|
|
203
|
+
|
|
204
|
+
For application scope one must make sure that only a single `mesh` instance is maintained throughout a lifecycle:
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
// src/main/app.ts
|
|
208
|
+
export class App {
|
|
209
|
+
mesh = new Mesh('App');
|
|
210
|
+
|
|
211
|
+
constuctor() {
|
|
212
|
+
// Add application-scoped (singleton) bindings, e.g.
|
|
213
|
+
this.mesh.service(Logger, GlobalLogger);
|
|
214
|
+
this.mesh.service(MyDatabase);
|
|
215
|
+
this.mesh.service(MyHttpServer);
|
|
216
|
+
// ...
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async start() {
|
|
220
|
+
// Define logic for application startup, e.g.
|
|
221
|
+
await this.mesh.resolve(MyDatabase).connect();
|
|
222
|
+
await this.mesh.resolve(MyHttpServer).listen();
|
|
223
|
+
// ...
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async stop() {
|
|
227
|
+
// Define logic for application shutdown, e.g.
|
|
228
|
+
await this.mesh.resolve(MyHttpServer).close();
|
|
229
|
+
await this.mesh.resolve(MyDatabase).close();
|
|
230
|
+
// ...
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
The entrypoint would be an actual "main" function that instantiates the `App` class and calls its `start` method:
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
// src/bin/serve.ts
|
|
240
|
+
import { App } from '../main/app.js';
|
|
241
|
+
|
|
242
|
+
const app = new App();
|
|
243
|
+
app.start()
|
|
244
|
+
.catch(err => {
|
|
245
|
+
// Setup app initialization error handling
|
|
246
|
+
process.exit(1);
|
|
247
|
+
});
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Test runtime setup
|
|
251
|
+
|
|
252
|
+
Test runtime can create a fresh `App` instance on every test case, e.g.:
|
|
253
|
+
|
|
254
|
+
```ts
|
|
255
|
+
// src/test/runtime.ts
|
|
256
|
+
import { App } from '../main/app.js';
|
|
257
|
+
|
|
258
|
+
let app: TestApp;
|
|
259
|
+
|
|
260
|
+
beforeEach(() => {
|
|
261
|
+
app = new TestApp();
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
export class TestApp extends App {
|
|
265
|
+
|
|
266
|
+
constructor() {
|
|
267
|
+
super();
|
|
268
|
+
// Setup test application
|
|
269
|
+
// e.g. one can substitute real components with mocks
|
|
270
|
+
this.mesh.service(ThirdPartyServiceMock);
|
|
271
|
+
this.mesh.alias(ThirdPartyService, ThirdPartyServiceMock);
|
|
272
|
+
// ...
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Each test would then be able to access `app` and re-bind any of the dependencies without affecting other test cases.
|
|
278
|
+
|
|
279
|
+
### Creating request/response scopes
|
|
280
|
+
|
|
281
|
+
Mesh IoC opts for maximum simplicity by following a straighforward model: each mesh instance is a scope.
|
|
282
|
+
|
|
283
|
+
Therefore, to create a scope, say, per HTTP request we need to simply create a mesh instance inside the HTTP handler. It's also a good idea to keep the logic of creating HTTP-scoped mesh inside the composition root, so that it can be overridden in tests.
|
|
171
284
|
|
|
172
|
-
|
|
285
|
+
```ts
|
|
286
|
+
// src/main/app.ts
|
|
287
|
+
|
|
288
|
+
export class App {
|
|
289
|
+
mesh = new Mesh('App');
|
|
290
|
+
|
|
291
|
+
constructor() {
|
|
292
|
+
// ...
|
|
293
|
+
this.mesh.service(MyServer);
|
|
294
|
+
// Bind the function that creates a scoped mesh, so that the server could use it.
|
|
295
|
+
this.mesh.constant('createRequestScope', (req: Request, res: Response) => this.createRequestScope());
|
|
296
|
+
}
|
|
173
297
|
|
|
174
|
-
|
|
298
|
+
protected createRequestScope(req: Request, res: Response) {
|
|
299
|
+
const mesh = new Mesh('Request');
|
|
300
|
+
// Allow request-scoped classes to also resolve app-scoped dependencies
|
|
301
|
+
mesh.parent = this.mesh;
|
|
302
|
+
// Scoped variables (req, res) can be bound as constants
|
|
303
|
+
mesh.constant(Request, req);
|
|
304
|
+
mesh.constant(Response, res);
|
|
305
|
+
// Create request-scoped bindings
|
|
306
|
+
mesh.service(Router, MyRouter);
|
|
307
|
+
// ...
|
|
308
|
+
return mesh;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Then in `MyServer`:
|
|
315
|
+
|
|
316
|
+
```ts
|
|
317
|
+
export class MyServer {
|
|
318
|
+
|
|
319
|
+
@dep({ key: 'createRequestScope' }) createRequestScope!: (req: Request, res: Response) => Mesh;
|
|
320
|
+
|
|
321
|
+
handleRequest(req: Request, res: Response) {
|
|
322
|
+
// This is an "entrypoint" of request scope
|
|
323
|
+
const mesh = this.createRequestScope(req, res);
|
|
324
|
+
// For example, let's delegate request handling to the Router
|
|
325
|
+
const router = mesh.resolve(Router);
|
|
326
|
+
router.handle();
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
In our example, `Router` is a request-scoped class and thus can access `req` and `res`:
|
|
332
|
+
|
|
333
|
+
```ts
|
|
334
|
+
export class MyRouter extends Router {
|
|
335
|
+
|
|
336
|
+
@dep() req!: Request;
|
|
337
|
+
@dep() res!: Response;
|
|
338
|
+
|
|
339
|
+
async handle() {
|
|
340
|
+
// ...
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
```
|
|
175
344
|
|
|
176
345
|
### Connecting "guest" instances
|
|
177
346
|
|
|
@@ -212,6 +381,14 @@ class UserService {
|
|
|
212
381
|
|
|
213
382
|
Note: the important limitation of this approach is that `@dep` are not available in entity constructors (e.g. `database` cannot be resolved in `User` constructor, because by the time the instance is instantiated it's not yet connected to the mesh).
|
|
214
383
|
|
|
384
|
+
## Tips and tricks
|
|
385
|
+
|
|
386
|
+
- Use classes as service keys whenever possible, this adds an additional compile-time type check to ensure that the implementation matches the declaration.
|
|
387
|
+
|
|
388
|
+
- TypeScript `interface` cannot be a service key, because it does not exist at runtime. Use `abstract class` instead.
|
|
389
|
+
|
|
390
|
+
- Don't be too dogmatic about interface/implementation split. For example, if all class methods are its public interface, there is no point in splitting it into an "interface" and its single implementation (e.g. ` abstract Config` and `ConfigImpl`) — this provides close to zero practical value and contributes towards people disliking the OOP paradigm for its verbosity and cargo culting. Instead just bind the concrete class to itself.
|
|
391
|
+
|
|
215
392
|
## License
|
|
216
393
|
|
|
217
394
|
[ISC](https://en.wikipedia.org/wiki/ISC_license) © Boris Okunskiy
|
package/out/main/mesh.d.ts
CHANGED
|
@@ -24,8 +24,8 @@ export declare class Mesh {
|
|
|
24
24
|
service<T>(key: AbstractClass<T> | string, impl: ServiceConstructor<T>): this;
|
|
25
25
|
constant<T>(key: ServiceKey<T>, value: T): this;
|
|
26
26
|
alias<T>(key: AbstractClass<T> | string, referenceKey: AbstractClass<T> | string): this;
|
|
27
|
-
resolve<T>(key: ServiceKey<T
|
|
28
|
-
tryResolve<T>(key: ServiceKey<T
|
|
27
|
+
resolve<T>(key: ServiceKey<T>, recursive?: boolean): T;
|
|
28
|
+
tryResolve<T>(key: ServiceKey<T>, recursive?: boolean): T | undefined;
|
|
29
29
|
connect<T>(value: T): T;
|
|
30
30
|
use(fn: Middleware): this;
|
|
31
31
|
protected instantiate<T>(binding: Binding<T>): T;
|
package/out/main/mesh.js
CHANGED
|
@@ -56,15 +56,15 @@ class Mesh {
|
|
|
56
56
|
this.bindings.set(k, { type: 'alias', key: refK });
|
|
57
57
|
return this;
|
|
58
58
|
}
|
|
59
|
-
resolve(key) {
|
|
60
|
-
const instance = this.tryResolve(key);
|
|
59
|
+
resolve(key, recursive = true) {
|
|
60
|
+
const instance = this.tryResolve(key, recursive);
|
|
61
61
|
if (instance === undefined) {
|
|
62
62
|
const k = util_1.keyToString(key);
|
|
63
63
|
throw new errors_1.MeshBindingNotFound(this.name, k);
|
|
64
64
|
}
|
|
65
65
|
return instance;
|
|
66
66
|
}
|
|
67
|
-
tryResolve(key) {
|
|
67
|
+
tryResolve(key, recursive = true) {
|
|
68
68
|
const k = util_1.keyToString(key);
|
|
69
69
|
let instance = this.instances.get(k);
|
|
70
70
|
if (instance) {
|
|
@@ -77,7 +77,7 @@ class Mesh {
|
|
|
77
77
|
this.instances.set(k, instance);
|
|
78
78
|
return instance;
|
|
79
79
|
}
|
|
80
|
-
if (this.parent) {
|
|
80
|
+
if (recursive && this.parent) {
|
|
81
81
|
return this.parent.tryResolve(key);
|
|
82
82
|
}
|
|
83
83
|
return undefined;
|