Skip to content

Commit

Permalink
Merge 0a0fa70 into d6e4e7e
Browse files Browse the repository at this point in the history
  • Loading branch information
PhakornKiong committed Apr 1, 2021
2 parents d6e4e7e + 0a0fa70 commit 1ab9abd
Show file tree
Hide file tree
Showing 5 changed files with 233 additions and 35 deletions.
46 changes: 39 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,8 @@ store1.run({}, () => {

## als.run<R>(defaults: Record<string, any>, callback: (...args: any[]) => R, ...args: any[]): R

Runs a function synchronously and start the boundary of a context, anything set to be run from within the callback will have the same context.

***Parameters***

- ***defaults***: Optional `Map` or `Record` containing default values for the context
Expand All @@ -227,6 +229,8 @@ store1.run({}, () => {

## als.get(key: string): T | undefined

Get the stored value in context or undefined

***Parameters***

- ***key***: a string key to retrieve the stored value in context
Expand All @@ -239,6 +243,8 @@ store1.run({}, () => {

## als.set(key: string, value: T): void

Set key & value to the current context

***Parameters***

- ***key***: a string key to store value in context
Expand All @@ -249,24 +255,51 @@ store1.run({}, () => {

## als.getStore(): StorageType | undefined

Get the entire context in Map object or undefined

***Return***

- Return the entire context in `Map` object or undefined (if it is called outside `als.run`)




## als.disable(): void

Disable the instance of `als`. All subsequent calls to `als.getStore()` & `als.get()` will return undefined until new context is created using `als.run()`. It is developer’s responsibility to ensure that `als` is disabled so the instance of `als` can be garbage-collected. This does not applies to the `store` or `asyncResource` (Which is used to achieve this functionality) as these objects are garbage collected when the async resources is completed (`after` hook)

Use this method when `als` is no longer in use.



## als.exit( fn:(...args: any[]) => any, ...args:any[] ): any

Runs a function synchronously outside of a context and returns its return value. The store is not accessible within the function or the asynchronous operations created within the function. Any `als.getStore()` or `als.get()` call done within the function will always return `undefined`.

***Parameters***

- ***fn***: Function to be bind to the current execution context
- ***...args***: Option arguments to be passed to `fn`

***Return***

- `fn`'s return value



## als.bind( fn:(...args: any[]) => any, ...args:any[] ): any

Bind a function to the current execution context. This is useful especially when you need to access the context outside of `als.run()` or when dealing when `EventEmitters`

***Parameters***

- ***fn***: Function to be bind to the current execution context
- ***...args***: Option arguments to be passed to `fn`

***Return***

- `fn`'s return value
- `fn`'s return value

```javascript
```javascript
const ALS = require('alscontext').default;
const store = ALS();

Expand All @@ -280,12 +313,14 @@ store.run({ test: 'something' }, () => {

bindedFunc(); // return "something" - able to get the context outside of run
someFunc(); // return undefined
```
```



## als.bindEmitter( asyncResource: AsyncResource, fn:(...args: any[]) => any, ...args:any[] ): any

An extension to the `als.bind` which allow developers to specify the execution context to be bind to the function. This is useful especially when you need to access the context outside of `als.run()` or when dealing when `EventEmitters`

***Parameters***

- ***asyncResource***: AsyncResource object from Nodejs (const asyncResource = new AsyncResource)
Expand Down Expand Up @@ -319,6 +354,3 @@ bindedFunc2(); // return 2 - the context for the inner run
otherFunc(); // return undefined

```



18 changes: 18 additions & 0 deletions src/als/als.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,24 @@ export class ALS<T> implements Context<T> {
return this.storage.getStore();
}

/**
* Disables the instqance of ALS
* @returns void
*/
disable(): void {
this.storage.disable();
}

/**
* Runs a function synchronously outside of a context and returns its return value
* @param {(...args:any[])=>R} callback Function that will be the boundary of the said context, anything set to be run from within the callback will have the same context
* @param {any[]} ...args Option arguments to be passed to be passed to callback
* @returns R callback's return value
*/
exit<R>(callback: (...args: any[]) => R, ...args: any[]): R {
return this.storage.exit(callback, ...args);
}

// Provide empty Map by default
/**
* Start the boundary of a context, anything set to be run from within the callback will have the same context
Expand Down
77 changes: 49 additions & 28 deletions src/cls/cls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export class CLS<T> implements Context<T> {
enabled: boolean;
kResourceStore: number;
active: any;
_hook: async_hooks.AsyncHook;
_contexts: Map<any, any>;

constructor() {
Expand Down Expand Up @@ -47,39 +48,43 @@ export class CLS<T> implements Context<T> {
this._enable();
}

/**
* Disable the instance of ALS.
* @returns void
*/
disable(): void {
if (this.enabled) {
this.enabled = false;
//disable hook
this._hook.disable();
}
}

/**
* Runs a function synchronously outside of a context and returns its return value
* @param {(...args:any[])=>R} callback Function that will be the boundary of the said context, anything set to be run from within the callback will have the same context
* @param {any[]} ...args Option arguments to be passed to be passed to callback
* @returns R callback's return value
*/
exit<R>(callback: (...args: any[]) => R, ...args: any[]): R {
if (!this.enabled) {
return Reflect.apply(callback, null, args);
}
this.disable();
try {
return Reflect.apply(callback, null, args);
} finally {
this._enable();
}
}

_enable(): void {
if (!this.enabled) {
this.enabled = true;
// eslint-disable-next-line @typescript-eslint/no-this-alias
const self = this;
const storageHook = async_hooks.createHook({
// eslint-disable-next-line @typescript-eslint/ban-types, @typescript-eslint/no-unused-vars
init(asyncId: number, type: string, triggerAsyncId: number, resource: object) {
if (self.active) {
self._contexts.set(asyncId, self.active[self.kResourceStore]);
} else {
self._contexts.set(asyncId, self._contexts.get(triggerAsyncId));
}
},
// before(asyncId) {
// },
// after(asyncId) {
// // debug2('after', asyncId);
// // To be review, do we need contexts till it is destroyed?
// // This doesn't really help
// // self._contexts.delete(asyncId);
// },
destroy(asyncId: number) {
self._contexts.delete(asyncId);
// Do i need to clear self.active? if not set to null when exiting asyncResource.runInAsyncScope
// if (self.active && self.active.asyncId() === asyncId) {
// debug2('delete');
// self.active = null;
// }
// debug2('destroy', asyncId);
},
});
storageHook.enable();
this._hook = storageHook(self);
this._hook.enable();
}
}

Expand Down Expand Up @@ -189,6 +194,22 @@ export class CLS<T> implements Context<T> {
}
}

const storageHook = (self: CLS<any>) => {
return async_hooks.createHook({
// eslint-disable-next-line @typescript-eslint/ban-types, @typescript-eslint/no-unused-vars
init(asyncId: number, type: string, triggerAsyncId: number, resource: object) {
if (self.active) {
self._contexts.set(asyncId, self.active[self.kResourceStore]);
} else {
self._contexts.set(asyncId, self._contexts.get(triggerAsyncId));
}
},
destroy(asyncId: number) {
self._contexts.delete(asyncId);
},
});
};

export default CLS;
// const fs = require('fs');
// function debug2(...args: any[]) {
Expand Down
63 changes: 63 additions & 0 deletions tests/als/als.enable-disable.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
const ALS = require('../../dist/als/als').default;

describe('ALS enable and disable tests', () => {
test('Nesting of disable and exit should work as intended', () => {
const store = new ALS();
store.run({}, () => {
store.set('foo', 'bar');
process.nextTick(() => {
expect(store.get('foo')).toEqual('bar');
process.nextTick(() => {
expect(store.getStore()).toBeUndefined();
});

store.disable();
expect(store.getStore()).toBeUndefined();

// Calls to exit() should not mess with enabled status
store.exit(() => {
expect(store.getStore()).toBeUndefined();
});
expect(store.getStore()).toBeUndefined();

process.nextTick(() => {
expect(store.getStore()).toBeUndefined();
store.run({ bar: 'foo' }, () => {
expect(store.get('bar')).toEqual('foo');
});
});
});
});
});

test('Exit should work as intended', () => {
const store = new ALS();
store.run({}, () => {
store.set('foo', 'bar');
store.exit(() => {
expect(store.getStore()).toBeUndefined();
});
expect(store.get('foo')).toEqual('bar');
});
});

test('Exit should able to run when not inside run', () => {
const store = new ALS();
const spyFn = jest.fn();
store.exit(() => {
spyFn();
expect(store.getStore()).toBeUndefined();
});
expect(spyFn).toBeCalledTimes(1);
});

test('Disable should work as intended', () => {
const store = new ALS();
store.run({}, () => {
store.set('foo', 'bar');
store.disable();
expect(store.getStore()).toBeUndefined();
});
store.disable(); // Wont trigger any error
});
});
64 changes: 64 additions & 0 deletions tests/cls/cls.enable-disable.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
const CLS = require('../../dist/cls/cls').default;

describe('CLS enable and disable tests', () => {
test('Nesting of disable and exit should work as intended', () => {
const store = new CLS();
store.run({}, () => {
store.set('foo', 'bar');
process.nextTick(() => {
expect(store.get('foo')).toEqual('bar');
process.nextTick(() => {
expect(store.getStore()).toBeUndefined();
});

store.disable();
expect(store.getStore()).toBeUndefined();

// Calls to exit() should not mess with enabled status
store.exit(() => {
expect(store.getStore()).toBeUndefined();
});
expect(store.getStore()).toBeUndefined();

process.nextTick(() => {
expect(store.getStore()).toBeUndefined();
store.run({ bar: 'foo' }, () => {
expect(store.get('bar')).toEqual('foo');
});
});
});
});
});

test('Exit should work as intended', () => {
const CLS = require('../../dist/cls/cls').default;
const store = new CLS();
store.run({}, () => {
store.set('foo', 'bar');
store.exit(() => {
expect(store.getStore()).toBeUndefined();
});
expect(store.get('foo')).toEqual('bar');
});
});

test('Exit should able to run when not inside run', () => {
const store = new CLS();
const spyFn = jest.fn();
store.exit(() => {
spyFn();
expect(store.getStore()).toBeUndefined();
});
expect(spyFn).toBeCalledTimes(1);
});

test('Disable should work as intended', () => {
const store = new CLS();
store.run({}, () => {
store.set('foo', 'bar');
store.disable();
expect(store.getStore()).toBeUndefined();
});
store.disable(); // Wont trigger any error
});
});

0 comments on commit 1ab9abd

Please sign in to comment.