@hg-ts/async-context 0.5.17 → 0.5.18

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hg-ts/async-context",
3
- "version": "0.5.17",
3
+ "version": "0.5.18",
4
4
  "main": "dist/index.js",
5
5
  "type": "module",
6
6
  "exports": {
@@ -18,11 +18,11 @@
18
18
  "test:dev": "vitest watch"
19
19
  },
20
20
  "devDependencies": {
21
- "@hg-ts-config/typescript": "0.5.17",
22
- "@hg-ts/exception": "0.5.17",
23
- "@hg-ts/linter": "0.5.17",
24
- "@hg-ts/tests": "0.5.17",
25
- "@hg-ts/types": "0.5.17",
21
+ "@hg-ts-config/typescript": "0.5.18",
22
+ "@hg-ts/exception": "0.5.18",
23
+ "@hg-ts/linter": "0.5.18",
24
+ "@hg-ts/tests": "0.5.18",
25
+ "@hg-ts/types": "0.5.18",
26
26
  "@types/node": "22.19.1",
27
27
  "@vitest/coverage-v8": "4.0.14",
28
28
  "eslint": "9.18.0",
@@ -35,7 +35,7 @@
35
35
  "vitest": "4.0.14"
36
36
  },
37
37
  "peerDependencies": {
38
- "@hg-ts/exception": "0.5.17",
38
+ "@hg-ts/exception": "0.5.18",
39
39
  "reflect-metadata": "*",
40
40
  "rxjs": "*",
41
41
  "tslib": "*",
@@ -0,0 +1,7 @@
1
+ import { BaseException } from '@hg-ts/exception';
2
+
3
+ export class AsyncContextNotFoundException extends BaseException {
4
+ public constructor() {
5
+ super('Async context not provided');
6
+ }
7
+ }
@@ -0,0 +1,82 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import {
3
+ MonoTypeOperatorFunction,
4
+ Observable,
5
+ } from 'rxjs';
6
+ import { AsyncContextNotFoundException } from './async-context.not-found.exception.js';
7
+
8
+ type AsyncContextProviderCallback<T> = () => T;
9
+ type AsyncContextFactory<InputType, Context> = (input: InputType) => Context;
10
+
11
+ type NextCallback<T> = (value: T) => void;
12
+
13
+ export class AsyncContextProvider<Context> {
14
+ protected readonly storage = new AsyncLocalStorage<Context>();
15
+
16
+ public get(): Nullable<Context> {
17
+ return this.storage.getStore() ?? null;
18
+ }
19
+
20
+ public getOrFail(): NonNullable<Context> {
21
+ const context = this.get();
22
+
23
+ if (typeof context === 'undefined' || context === null) {
24
+ throw new AsyncContextNotFoundException();
25
+ }
26
+
27
+ return context as NonNullable<Context>;
28
+ }
29
+
30
+ public run<ReturnType>(
31
+ context: Context,
32
+ callback: AsyncContextProviderCallback<ReturnType>,
33
+ ): ReturnType {
34
+ return this.storage.run(context, callback);
35
+ }
36
+
37
+ public exit<ReturnType>(
38
+ callback: AsyncContextProviderCallback<ReturnType>,
39
+ ): ReturnType {
40
+ return this.storage.exit(callback);
41
+ }
42
+
43
+ public runOperator<T>(
44
+ factory?: AsyncContextFactory<T, Context>,
45
+ ): MonoTypeOperatorFunction<T> {
46
+ return this.createOperator<T>((next, value) => {
47
+ const context = factory ? factory(value) : value as any;
48
+ this.storage.run(context, next);
49
+ });
50
+ }
51
+
52
+ public exitOperator<T>(): MonoTypeOperatorFunction<T> {
53
+ return this.createOperator(next => this.storage.exit(next));
54
+ }
55
+
56
+ private createOperator<T>(onNext: (next: () => void, value: T) => void): MonoTypeOperatorFunction<any> {
57
+ return (source): Observable<T> => new Observable<T>(subscriber => {
58
+ const callNext: NextCallback<T> = input => {
59
+ const next = (): void => {
60
+ if (!subscriber.closed) {
61
+ subscriber.next(input);
62
+ }
63
+ };
64
+ onNext(next, input);
65
+ };
66
+ const callError: NextCallback<T> = input => {
67
+ const next = (): void => {
68
+ if (!subscriber.closed) {
69
+ subscriber.error(input);
70
+ }
71
+ };
72
+ onNext(next, input);
73
+ };
74
+
75
+ source.subscribe({
76
+ next: callNext,
77
+ error: callError,
78
+ complete: () => subscriber.complete(),
79
+ });
80
+ });
81
+ }
82
+ }
@@ -0,0 +1,161 @@
1
+ import {
2
+ Describe,
3
+ expect,
4
+ ExpectException,
5
+ Suite,
6
+ Test,
7
+ } from '@hg-ts/tests';
8
+ import {
9
+ Subject,
10
+ Subscription,
11
+ tap,
12
+ } from 'rxjs';
13
+
14
+ import { AsyncContextNotFoundException } from './async-context.not-found.exception.js';
15
+ import { AsyncContextProvider } from './async-context.provider.js';
16
+
17
+ type OperatorTestSubscription = {
18
+ subscription: Subscription;
19
+ subject: Subject<number>;
20
+ input: number;
21
+ context: number;
22
+ };
23
+
24
+ @Describe()
25
+ export class AsyncContextTest extends Suite {
26
+ private context: AsyncContextProvider<number>;
27
+
28
+ @Test()
29
+ public async emptyContext(): Promise<void> {
30
+ expect(this.context.get()).toBe(null);
31
+ }
32
+
33
+ @Test()
34
+ public async noPromiseContext(): Promise<void> {
35
+ const contextValue = Math.random();
36
+ const resultValue = Math.random();
37
+
38
+ const result = this.context.run(contextValue, () => {
39
+ expect(this.context.getOrFail()).toBe(contextValue);
40
+
41
+ this.context.exit(() => {
42
+ expect(this.context.get()).toBe(null);
43
+ });
44
+ return resultValue;
45
+ });
46
+
47
+ expect(result).toBe(resultValue);
48
+ }
49
+
50
+ @Test()
51
+ public async promiseContext(): Promise<void> {
52
+ const contextValue = Math.random();
53
+ const resultValue = Math.random();
54
+
55
+ const result = await this.context.run(contextValue, async() => {
56
+ expect(this.context.getOrFail()).toBe(contextValue);
57
+
58
+ await this.context.exit(async() => {
59
+ expect(this.context.get()).toBe(null);
60
+ });
61
+ return resultValue;
62
+ });
63
+
64
+ expect(result).toBe(resultValue);
65
+ }
66
+
67
+ @Test()
68
+ public async operatorContext(): Promise<void> {
69
+ const { subject, input } = this.createOperatorTestSubscription();
70
+
71
+ subject.next(input);
72
+ subject.complete();
73
+
74
+ expect.assertions(4);
75
+ }
76
+
77
+ @Test()
78
+ public async operatorWithFactory(): Promise<void> {
79
+ const { subject, input } = this.createOperatorTestSubscription(value => -value);
80
+
81
+ subject.next(input);
82
+ subject.complete();
83
+
84
+ expect.assertions(4);
85
+ }
86
+
87
+ @Test()
88
+ public async operatorWithErrorContext(): Promise<void> {
89
+ const { subject, input } = this.createOperatorTestSubscription();
90
+
91
+ subject.error(input);
92
+ subject.complete();
93
+
94
+ expect.assertions(4);
95
+ }
96
+
97
+ @Test()
98
+ public async operatorAfterUnsubscribe(): Promise<void> {
99
+ const { subject, input, subscription } = this.createOperatorTestSubscription();
100
+
101
+ subscription.unsubscribe();
102
+ subject.next(input);
103
+ subject.complete();
104
+
105
+ expect.assertions(2);
106
+ }
107
+
108
+ @Test()
109
+ public async operatorWithErrorAfterUnsubscribe(): Promise<void> {
110
+ const { subject, input, subscription } = this.createOperatorTestSubscription();
111
+
112
+ subscription.unsubscribe();
113
+ subject.error(input);
114
+ subject.complete();
115
+
116
+ expect.assertions(2);
117
+ }
118
+
119
+ @Test()
120
+ @ExpectException(AsyncContextNotFoundException)
121
+ public async contextNotFound(): Promise<void> {
122
+ this.context.getOrFail();
123
+ }
124
+
125
+ public override async beforeEach(): Promise<void> {
126
+ this.context = new AsyncContextProvider();
127
+ }
128
+
129
+ private createOperatorTestSubscription(factory?: (input: number) => number): OperatorTestSubscription {
130
+ const subject = new Subject<number>();
131
+ const input = Math.random();
132
+ const context = factory ? factory(input) : input;
133
+
134
+ const withContext = (value: number): void => {
135
+ expect(value).toBe(input);
136
+ expect(this.context.getOrFail()).toBe(context);
137
+ };
138
+
139
+ const withoutContext = (value: number): void => {
140
+ expect(value).toBe(input);
141
+ expect(this.context.get()).toBe(null);
142
+ };
143
+
144
+ const subscription = subject.asObservable()
145
+ .pipe(
146
+ this.context.runOperator(factory),
147
+ tap({
148
+ next: withContext,
149
+ error: withContext,
150
+ }),
151
+ this.context.exitOperator(),
152
+ tap({
153
+ next: withoutContext,
154
+ error: withoutContext,
155
+ }),
156
+ )
157
+ .subscribe({ next() {}, error() {} });
158
+
159
+ return { subject, subscription, input, context };
160
+ }
161
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from './async-context.provider.js';
2
+ export * from './async-context.not-found.exception.js';