mesh-ioc 1.3.0 β†’ 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 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 together application services (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.)
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
- ## Quick API Cheatsheet
16
+ ## API Cheatsheet
17
17
 
18
18
  ```ts
19
19
  // Mesh is an IoC container that stores service bindings and instantiated objects
@@ -57,25 +57,6 @@ class User {
57
57
 
58
58
  const user = mesh.connect(new User()); // now user.db will also be resolved by Mesh
59
59
 
60
- // Scopes:
61
-
62
- // Declare scoped bindings
63
- mesh.scope('request')
64
- .service(UsersRouter)
65
- .service(OrdersRouter);
66
-
67
- // Create and use scope
68
- const request = mesh.createScope('request')
69
- // Bind scope-specific data
70
- .constant(Request, req)
71
- .constant(Response, res);
72
-
73
- // Scoped services can use scope-specific data
74
- class UsersRouter {
75
- @dep() req!: Request;
76
- @dep() res!: Response;
77
- }
78
-
79
60
  // Middleware:
80
61
  mesh.use(instance => {
81
62
  // Allows modifying instances as they are created or connected
@@ -161,16 +142,20 @@ class Redis {
161
142
 
162
143
  // app.ts
163
144
 
164
- class AppMesh extends Mesh {
165
- // List all services so that mesh connects them together
166
- this.service(Redis);
167
- this.service(Logger, ConsoleLogger);
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
+ }
168
153
  }
169
154
  ```
170
155
 
171
156
  There are several aspects that differentiate Mesh IoC from the rest of the DI libraries.
172
157
 
173
- - Mesh is used only for services, so 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.
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.
174
159
 
175
160
  - If you're familiar with Inversify, this effectively makes all bindings `inSingletonScope`.
176
161
 
@@ -186,110 +171,176 @@ There are several aspects that differentiate Mesh IoC from the rest of the DI li
186
171
 
187
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.
188
173
 
189
- - 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.
190
175
 
191
- **Important!** Mesh should be used to track _services_. We defined services as classes with **zero-argument constructors** (this is also enforced by TypeScript). However, there are multiple patterns to support constructor arguments, read on!
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!
192
177
 
193
178
  ## Application Architecture Guide
194
179
 
195
- This short guide briefly explains the basic concepts of a good application architecture where all components are loosely coupled, dependencies are easy to reason about and are not mixed with the actual data arguments.
196
-
197
- 1. Identify the layers of your application. Oftentimes different components have different lifespans or, as we tend to refer to it, scopes:
198
-
199
- - **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).
200
- - **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.
201
- - **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)
202
-
203
- 2. Build the mesh hierarchy, starting from application scope, descending into more granular scopes.
204
-
205
- ```ts
206
- // app.ts
207
- export class App {
208
- // You can either inherit from Mesh or store it as a field.
209
- // Name parameter is optional, but can be useful for debugging.
210
- mesh: Mesh;
211
-
212
- constructor() {
213
- this.mesh = new Mesh('App')
214
- // Define your application-scoped services
215
- .constant(App, this)
216
- .service(Logger, GlobalLogger);
217
- .service(MyDatabase);
218
- .service(MyServer);
219
- // Define your session-scoped services
220
- .scope('session')
221
- .service(Logger, SessionLogger)
222
- .service(SessionScopedService);
223
- }
224
-
225
- start() {
226
- // Define logic for application startup
227
- // (e.g. connect to databases, start listening to servers, etc)
228
- this.mesh.resolve(MyServer).listen();
229
- }
230
-
231
- startSession(req: Request, res: Response) {
232
- // Bind session-specific data (e.g. request, response, client web socket, session id, etc)
233
- const sessionMesh = this.mesh.createScope('session')
234
- // Session-specific data can be bound by session-scoped services
235
- .constant(Request, req)
236
- .constant(Response, res);
237
- // Define logic for session initialisation
238
- sessionMesh.resolve(SessionScopedService).doStuff();
239
- }
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
+ // ...
240
217
  }
241
- ```
242
218
 
243
- 3. Create an application entrypoint (advice: never mix modules that export classes with entrypoint modules!):
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
+ }
244
225
 
245
- ```ts
246
- // bin/run.ts
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
+ }
247
232
 
248
- const app = new App();
249
- app.start();
250
- ```
233
+ }
234
+ ```
251
235
 
252
- 4. Identify the proper component for session entrypoint:
236
+ The entrypoint would be an actual "main" function that instantiates the `App` class and calls its `start` method:
253
237
 
254
- ```ts
255
- export class MyServer {
256
- @dep() app!: App;
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
257
 
258
- // The actual server (e.g. http server or web socket server)
259
- server: Server;
258
+ let app: TestApp;
260
259
 
261
- constructor() {
262
- this.server = new Server((req, res) => {
263
- // This is the actual entrypoint of Session
264
- app.startSession(req, res);
265
- });
266
- }
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
+ // ...
267
273
  }
268
- ```
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.
269
278
 
270
- 5. Use `@dep()` to transparently inject dependencies in your services:
279
+ ### Creating request/response scopes
271
280
 
272
- ```ts
273
- export class SessionScopedService {
281
+ Mesh IoC opts for maximum simplicity by following a straighforward model: each mesh instance is a scope.
274
282
 
275
- @dep() database!: Database;
276
- @dep() req!: Request;
277
- @dep() res!: Response;
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.
284
+
285
+ ```ts
286
+ // src/main/app.ts
278
287
 
288
+ export class App {
289
+ mesh = new Mesh('App');
290
+
291
+ constructor() {
279
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());
280
296
  }
281
- ```
282
297
 
283
- 6. Come up with conventions and document them, for example:
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
+ }
284
310
 
285
- - create different directories for services with different scopes
286
- - separate entrypoints from the rest of the modules
287
- - entrypoints only import, instantiate and invoke methods (think "runnable from CLI")
288
- - all other modules only export stuff
311
+ }
312
+ ```
313
+
314
+ Then in `MyServer`:
289
315
 
290
- You can take those further and adapt to your own needs. Meshes are composable and the underlying mechanics are quite simple. Start using it and you'll get a better understanding of how to adapt it to the needs of your particular case.
316
+ ```ts
317
+ export class MyServer {
291
318
 
292
- ## Advanced
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
+ ```
293
344
 
294
345
  ### Connecting "guest" instances
295
346
 
@@ -330,6 +381,14 @@ class UserService {
330
381
 
331
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).
332
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
+
333
392
  ## License
334
393
 
335
394
  [ISC](https://en.wikipedia.org/wiki/ISC_license) Β© Boris Okunskiy
@@ -1,5 +1,4 @@
1
1
  export * from './decorators/dep';
2
2
  export * from './errors';
3
3
  export * from './mesh';
4
- export * from './scope';
5
4
  export * from './types';
package/out/main/index.js CHANGED
@@ -13,5 +13,4 @@ Object.defineProperty(exports, "__esModule", { value: true });
13
13
  __exportStar(require("./decorators/dep"), exports);
14
14
  __exportStar(require("./errors"), exports);
15
15
  __exportStar(require("./mesh"), exports);
16
- __exportStar(require("./scope"), exports);
17
16
  __exportStar(require("./types"), exports);
@@ -1,24 +1,33 @@
1
- import { Scope } from './scope';
2
1
  import { AbstractClass, Binding, Middleware, ServiceConstructor, ServiceKey } from './types';
3
2
  export declare const MESH_REF: unique symbol;
3
+ /**
4
+ * An IoC container.
5
+ *
6
+ * Encapsulates bindings β€” a map that allows to associate a _service key_ with a way to obtain an instance.
7
+ *
8
+ * Three binding types are supported via corresponding methods:
9
+ *
10
+ * - `service` β€” a zero-arg constructor mapping; these will be instantiated on demand and cached in this mesh
11
+ * - `constant` β€” an instance of a class (can be bound by using class as service name) or an arbitrary value bound by a string key
12
+ * - `alias` β€” a "redirect" mapping, useful in tests
13
+ */
4
14
  export declare class Mesh {
5
15
  name: string;
6
16
  parent: Mesh | undefined;
7
- currentScope: Scope;
8
- childScopes: Map<string, Scope>;
17
+ bindings: Map<string, Binding<any>>;
9
18
  instances: Map<string, any>;
10
19
  middlewares: Middleware[];
11
- constructor(name?: string, parent?: Mesh | undefined, scope?: Scope);
20
+ constructor(name?: string, parent?: Mesh | undefined);
21
+ [Symbol.iterator](): Generator<[string, Binding<any>], void, undefined>;
22
+ clone(): Mesh;
12
23
  service<T>(impl: ServiceConstructor<T>): this;
13
24
  service<T>(key: AbstractClass<T> | string, impl: ServiceConstructor<T>): this;
14
25
  constant<T>(key: ServiceKey<T>, value: T): this;
15
26
  alias<T>(key: AbstractClass<T> | string, referenceKey: AbstractClass<T> | string): this;
16
- resolve<T>(key: ServiceKey<T>): T;
17
- tryResolve<T>(key: ServiceKey<T>): T | undefined;
27
+ resolve<T>(key: ServiceKey<T>, recursive?: boolean): T;
28
+ tryResolve<T>(key: ServiceKey<T>, recursive?: boolean): T | undefined;
18
29
  connect<T>(value: T): T;
19
30
  use(fn: Middleware): this;
20
- scope(scopeId: string): Scope;
21
- createScope(scopeId: string, scopeName?: string): Mesh;
22
31
  protected instantiate<T>(binding: Binding<T>): T;
23
32
  protected applyMiddleware<T>(value: T): T;
24
33
  protected injectRef(value: any): void;
package/out/main/mesh.js CHANGED
@@ -2,53 +2,82 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Mesh = exports.MESH_REF = void 0;
4
4
  const errors_1 = require("./errors");
5
- const scope_1 = require("./scope");
6
5
  const util_1 = require("./util");
7
6
  exports.MESH_REF = Symbol.for('MESH_REF');
7
+ /**
8
+ * An IoC container.
9
+ *
10
+ * Encapsulates bindings β€” a map that allows to associate a _service key_ with a way to obtain an instance.
11
+ *
12
+ * Three binding types are supported via corresponding methods:
13
+ *
14
+ * - `service` β€” a zero-arg constructor mapping; these will be instantiated on demand and cached in this mesh
15
+ * - `constant` β€” an instance of a class (can be bound by using class as service name) or an arbitrary value bound by a string key
16
+ * - `alias` β€” a "redirect" mapping, useful in tests
17
+ */
8
18
  class Mesh {
9
- constructor(name = 'default', parent = undefined, scope) {
19
+ constructor(name = 'default', parent = undefined) {
10
20
  this.name = name;
11
21
  this.parent = parent;
12
- this.childScopes = new Map();
22
+ this.bindings = new Map();
13
23
  this.instances = new Map();
14
24
  this.middlewares = [];
15
- this.currentScope = scope !== null && scope !== void 0 ? scope : new scope_1.Scope(name);
16
- this.currentScope.constant('Mesh', this);
25
+ this.constant(Mesh, this);
26
+ }
27
+ *[Symbol.iterator]() {
28
+ yield* this.bindings.entries();
29
+ }
30
+ clone() {
31
+ const clone = new Mesh();
32
+ clone.parent = this.parent;
33
+ clone.bindings = new Map(this.bindings);
34
+ return clone;
17
35
  }
18
36
  service(key, impl) {
19
- this.currentScope.service(key, impl);
20
- return this;
37
+ const k = util_1.keyToString(key);
38
+ if (typeof impl === 'function') {
39
+ this.bindings.set(k, { type: 'service', class: impl });
40
+ return this;
41
+ }
42
+ else if (typeof key === 'function') {
43
+ this.bindings.set(k, { type: 'service', class: key });
44
+ return this;
45
+ }
46
+ throw new errors_1.MeshInvalidBinding(String(key));
21
47
  }
22
48
  constant(key, value) {
23
- this.currentScope.constant(key, value);
49
+ const k = util_1.keyToString(key);
50
+ this.bindings.set(k, { type: 'constant', value });
24
51
  return this;
25
52
  }
26
53
  alias(key, referenceKey) {
27
- this.currentScope.alias(key, referenceKey);
54
+ const k = util_1.keyToString(key);
55
+ const refK = util_1.keyToString(referenceKey);
56
+ this.bindings.set(k, { type: 'alias', key: refK });
28
57
  return this;
29
58
  }
30
- resolve(key) {
31
- const instance = this.tryResolve(key);
59
+ resolve(key, recursive = true) {
60
+ const instance = this.tryResolve(key, recursive);
32
61
  if (instance === undefined) {
33
62
  const k = util_1.keyToString(key);
34
63
  throw new errors_1.MeshBindingNotFound(this.name, k);
35
64
  }
36
65
  return instance;
37
66
  }
38
- tryResolve(key) {
67
+ tryResolve(key, recursive = true) {
39
68
  const k = util_1.keyToString(key);
40
69
  let instance = this.instances.get(k);
41
70
  if (instance) {
42
71
  return instance;
43
72
  }
44
- const binding = this.currentScope.bindings.get(k);
73
+ const binding = this.bindings.get(k);
45
74
  if (binding) {
46
75
  instance = this.instantiate(binding);
47
76
  instance = this.connect(instance);
48
77
  this.instances.set(k, instance);
49
78
  return instance;
50
79
  }
51
- if (this.parent) {
80
+ if (recursive && this.parent) {
52
81
  return this.parent.tryResolve(key);
53
82
  }
54
83
  return undefined;
@@ -62,20 +91,6 @@ class Mesh {
62
91
  this.middlewares.push(fn);
63
92
  return this;
64
93
  }
65
- scope(scopeId) {
66
- let scope = this.childScopes.get(scopeId);
67
- if (!scope) {
68
- scope = new scope_1.Scope(scopeId);
69
- this.childScopes.set(scopeId, scope);
70
- }
71
- return scope;
72
- }
73
- createScope(scopeId, scopeName = scopeId) {
74
- const childScope = this.childScopes.get(scopeId);
75
- const newScope = new scope_1.Scope(scopeName, childScope !== null && childScope !== void 0 ? childScope : []);
76
- const mesh = new Mesh(scopeId, this, newScope);
77
- return mesh;
78
- }
79
94
  instantiate(binding) {
80
95
  switch (binding.type) {
81
96
  case 'alias':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mesh-ioc",
3
- "version": "1.3.0",
3
+ "version": "2.1.0",
4
4
  "description": "Mesh: Powerful and Lightweight IoC Library",
5
5
  "main": "out/main/index.js",
6
6
  "types": "out/main/index.d.ts",
@@ -1,7 +0,0 @@
1
- import { ServiceMetadata } from '../types';
2
- export declare const serviceMetadata: ServiceMetadata[];
3
- export interface SvcOptions {
4
- alias?: string;
5
- metadata?: any;
6
- }
7
- export declare function service(options?: SvcOptions): (target: any) => void;
@@ -1,10 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.service = exports.serviceMetadata = void 0;
4
- exports.serviceMetadata = [];
5
- function service(options = {}) {
6
- return function (target) {
7
- exports.serviceMetadata.push(Object.assign({ class: target }, options));
8
- };
9
- }
10
- exports.service = service;