als-browser 1.0.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/.claude/settings.local.json +10 -0
- package/README.md +257 -0
- package/package.json +40 -0
- package/src/async-context-frame.ts +53 -0
- package/src/async-local-storage.test.ts +264 -0
- package/src/async-local-storage.ts +111 -0
- package/src/index.ts +8 -0
- package/src/patches/event-target.test.ts +482 -0
- package/src/patches/event-target.ts +100 -0
- package/src/patches/index.ts +57 -0
- package/src/patches/microtasks.test.ts +232 -0
- package/src/patches/microtasks.ts +31 -0
- package/src/patches/observers.test.ts +594 -0
- package/src/patches/observers.ts +112 -0
- package/src/patches/patch-helper.ts +73 -0
- package/src/patches/promise.test.ts +355 -0
- package/src/patches/promise.ts +83 -0
- package/src/patches/timers.test.ts +321 -0
- package/src/patches/timers.ts +109 -0
- package/src/patches/xhr.test.ts +81 -0
- package/src/patches/xhr.ts +58 -0
- package/src/snapshot.test.ts +66 -0
- package/src/snapshot.ts +38 -0
- package/tsconfig.json +19 -0
- package/tsup.config.ts +12 -0
- package/vitest.config.ts +8 -0
package/README.md
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
# als-browser
|
|
2
|
+
|
|
3
|
+
Browser-compatible polyfill for Node.js's `AsyncLocalStorage` API. This package
|
|
4
|
+
enables async context propagation in browser environments by patching common
|
|
5
|
+
async browser APIs.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Full `AsyncLocalStorage` API compatibility
|
|
10
|
+
- Automatic patching of browser async APIs
|
|
11
|
+
- Zero dependencies (dev dependencies only)
|
|
12
|
+
- TypeScript support with full type definitions
|
|
13
|
+
- ESM and CommonJS builds
|
|
14
|
+
- Comprehensive test coverage
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install als-browser
|
|
20
|
+
# or
|
|
21
|
+
pnpm add als-browser
|
|
22
|
+
# or
|
|
23
|
+
yarn add als-browser
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
import { AsyncLocalStorage } from 'als-browser';
|
|
30
|
+
|
|
31
|
+
// Create a storage instance
|
|
32
|
+
const requestContext = new AsyncLocalStorage<{ userId: string }>();
|
|
33
|
+
|
|
34
|
+
// Use run() to execute code in a context
|
|
35
|
+
requestContext.run({ userId: '123' }, () => {
|
|
36
|
+
console.log(requestContext.getStore()); // { userId: '123' }
|
|
37
|
+
|
|
38
|
+
// Context is preserved through setTimeout
|
|
39
|
+
setTimeout(() => {
|
|
40
|
+
console.log(requestContext.getStore()); // { userId: '123' }
|
|
41
|
+
}, 100);
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## API
|
|
46
|
+
|
|
47
|
+
### `AsyncLocalStorage<T>`
|
|
48
|
+
|
|
49
|
+
#### `constructor(options?)`
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
const store = new AsyncLocalStorage<T>({
|
|
53
|
+
defaultValue?: T, // Optional default value
|
|
54
|
+
name?: string // Optional name for debugging
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
#### `run(data, callback, ...args)`
|
|
59
|
+
|
|
60
|
+
Run a function in a new async context with the given data.
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
const result = store.run(myData, () => {
|
|
64
|
+
// Your code here
|
|
65
|
+
return store.getStore(); // Returns myData
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
#### `getStore()`
|
|
70
|
+
|
|
71
|
+
Get the current value from this store.
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
const currentValue = store.getStore();
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
#### `enterWith(data)`
|
|
78
|
+
|
|
79
|
+
Enter a new async context with the given data (no callback).
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
store.enterWith(myData);
|
|
83
|
+
console.log(store.getStore()); // myData
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
#### `exit(callback, ...args)`
|
|
87
|
+
|
|
88
|
+
Run a function with the store value set to undefined.
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
store.exit(() => {
|
|
92
|
+
console.log(store.getStore()); // undefined
|
|
93
|
+
});
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
#### `disable()`
|
|
97
|
+
|
|
98
|
+
Remove this store from the current async context.
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
store.disable();
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
#### Static: `bind(fn)`
|
|
105
|
+
|
|
106
|
+
Bind a function to the current async context.
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
const boundFn = AsyncLocalStorage.bind(() => {
|
|
110
|
+
return store.getStore();
|
|
111
|
+
});
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
#### Static: `snapshot()`
|
|
115
|
+
|
|
116
|
+
Capture the current async context and return a function that can restore it.
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
const snapshot = AsyncLocalStorage.snapshot();
|
|
120
|
+
snapshot(() => {
|
|
121
|
+
// Runs in captured context
|
|
122
|
+
});
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Manual Context Propagation
|
|
126
|
+
|
|
127
|
+
For advanced use cases like code transformers or custom async instrumentation, you can manually capture and restore async context around `await` points.
|
|
128
|
+
|
|
129
|
+
#### `capture(container, promise)`
|
|
130
|
+
|
|
131
|
+
Capture the current async context frame before an await and store it in a container object.
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
import { capture, restore, SnapshotContainer } from 'als-browser';
|
|
135
|
+
|
|
136
|
+
const container: SnapshotContainer = {};
|
|
137
|
+
const result = restore(container, await capture(container, promise));
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
#### `restore(container, value)`
|
|
141
|
+
|
|
142
|
+
Restore the async context frame after an await from the container object.
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
// Transform: await foo()
|
|
146
|
+
// Into: restore(container, await capture(container, foo()))
|
|
147
|
+
|
|
148
|
+
const container: SnapshotContainer = {};
|
|
149
|
+
store.run(myData, async () => {
|
|
150
|
+
// Manually preserve context across await
|
|
151
|
+
restore(container, await capture(container, asyncOperation()));
|
|
152
|
+
console.log(store.getStore()); // myData is preserved
|
|
153
|
+
});
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
These functions are primarily useful for:
|
|
157
|
+
- Code transformers/compilers that automatically instrument async functions
|
|
158
|
+
- Custom async context tracking systems
|
|
159
|
+
- Debugging and understanding async context flow
|
|
160
|
+
|
|
161
|
+
**Note**: For normal application code, prefer using the automatic patches or `AsyncLocalStorage.bind()`/`snapshot()`.
|
|
162
|
+
|
|
163
|
+
## Patched Browser APIs
|
|
164
|
+
|
|
165
|
+
The following browser APIs are automatically patched to preserve async context:
|
|
166
|
+
|
|
167
|
+
### Timers
|
|
168
|
+
- `setTimeout`
|
|
169
|
+
- `setInterval`
|
|
170
|
+
- `setImmediate` (if available)
|
|
171
|
+
|
|
172
|
+
### Animation
|
|
173
|
+
- `requestAnimationFrame`
|
|
174
|
+
- `requestIdleCallback`
|
|
175
|
+
|
|
176
|
+
### Network
|
|
177
|
+
- `XMLHttpRequest` event handlers (addEventListener and on* properties)
|
|
178
|
+
|
|
179
|
+
## How It Works
|
|
180
|
+
|
|
181
|
+
This package implements Node.js's `AsyncContextFrame` model adapted for browsers:
|
|
182
|
+
|
|
183
|
+
1. **AsyncContextFrame**: A Map-based storage for async context, stored in a module-level variable
|
|
184
|
+
2. **AsyncLocalStorage**: The main API that stores and retrieves values from the current frame
|
|
185
|
+
3. **Browser API Patches**: Automatically wraps callbacks to preserve context across async boundaries
|
|
186
|
+
|
|
187
|
+
The implementation replaces Node.js's V8 embedder data APIs with a simple module-level variable, making it work in any JavaScript environment.
|
|
188
|
+
|
|
189
|
+
## Limitations
|
|
190
|
+
|
|
191
|
+
- **Promise-based APIs**: This package does not automatically patch promise-based APIs like `fetch()`. For those, you need to manually propagate context using `AsyncLocalStorage.bind()` or `AsyncLocalStorage.snapshot()`.
|
|
192
|
+
- **EventTarget.addEventListener**: Only `XMLHttpRequest` is patched. Other event targets may need manual context propagation.
|
|
193
|
+
- **Module-level state**: The context is stored in a module-level variable, which means it's shared across all code in the same JavaScript realm.
|
|
194
|
+
|
|
195
|
+
## Example: Request Tracing
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
import { AsyncLocalStorage } from 'als-browser';
|
|
199
|
+
|
|
200
|
+
const requestId = new AsyncLocalStorage<string>();
|
|
201
|
+
|
|
202
|
+
function generateId() {
|
|
203
|
+
return Math.random().toString(36).slice(2);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function log(message: string) {
|
|
207
|
+
const id = requestId.getStore() || 'no-context';
|
|
208
|
+
console.log(`[${id}] ${message}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Start a request
|
|
212
|
+
requestId.run(generateId(), async () => {
|
|
213
|
+
log('Request started');
|
|
214
|
+
|
|
215
|
+
// Context preserved through setTimeout
|
|
216
|
+
setTimeout(() => {
|
|
217
|
+
log('Async operation 1');
|
|
218
|
+
}, 100);
|
|
219
|
+
|
|
220
|
+
// Context preserved through requestAnimationFrame
|
|
221
|
+
requestAnimationFrame(() => {
|
|
222
|
+
log('Animation frame');
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// For fetch, manually bind
|
|
226
|
+
const boundHandler = AsyncLocalStorage.bind(async () => {
|
|
227
|
+
const response = await fetch('/api/data');
|
|
228
|
+
log('Fetch completed');
|
|
229
|
+
return response.json();
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
await boundHandler();
|
|
233
|
+
});
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## Testing
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
# Run tests
|
|
240
|
+
pnpm test
|
|
241
|
+
|
|
242
|
+
# Build
|
|
243
|
+
pnpm build
|
|
244
|
+
|
|
245
|
+
# Type check
|
|
246
|
+
pnpm typecheck
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## License
|
|
250
|
+
|
|
251
|
+
MIT
|
|
252
|
+
|
|
253
|
+
## Credits
|
|
254
|
+
|
|
255
|
+
This implementation is based on Node.js's `AsyncLocalStorage` and `AsyncContextFrame` APIs:
|
|
256
|
+
- `lib/internal/async_context_frame.js`
|
|
257
|
+
- `lib/internal/async_local_storage/async_context_frame.js`
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "als-browser",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Browser polyfill for Node.js AsyncLocalStorage",
|
|
5
|
+
"author": "Stephen Belanger <admin@stephenbelanger.com>",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"module": "./dist/index.mjs",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
"./package.json": "./package.json",
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.mjs",
|
|
15
|
+
"module": "./dist/index.mjs",
|
|
16
|
+
"require": "./dist/index.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsup",
|
|
21
|
+
"watch": "tsup --watch",
|
|
22
|
+
"test": "vitest run",
|
|
23
|
+
"lint": "eslint src --ext .ts",
|
|
24
|
+
"typecheck": "tsc --noEmit"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/node": "^20.0.0",
|
|
28
|
+
"happy-dom": "^12.0.0",
|
|
29
|
+
"tsup": "^8.0.0",
|
|
30
|
+
"typescript": "^5.3.0",
|
|
31
|
+
"vitest": "^1.0.0"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"async-local-storage",
|
|
35
|
+
"context",
|
|
36
|
+
"async-context",
|
|
37
|
+
"browser",
|
|
38
|
+
"polyfill"
|
|
39
|
+
]
|
|
40
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { AsyncLocalStorage } from "./async-local-storage";
|
|
2
|
+
|
|
3
|
+
// Use a global Symbol to coordinate between ESM and CJS builds
|
|
4
|
+
const CURRENT_FRAME_SYMBOL = Symbol.for("als-browser:currentFrame");
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* AsyncContextFrame is a Map-based storage for async context.
|
|
8
|
+
* In Node.js, this uses V8's continuation preserved embedder data.
|
|
9
|
+
* In the browser, we use a global Symbol to coordinate between ESM/CJS builds.
|
|
10
|
+
*/
|
|
11
|
+
export class AsyncContextFrame extends Map<AsyncLocalStorage<any>, any> {
|
|
12
|
+
constructor(store: AsyncLocalStorage<any>, data: any) {
|
|
13
|
+
super(AsyncContextFrame.current());
|
|
14
|
+
this.set(store, data);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
disable(store: AsyncLocalStorage<any>) {
|
|
18
|
+
this.delete(store);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get the current async context frame.
|
|
23
|
+
*/
|
|
24
|
+
static current(): AsyncContextFrame | undefined {
|
|
25
|
+
return (globalThis as any)[CURRENT_FRAME_SYMBOL];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Set the current async context frame.
|
|
30
|
+
*/
|
|
31
|
+
static set(frame: AsyncContextFrame | undefined): void {
|
|
32
|
+
(globalThis as any)[CURRENT_FRAME_SYMBOL] = frame;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Exchange the current frame with a new one, returning the previous frame.
|
|
37
|
+
*/
|
|
38
|
+
static exchange(
|
|
39
|
+
frame: AsyncContextFrame | undefined
|
|
40
|
+
): AsyncContextFrame | undefined {
|
|
41
|
+
const prior = this.current();
|
|
42
|
+
this.set(frame);
|
|
43
|
+
return prior;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Disable (remove) a specific store from the current frame.
|
|
48
|
+
*/
|
|
49
|
+
static disable(store: AsyncLocalStorage<any>): void {
|
|
50
|
+
const frame = this.current();
|
|
51
|
+
frame?.disable(store);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { AsyncLocalStorage } from "./index";
|
|
3
|
+
import { AsyncContextFrame } from "./async-context-frame";
|
|
4
|
+
|
|
5
|
+
describe("AsyncLocalStorage", () => {
|
|
6
|
+
let als: AsyncLocalStorage<number>;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
// Reset the global context frame before each test
|
|
10
|
+
AsyncContextFrame.set(undefined);
|
|
11
|
+
als = new AsyncLocalStorage<number>();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe("Basic API", () => {
|
|
15
|
+
it("should return undefined when no store is set", () => {
|
|
16
|
+
expect(als.getStore()).toBeUndefined();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("should return default value when configured", () => {
|
|
20
|
+
const alsWithDefault = new AsyncLocalStorage<number>({
|
|
21
|
+
defaultValue: 42,
|
|
22
|
+
});
|
|
23
|
+
expect(alsWithDefault.getStore()).toBe(42);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("should support name option", () => {
|
|
27
|
+
const namedAls = new AsyncLocalStorage<number>({ name: "test-store" });
|
|
28
|
+
expect(namedAls.name).toBe("test-store");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("should preserve context in run()", () => {
|
|
32
|
+
const result = als.run(123, () => {
|
|
33
|
+
return als.getStore();
|
|
34
|
+
});
|
|
35
|
+
expect(result).toBe(123);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("should restore context after run() completes", () => {
|
|
39
|
+
expect(als.getStore()).toBeUndefined();
|
|
40
|
+
als.run(123, () => {
|
|
41
|
+
expect(als.getStore()).toBe(123);
|
|
42
|
+
});
|
|
43
|
+
expect(als.getStore()).toBeUndefined();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("should handle nested run() calls", () => {
|
|
47
|
+
als.run(1, () => {
|
|
48
|
+
expect(als.getStore()).toBe(1);
|
|
49
|
+
als.run(2, () => {
|
|
50
|
+
expect(als.getStore()).toBe(2);
|
|
51
|
+
als.run(3, () => {
|
|
52
|
+
expect(als.getStore()).toBe(3);
|
|
53
|
+
});
|
|
54
|
+
expect(als.getStore()).toBe(2);
|
|
55
|
+
});
|
|
56
|
+
expect(als.getStore()).toBe(1);
|
|
57
|
+
});
|
|
58
|
+
expect(als.getStore()).toBeUndefined();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("should support enterWith()", () => {
|
|
62
|
+
expect(als.getStore()).toBeUndefined();
|
|
63
|
+
als.enterWith(456);
|
|
64
|
+
expect(als.getStore()).toBe(456);
|
|
65
|
+
als.enterWith(789);
|
|
66
|
+
expect(als.getStore()).toBe(789);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("should support exit()", () => {
|
|
70
|
+
als.enterWith(100);
|
|
71
|
+
expect(als.getStore()).toBe(100);
|
|
72
|
+
const result = als.exit(() => {
|
|
73
|
+
expect(als.getStore()).toBeUndefined();
|
|
74
|
+
return "exited";
|
|
75
|
+
});
|
|
76
|
+
expect(result).toBe("exited");
|
|
77
|
+
expect(als.getStore()).toBe(100);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("should support disable()", () => {
|
|
81
|
+
als.enterWith(200);
|
|
82
|
+
expect(als.getStore()).toBe(200);
|
|
83
|
+
als.disable();
|
|
84
|
+
expect(als.getStore()).toBeUndefined();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("Static methods", () => {
|
|
89
|
+
it("should bind function to current context", () => {
|
|
90
|
+
let capturedValue: number | undefined;
|
|
91
|
+
|
|
92
|
+
als.run(999, () => {
|
|
93
|
+
const bound = AsyncLocalStorage.bind(() => {
|
|
94
|
+
capturedValue = als.getStore();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Call bound function outside of run context
|
|
98
|
+
AsyncContextFrame.set(undefined);
|
|
99
|
+
bound();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
expect(capturedValue).toBe(999);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("should preserve 'this' context in bound functions", () => {
|
|
106
|
+
const obj = {
|
|
107
|
+
value: 42,
|
|
108
|
+
getValue(this: { value: number }) {
|
|
109
|
+
return this.value;
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const bound = AsyncLocalStorage.bind(obj.getValue);
|
|
114
|
+
expect(bound.call(obj)).toBe(42);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("should support snapshot()", () => {
|
|
118
|
+
const snapshot = als.run(555, () => {
|
|
119
|
+
return AsyncLocalStorage.snapshot();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Run snapshot outside of original context
|
|
123
|
+
AsyncContextFrame.set(undefined);
|
|
124
|
+
const result = snapshot(() => {
|
|
125
|
+
return als.getStore();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
expect(result).toBe(555);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
describe("Multiple stores", () => {
|
|
134
|
+
it("should isolate different AsyncLocalStorage instances", () => {
|
|
135
|
+
const als1 = new AsyncLocalStorage<string>();
|
|
136
|
+
const als2 = new AsyncLocalStorage<number>();
|
|
137
|
+
|
|
138
|
+
als1.run("hello", () => {
|
|
139
|
+
als2.run(42, () => {
|
|
140
|
+
expect(als1.getStore()).toBe("hello");
|
|
141
|
+
expect(als2.getStore()).toBe(42);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("should handle nested runs with multiple stores", () => {
|
|
147
|
+
const als1 = new AsyncLocalStorage<string>();
|
|
148
|
+
const als2 = new AsyncLocalStorage<number>();
|
|
149
|
+
|
|
150
|
+
als1.run("outer", () => {
|
|
151
|
+
als2.run(1, () => {
|
|
152
|
+
expect(als1.getStore()).toBe("outer");
|
|
153
|
+
expect(als2.getStore()).toBe(1);
|
|
154
|
+
|
|
155
|
+
als1.run("inner", () => {
|
|
156
|
+
als2.run(2, () => {
|
|
157
|
+
expect(als1.getStore()).toBe("inner");
|
|
158
|
+
expect(als2.getStore()).toBe(2);
|
|
159
|
+
});
|
|
160
|
+
expect(als1.getStore()).toBe("inner");
|
|
161
|
+
expect(als2.getStore()).toBe(1);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
expect(als1.getStore()).toBe("outer");
|
|
165
|
+
expect(als2.getStore()).toBe(1);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe("Edge cases", () => {
|
|
172
|
+
it("should handle run() with same value as current", () => {
|
|
173
|
+
als.enterWith(777);
|
|
174
|
+
const callCount = { count: 0 };
|
|
175
|
+
|
|
176
|
+
als.run(777, () => {
|
|
177
|
+
callCount.count++;
|
|
178
|
+
expect(als.getStore()).toBe(777);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
expect(callCount.count).toBe(1);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("should pass arguments to run() callback", () => {
|
|
185
|
+
const result = als.run(
|
|
186
|
+
100,
|
|
187
|
+
(a: number, b: string, c: boolean) => {
|
|
188
|
+
return { a, b, c, store: als.getStore() };
|
|
189
|
+
},
|
|
190
|
+
1,
|
|
191
|
+
"test",
|
|
192
|
+
true
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
expect(result).toEqual({ a: 1, b: "test", c: true, store: 100 });
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("should pass arguments to exit() callback", () => {
|
|
199
|
+
const result = als.exit(
|
|
200
|
+
(x: number, y: number) => {
|
|
201
|
+
return x + y;
|
|
202
|
+
},
|
|
203
|
+
5,
|
|
204
|
+
10
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
expect(result).toBe(15);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("should handle undefined as a valid store value", () => {
|
|
211
|
+
als.run(undefined as any, () => {
|
|
212
|
+
expect(als.getStore()).toBeUndefined();
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("should handle null as a valid store value", () => {
|
|
217
|
+
const alsNull = new AsyncLocalStorage<string | null>();
|
|
218
|
+
alsNull.run(null, () => {
|
|
219
|
+
expect(alsNull.getStore()).toBeNull();
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe("Context propagation", () => {
|
|
225
|
+
it("should propagate through chained setTimeout calls", async () => {
|
|
226
|
+
const sequence: number[] = [];
|
|
227
|
+
|
|
228
|
+
const promise = new Promise<void>((resolve) => {
|
|
229
|
+
als.run(1, () => {
|
|
230
|
+
sequence.push(als.getStore()!);
|
|
231
|
+
setTimeout(() => {
|
|
232
|
+
sequence.push(als.getStore()!);
|
|
233
|
+
setTimeout(() => {
|
|
234
|
+
sequence.push(als.getStore()!);
|
|
235
|
+
resolve();
|
|
236
|
+
}, 10);
|
|
237
|
+
}, 10);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
await promise;
|
|
242
|
+
expect(sequence).toEqual([1, 1, 1]);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("should isolate contexts in parallel setTimeout calls", async () => {
|
|
246
|
+
const results: number[] = [];
|
|
247
|
+
|
|
248
|
+
const promises = [1, 2, 3].map((value) => {
|
|
249
|
+
return new Promise<void>((resolve) => {
|
|
250
|
+
als.run(value, () => {
|
|
251
|
+
setTimeout(() => {
|
|
252
|
+
results.push(als.getStore()!);
|
|
253
|
+
resolve();
|
|
254
|
+
}, 10);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
await Promise.all(promises);
|
|
260
|
+
expect(results.sort()).toEqual([1, 2, 3]);
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
});
|