Creating scopeโ
Out of the box, Clawject provides two scopes: singleton
and transient
, but sometimes you may need to define your own scopes.
For example, if you are developing an http server application, you may want to provide a separate instance of a particular bean or set of beans for each request. Clawject provides a mechanism for creating custom scopes for scenarios such as this.
To create a custom scope, you should implement the Scope
interface.
In the following steps, we will implement request
scope that
is using AsyncLocalStorage#run
to assign and retrieve httpRequestId
.
Creating HttpExecutionContextโ
First of all - let's define HttpExecutionContext
class that will store unique id to each http request and
allows us to retrieve getCurrentRequestId
in the context of c callback call:
import { AsyncLocalStorage } from 'node:async_hooks';
export class HttpExecutionContext {
private static idSeq = 0;
private static asyncLocalStorage = new AsyncLocalStorage<number>();
static run<T>(callback: () => T): T {
return this.asyncLocalStorage.run(this.idSeq++, callback);
}
static getCurrentRequestId(): number {
//For simplicity - let's assume that AsyncLocalStorage always returns a value
return this.asyncLocalStorage.getStore()!;
}
}
Creating Scope implementationโ
Now let's create RequestScope
class that implements Scope
interface:
import { Scope, ObjectFactory, ObjectFactoryResult } from '@clawject/di';
export class RequestScope implements Scope {
static readonly instance = new RequestScope();
private constructor() {}
private beginCallbacks: (() => Promise<void>)[] = [];
private requestIdToNameToInstance = new Map<number, Map<string, any>>();
private destructionCallbacks = new Map<number, Map<string, () => void>>();
onScopeBegin(): Promise<void>
onScopeEnded(): Promise<void>
registerScopeBeginCallback(callback: () => Promise<void>): void
removeScopeBeginCallback(callback: () => Promise<void>): void
get(name: string, objectFactory: ObjectFactory): ObjectFactoryResult
remove(name: string): ObjectFactoryResult | null
registerDestructionCallback(name: string, callback: () => void): void
useProxy(): boolean
}
Now let's implement each method
onScopeBegin
โ
This method is not a part of Scope
interface, so you can name and implement it as you wish.
This method will be called when the scope is about to start, and it should execute and await all beginning callbacks that were registered via Scope#registerScopeBeginCallback
.
In our case, this method will be called at the start of each http request.
export class RequestScope implements Scope {
private beginCallbacks: (() => Promise<void>)[] = [];
async onScopeBegin(): Promise<void> {
await Promise.all(this.beginCallbacks.map(cb => cb()));
}
}
It's crucial to call all registered callbacks and await them all before the scope begins,
because some beans can be async
and may require some initialization before the scope begins.
onScopeEnded
โ
This method should be called when the scope is about to end. It should do a final cleanup and destroy all beans that were created during the scope's lifetime. This method should do the following:
- Execute and await all destruction callbacks that were registered via
Scope#registerDestructionCallback
- Remove all beans that were created during the scope's lifetime from the underlying storage.
- Remove all destruction callbacks that were registered via
Scope#registerDestructionCallback
import { HttpExecutionContext } from './HttpExecutionContext';
export class RequestScope implements Scope {
private requestIdToNameToInstance = new Map<number, Map<string, any>>();
private destructionCallbacks = new Map<number, Map<string, () => void>>();
async onScopeEnded(): Promise<void> {
const requestId = HttpExecutionContext.getCurrentRequestId();
const destructionCallbacks = Array.from(this.destructionCallbacks.get(requestId)?.values() ?? []);
await Promise.all(destructionCallbacks.map(cb => cb()));
this.requestIdToNameToInstance.delete(requestId);
this.destructionCallbacks.delete(requestId);
}
}
This method will be called at the end of each http request.
registerScopeBeginCallback
โ
This method is a part of Scope
interface, so you should implement it as it is defined in the interface.
This method should register a callback that will be executed when the scope begins, but it should not execute the callback. Note that the callback is unique for each created application context, and will be called only for application contexts that are using this scope.
export class RequestScope implements Scope {
private beginCallbacks: (() => Promise<void>)[] = [];
registerScopeBeginCallback(callback: () => Promise<void>): void {
this.beginCallbacks.push(callback);
}
}
removeScopeBeginCallback
โ
This method is a part of Scope
interface, so you should implement it as it is defined in the interface.
This method should remove a callback that was registered via Scope#registerScopeBeginCallback
, it should not execute the callback.
This method will be called only on application context shutdown.
export class RequestScope implements Scope {
private beginCallbacks: (() => Promise<void>)[] = [];
removeScopeBeginCallback(callback: () => Promise<void>): void {
this.beginCallbacks = this.beginCallbacks.filter(cb => cb !== callback);
}
}
get
โ
This method is a part of Scope
interface, so you should implement it as it is defined in the interface.
This method should do the following:
- Return an instance of a bean that is associated with the given name using
ObjectFactory
as a factory for creating the bean. - If the bean is not found in the underlying storage, it should create a new instance of the bean using the
ObjectFactory
and store it in the underlying storage. - If the bean is found in the underlying storage, it should return the instance of the bean from the underlying storage.
- If after calling objectFactory.getObject() it receives a Promise,
it should return a new promise that will resolve to the instance of the bean and store it in the underlying storage.
When the promise resolves and
get
method will be called again with the same name - it should return the awaited value of the bean from the underlying storage. If a result of awaited value is also a Promiseโit should store it and return it as a result ofget
method.
import { ObjectFactory, ObjectFactoryResult, Scope } from '@clawject/di';
import { HttpExecutionContext } from './HttpExecutionContext';
export class RequestScope implements Scope {
private requestIdToNameToInstance = new Map<number, Map<string, any>>();
get(name: string, objectFactory: ObjectFactory): ObjectFactoryResult {
const requestId = HttpExecutionContext.getCurrentRequestId();
let nameToInstance = this.requestIdToNameToInstance.get(requestId);
if (!nameToInstance) {
nameToInstance = new Map();
this.requestIdToNameToInstance.set(requestId, nameToInstance);
}
let object: any;
if (nameToInstance.has(name)) {
object = nameToInstance.get(name);
} else {
object = objectFactory.getObject();
if (object instanceof Promise) {
object = object.then(resolvedObject => {
nameToInstance!.set(name, resolvedObject);
return resolvedObject;
});
}
nameToInstance.set(name, object);
}
return object;
}
}
remove
โ
This method is a part of Scope
interface, so you should implement it as it is defined in the interface.
This method should do the following:
- Remove the bean associated with the given name from the underlying storage.
- Return the instance of the bean that was removed from the underlying storage or
null
if the bean was not found in the underlying storage.
import { ObjectFactoryResult, Scope } from '@clawject/di';
import { HttpExecutionContext } from './HttpExecutionContext';
export class RequestScope implements Scope {
private requestIdToNameToInstance = new Map<number, Map<string, any>>();
private destructionCallbacks = new Map<number, Map<string, () => void>>();
remove(name: string): ObjectFactoryResult | null {
const requestId = HttpExecutionContext.getCurrentRequestId();
const instance = this.requestIdToNameToInstance.get(requestId)?.get(name);
this.destructionCallbacks.get(requestId)?.delete(name);
this.requestIdToNameToInstance.get(requestId)?.delete(name);
return instance ?? null;
}
}
registerDestructionCallback
โ
This method is a part of Scope
interface, so you should implement it as it is defined in the interface.
This method should register a callback that will be executed when the scope ends, but it should not execute the callback. This method will be called by a container when a bean is being destroyed.
import { HttpExecutionContext } from './HttpExecutionContext';
export class RequestScope implements Scope {
private destructionCallbacks = new Map<number, Map<string, () => void>>();
registerDestructionCallback(name: string, callback: () => void): void {
const requestId = HttpExecutionContext.getCurrentRequestId();
let nameToCallback = this.destructionCallbacks.get(requestId);
if (!nameToCallback) {
nameToCallback = new Map();
this.destructionCallbacks.set(requestId, nameToCallback);
}
nameToCallback.set(name, callback);
}
}
useProxy
โ
This method is a part of Scope
interface, so you should implement it as it is defined in the interface.
Note that this method is optional, and you can omit it, default implementation will return true
.
This method should return true
if the scope requires a proxy to be injected instead of the actual bean instance, and false
otherwise.
Note that in most of the cases you will need to inject proxies.
export class RequestScope implements Scope {
useProxy(): boolean {
return true;
}
}
Put the pieces togetherโ
import { Scope, ObjectFactory, ObjectFactoryResult } from '@clawject/di';
import { HttpExecutionContext } from './HttpExecutionContext';
export class RequestScope implements Scope {
private constructor() {}
static readonly instance = new RequestScope();
private beginCallbacks: (() => Promise<void>)[] = [];
private requestIdToNameToInstance = new Map<number, Map<string, any>>();
private destructionCallbacks = new Map<number, Map<string, () => void>>();
async onScopeBegin(): Promise<void> {
await Promise.all(this.beginCallbacks.map(cb => cb()));
}
async onScopeEnded(): Promise<void> {
const requestId = HttpExecutionContext.getCurrentRequestId();
const destructionCallbacks = Array.from(this.destructionCallbacks.get(requestId)?.values() ?? []);
await Promise.all(destructionCallbacks.map(cb => cb()));
this.requestIdToNameToInstance.delete(requestId);
this.destructionCallbacks.delete(requestId);
}
registerScopeBeginCallback(callback: () => Promise<void>): void {
this.beginCallbacks.push(callback);
}
removeScopeBeginCallback(callback: () => Promise<void>): void {
this.beginCallbacks = this.beginCallbacks.filter(cb => cb !== callback);
}
get(name: string, objectFactory: ObjectFactory): ObjectFactoryResult {
const requestId = HttpExecutionContext.getCurrentRequestId();
let nameToInstance = this.requestIdToNameToInstance.get(requestId);
if (!nameToInstance) {
nameToInstance = new Map();
this.requestIdToNameToInstance.set(requestId, nameToInstance);
}
let object: any;
if (nameToInstance.has(name)) {
object = nameToInstance.get(name);
} else {
object = objectFactory.getObject();
if (object instanceof Promise) {
object = object.then(resolvedObject => {
nameToInstance!.set(name, resolvedObject);
return resolvedObject;
});
}
nameToInstance.set(name, object);
}
return object;
}
remove(name: string): ObjectFactoryResult | null {
const requestId = HttpExecutionContext.getCurrentRequestId();
const instance = this.requestIdToNameToInstance.get(requestId)?.get(name);
this.destructionCallbacks.get(requestId)?.delete(name);
this.requestIdToNameToInstance.get(requestId)?.delete(name);
return instance ?? null;
}
registerDestructionCallback(name: string, callback: () => void): void {
const requestId = HttpExecutionContext.getCurrentRequestId();
let nameToCallback = this.destructionCallbacks.get(requestId);
if (!nameToCallback) {
nameToCallback = new Map();
this.destructionCallbacks.set(requestId, nameToCallback);
}
nameToCallback.set(name, callback);
}
useProxy(): boolean {
return true;
}
}
Creating the server codeโ
Now let's create a simple http server that will use our custom scope.
To do this, we will use the http
module from Node.js and the HttpExecutionContext
class that we created earlier.
When a request is received, we will run the request handling code inside HttpExecutionContext's run
method.
Before any request handling code is executed, we will call RequestScope#onScopeBegin
to initialize the scoped beans, and await initialization.
After the request handling code is executed, we will call RequestScope#onScopeEnded
to destroy the scoped beans, and await destruction.
import http from 'node:http';
import { HttpExecutionContext } from './HTTPExecutionContext';
import { RequestScope } from './RequestScope';
http.createServer((req, res) => {
HttpExecutionContext.run(async () => {
await RequestScope.instance.onScopeBegin();
/* request-handling */
await RequestScope.instance.onScopeEnded();
});
}).listen(8080);
Registering the Scopeโ
To make the Clawject container aware of your new scope,
you need to register it through the registerScope
method on a ScopeRegister
class.
Note that you should register your scope before creating the container.
import { ScopeRegister } from '@clawject/di';
import { RequestScope } from './RequestScope';
ScopeRegister.registerScope('request', RequestScope.instance);
Using the Custom Scopeโ
Now we have registered our custom scope, and we can use it in our beans.
Let's define RequestStorage
class that can store arbitrary data for each http request:
export class RequestStorage {
private data = new Map<string, any>();
set(key: string, value: any): void {
this.data.set(key, value);
}
get(key: string): any {
return this.data.get(key);
}
}
Now let's define request
bean that will be scoped to each http request:
import { Bean, ClawjectApplication, Scope } from '@clawject/di';
import { RequestStorage } from './RequestStorage';
@ClawjectApplication
export class Application {
@Scope('request') requestStorage = Bean(RequestStorage)
}
Now, when you inject RequestStorage into other beans (even singleton-scoped) โ the proxy will be injected, and the actual instance of RequestStorage
will be created and destroyed for each http request.
import { RequestStorage } from './RequestStorage';
export class SomeService {
constructor(private requestStorage: RequestStorage) {}
/* ... */
}