Comparing version 0.2.1 to 0.3.0
import { Mesh } from './mesh'; | ||
import { ServiceConstructor } from './types'; | ||
import { Constructor, ServiceConstructor } from './types'; | ||
export declare abstract class Binding<T> { | ||
@@ -15,7 +15,18 @@ readonly mesh: Mesh; | ||
export declare class ServiceBinding<T> extends Binding<T> { | ||
readonly ctor: ServiceConstructor<T>; | ||
ctor: ServiceConstructor<T>; | ||
instance: T | undefined; | ||
constructor(mesh: Mesh, key: string, ctor: ServiceConstructor<T>); | ||
get(): T; | ||
protected processClass(ctor: any): { | ||
new (): { | ||
[x: string]: any; | ||
}; | ||
[x: string]: any; | ||
}; | ||
} | ||
export declare class ClassBinding<T extends Constructor<T>> extends Binding<T> { | ||
readonly ctor: T; | ||
constructor(mesh: Mesh, key: string, ctor: T); | ||
get(): T; | ||
} | ||
export declare class ProxyBinding<T> extends Binding<T> { | ||
@@ -22,0 +33,0 @@ readonly alias: string; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.ProxyBinding = exports.ServiceBinding = exports.ConstantBinding = exports.Binding = void 0; | ||
exports.ProxyBinding = exports.ClassBinding = exports.ServiceBinding = exports.ConstantBinding = exports.Binding = void 0; | ||
class Binding { | ||
@@ -24,12 +24,32 @@ constructor(mesh, key) { | ||
super(mesh, key); | ||
this.ctor = ctor; | ||
this.ctor = this.processClass(ctor); | ||
} | ||
get() { | ||
if (!this.instance) { | ||
this.instance = this.mesh.connect(new this.ctor()); | ||
const inst = new this.ctor(); | ||
this.instance = this.mesh.connect(inst); | ||
} | ||
return this.instance; | ||
} | ||
processClass(ctor) { | ||
// A fake derived class is created with Mesh attached to its prototype. | ||
// This allows accessing deps in constructor whilst preserving instanceof. | ||
const derived = class extends ctor { | ||
}; | ||
Object.defineProperty(derived, 'name', { value: ctor.name }); | ||
this.mesh.injectRef(derived.prototype); | ||
return derived; | ||
} | ||
} | ||
exports.ServiceBinding = ServiceBinding; | ||
class ClassBinding extends Binding { | ||
constructor(mesh, key, ctor) { | ||
super(mesh, key); | ||
this.ctor = ctor; | ||
} | ||
get() { | ||
return this.ctor; | ||
} | ||
} | ||
exports.ClassBinding = ClassBinding; | ||
class ProxyBinding extends Binding { | ||
@@ -36,0 +56,0 @@ constructor(mesh, key, alias) { |
@@ -17,5 +17,5 @@ declare class BaseError extends Error { | ||
} | ||
export declare class MeshInvalidServiceBinding extends BaseError { | ||
export declare class MeshInvalidBinding extends BaseError { | ||
constructor(key: string); | ||
} | ||
export {}; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.MeshInvalidServiceBinding = exports.MeshServiceNotFound = exports.DepInstanceNotConnected = exports.DepKeyNotInferred = void 0; | ||
exports.MeshInvalidBinding = exports.MeshServiceNotFound = exports.DepInstanceNotConnected = exports.DepKeyNotInferred = void 0; | ||
class BaseError extends Error { | ||
@@ -35,5 +35,5 @@ constructor() { | ||
exports.MeshServiceNotFound = MeshServiceNotFound; | ||
class MeshInvalidServiceBinding extends BaseError { | ||
class MeshInvalidBinding extends BaseError { | ||
constructor(key) { | ||
super(`Invalid service binding "${key}". Valid bindings are: ` + | ||
super(`Invalid binding "${key}". Valid bindings are: ` + | ||
`string to constructor e.g. ("MyService", MyService) or ` + | ||
@@ -44,2 +44,2 @@ `abstract class to constructor e.g. (MyService, MyServiceImpl) or` + | ||
} | ||
exports.MeshInvalidServiceBinding = MeshInvalidServiceBinding; | ||
exports.MeshInvalidBinding = MeshInvalidBinding; |
@@ -0,3 +1,4 @@ | ||
import { ClassBinding } from '.'; | ||
import { Binding } from './bindings'; | ||
import { AbstractService, Middleware, ServiceConstructor, ServiceKey } from './types'; | ||
import { AbstractClass, Constructor, Middleware, ServiceConstructor, ServiceKey } from './types'; | ||
export declare const MESH_REF: unique symbol; | ||
@@ -11,6 +12,9 @@ export declare class Mesh { | ||
bind<T>(impl: ServiceConstructor<T>): Binding<T>; | ||
bind<T>(key: AbstractService<T> | string, impl: ServiceConstructor<T>): Binding<T>; | ||
bind<T>(key: AbstractClass<T> | string, impl: ServiceConstructor<T>): Binding<T>; | ||
protected _bindService<T>(k: string, impl: ServiceConstructor<T>): Binding<T>; | ||
class<T extends Constructor<any>>(ctor: T): ClassBinding<T>; | ||
class<T extends Constructor<any>>(key: AbstractClass<T> | string, ctor: T): Binding<T>; | ||
protected _bindClass<T extends Constructor<any>>(k: string, ctor: T): ClassBinding<T>; | ||
constant<T>(key: ServiceKey<T>, value: T): Binding<T>; | ||
alias<T>(key: AbstractService<T> | string, referenceKey: AbstractService<T> | string): Binding<T>; | ||
alias<T>(key: AbstractClass<T> | string, referenceKey: AbstractClass<T> | string): Binding<T>; | ||
resolve<T>(key: ServiceKey<T>): T; | ||
@@ -20,3 +24,3 @@ connect<T>(value: T): T; | ||
protected applyMiddleware<T>(value: T): T; | ||
protected addMeshRef(value: any): void; | ||
injectRef(value: any): void; | ||
} |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.Mesh = exports.MESH_REF = void 0; | ||
const _1 = require("."); | ||
const bindings_1 = require("./bindings"); | ||
@@ -23,3 +24,3 @@ const errors_1 = require("./errors"); | ||
} | ||
throw new errors_1.MeshInvalidServiceBinding(String(key)); | ||
throw new errors_1.MeshInvalidBinding(String(key)); | ||
} | ||
@@ -31,2 +32,17 @@ _bindService(k, impl) { | ||
} | ||
class(key, ctor) { | ||
const k = keyToString(key); | ||
if (typeof ctor === 'function') { | ||
return this._bindClass(k, ctor); | ||
} | ||
else if (typeof key === 'function') { | ||
return this._bindClass(k, key); | ||
} | ||
throw new errors_1.MeshInvalidBinding(String(key)); | ||
} | ||
_bindClass(k, ctor) { | ||
const binding = new _1.ClassBinding(this, k, ctor); | ||
this.bindings.set(k, binding); | ||
return binding; | ||
} | ||
constant(key, value) { | ||
@@ -58,3 +74,3 @@ const k = keyToString(key); | ||
const res = this.applyMiddleware(value); | ||
this.addMeshRef(res); | ||
this.injectRef(res); | ||
return res; | ||
@@ -73,3 +89,3 @@ } | ||
} | ||
addMeshRef(value) { | ||
injectRef(value) { | ||
if (typeof value !== 'object') { | ||
@@ -76,0 +92,0 @@ return; |
@@ -7,7 +7,7 @@ export declare type Constructor<T> = { | ||
}; | ||
export declare type AbstractService<T> = { | ||
export declare type AbstractClass<T> = { | ||
name: string; | ||
prototype: T; | ||
}; | ||
export declare type ServiceKey<T> = ServiceConstructor<T> | AbstractService<T> | string; | ||
export declare type ServiceKey<T> = ServiceConstructor<T> | AbstractClass<T> | string; | ||
export declare type Middleware = (instance: any) => any; |
{ | ||
"name": "mesh-ioc", | ||
"version": "0.2.1", | ||
"version": "0.3.0", | ||
"description": "Mesh: Powerful and Lightweight IoC Library", | ||
@@ -5,0 +5,0 @@ "main": "out/main/index.js", |
199
README.md
@@ -83,3 +83,6 @@ # Mesh IoC | ||
this.redis = new RedisClient(/* ... */); | ||
this.redis.on('connect', () => this.logger.log('Connected to Redis')); | ||
this.redis.on('connect', () => { | ||
// Logger can now be used by this class transparently | ||
this.logger.log('Connected to Redis'); | ||
}); | ||
} | ||
@@ -91,4 +94,4 @@ } | ||
class AppMesh extends Mesh { | ||
this.bind(Redis); | ||
this.bind(Logger, ConsoleLogger); | ||
this.bind(Redis); | ||
} | ||
@@ -116,1 +119,193 @@ ``` | ||
- Constant values can be bound to mesh. Those could be instances of other classes. | ||
**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 construtor arguments, read on! | ||
## Application Architecture Guide | ||
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. | ||
1. Identify the layers of your application. Oftentimes different components have different lifespans or, as we tend to refer to it, scopes: | ||
- **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). | ||
- **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. | ||
- **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) | ||
2. Build the mesh hierarchy, starting from application scope. | ||
```ts | ||
// app.ts | ||
export class App { | ||
// You can either inherit from Mesh or store it as a field. | ||
// Name parameter is optional, but can be useful for debugging. | ||
mesh = new Mesh('App'); | ||
// Define your application-scoped services | ||
logger = mesh.bind(Logger, GlobalLogger); | ||
database = mesh.bind(MyDatabase); | ||
server = mesh.bind(MyServer); | ||
// ... | ||
start() { | ||
// Define logic for application startup | ||
// (e.g. connect to databases, start listening to servers, etc) | ||
} | ||
} | ||
// session.ts | ||
export class Session { | ||
mesh: Mesh; | ||
// A sample session scope will depend on Request and Response; | ||
// Parent mesh is required so that session-scoped services | ||
// can access application-scoped services | ||
// (the other way around does not work, obviously) | ||
constructor(parentMesh: Mesh, req: Request, res: Response) { | ||
this.mesh = new Mesh('Session', parentMesh); | ||
// Session dependencies can be bound as constants | ||
this.mesh.constant(Request, req); | ||
this.mesh.constant(Response, req); | ||
// Bindings from parent can be overridden, so that session-scoped services | ||
// could use more specialized versions | ||
this.mesh.bind(Logger, SessionLogger); | ||
// Define other session-scoped services | ||
this.mesh.bind(SessionScopedService); | ||
// ... | ||
} | ||
start() { | ||
// Define what happens when session is established | ||
} | ||
} | ||
``` | ||
3. Create an application entrypoint (advice: never mix modules that export classes with entrypoint modules!): | ||
```ts | ||
// bin/run.ts | ||
const app = new App(); | ||
app.start(); | ||
``` | ||
4. Identify the proper component for session entrypoint: | ||
```ts | ||
export class MyServer { | ||
// Note: Mesh is automatically available in all "connected" classes | ||
@dep() mesh!: Mesh; | ||
// The actual server (e.g. http server or web socket server) | ||
server: Server; | ||
constructor() { | ||
this.server = new Server((req, res) => { | ||
// This is the actual entrypoint of Session | ||
const session = new Session(this.mesh, req, res); | ||
// Note the similarity to application entrypoint | ||
session.start(); | ||
}); | ||
} | ||
} | ||
``` | ||
5. Use `@dep()` to transparently inject dependencies in your services: | ||
```ts | ||
export class SessionScopedService { | ||
@dep() database!: Database; | ||
@dep() req!: Request; | ||
@dep() res!: Request; | ||
// ... | ||
} | ||
``` | ||
6. Come up with conventions and document them, for example: | ||
- create different directories for services with different scopes | ||
- separate entrypoints from the rest of the modules | ||
- entrypoints only import, instantiate and invoke methods (think "runnable from CLI") | ||
- all other modules only export stuff | ||
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. | ||
## Advanced | ||
### Connecting "guest" instances | ||
Mesh IoC allows connecting an arbitrary instance to the mesh, so that the `@dep` can be used in it. | ||
For example: | ||
```ts | ||
// This entity class is not managed by Mesh directly, instead it's instantiated by UserService | ||
class User { | ||
@dep() database!: Database; | ||
// Note: constructor can have arbitrary arguments in this case, | ||
// because the instantiated isn't controlled by Mesh | ||
constructor( | ||
public firstName = '', | ||
public lastName = '', | ||
public email = '', | ||
// ... | ||
) {} | ||
async save() { | ||
await this.database.save(this); | ||
} | ||
} | ||
class UserService { | ||
@dep() mesh!: Mesh; | ||
createUser(firstName = '', lastName = '', email = '', /*...*/) { | ||
const user = new User(); | ||
// Now connect it to mesh, so that User can access its services via `@dep` | ||
return this.mesh.connect(user); | ||
} | ||
} | ||
``` | ||
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). | ||
### Binding classes | ||
Mesh typically connects the instances of services (again, services are classes with zero-arg constructors). | ||
However, in some cases you may need to instantiate other classes, for example, third-party or with non-zero-arg constructors. You can always do it directly, however, a level of indirection can be introduced by defining a class on Mesh. This can be especially useful in tests where classes can be substituted on Mesh level without changing the implementation. | ||
Example: | ||
```ts | ||
// An example arbitrary class, unconnected to mesh | ||
class Session { | ||
constructor(readonly sessionId: number) {} | ||
} | ||
// A normal service connected to mesh | ||
class SessionManager { | ||
// Class constructor is injected (note: `key` is required because it cannot be deferred in this case) | ||
@dep({ key: 'Session' }) Session!: typeof Session; | ||
createSession(id: number): Session { | ||
// Instantiate using injected constructor | ||
return new this.Session(id); | ||
} | ||
} | ||
// ... | ||
const mesh = new Mesh(); | ||
mesh.bind(SessionManager); | ||
mesh.class(Session); // This makes Session class available as a binding | ||
// In tests this can be overridden, so SessionManager will transparently instantiate a different class | ||
// (assuming constructor signatures match) | ||
mesh.class(Session, MySession); | ||
``` | ||
This approach can also be combined with `connect` so that the arbitrary class can also use `@dep`. Mix & match FTW! | ||
## License | ||
[ISC](https://en.wikipedia.org/wiki/ISC_license) © Boris Okunskiy |
26867
373
309