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.
@@ -0,0 +1,57 @@
1
+ import { patchTimers, unpatchTimers } from "./timers";
2
+ import { patchXHR, unpatchXHR } from "./xhr";
3
+ import { patchPromise, unpatchPromise } from "./promise";
4
+ import { patchMicrotasks, unpatchMicrotasks } from "./microtasks";
5
+ import { patchObservers, unpatchObservers } from "./observers";
6
+ import { patchEventTarget, unpatchEventTarget } from "./event-target";
7
+
8
+ let patched = false;
9
+
10
+ /**
11
+ * Apply all browser API patches to enable async context propagation.
12
+ * This function is idempotent - it will only patch once even if called multiple times.
13
+ *
14
+ * @returns A function that can be called to remove all patches
15
+ */
16
+ export function patchAll(): () => void {
17
+ if (patched) {
18
+ return () => {}; // Already patched, return no-op
19
+ }
20
+ patched = true;
21
+
22
+ // Patch Promise continuation methods (.then, .catch, .finally)
23
+ patchPromise();
24
+
25
+ // Patch microtask scheduling
26
+ patchMicrotasks();
27
+
28
+ // Patch generic EventTarget.addEventListener (covers most event-based APIs)
29
+ patchEventTarget();
30
+
31
+ // Patch timer functions (setTimeout, setInterval, etc.)
32
+ patchTimers();
33
+
34
+ // Patch XHR on* properties (addEventListener is covered by EventTarget patch)
35
+ patchXHR();
36
+
37
+ // Patch Observer APIs (MutationObserver, ResizeObserver, etc.)
38
+ patchObservers();
39
+
40
+ return unpatchAll;
41
+ }
42
+
43
+ /**
44
+ * Remove all browser API patches, restoring original behavior.
45
+ * This function is idempotent - it will only unpatch once even if called multiple times.
46
+ */
47
+ export function unpatchAll(): void {
48
+ if (!patched) return;
49
+ patched = false;
50
+
51
+ unpatchObservers();
52
+ unpatchXHR();
53
+ unpatchTimers();
54
+ unpatchEventTarget();
55
+ unpatchMicrotasks();
56
+ unpatchPromise();
57
+ }
@@ -0,0 +1,232 @@
1
+ import { describe, it, expect, beforeEach } from "vitest";
2
+ import { AsyncLocalStorage } from "../async-local-storage";
3
+ import { AsyncContextFrame } from "../async-context-frame";
4
+ import { patchMicrotasks } from "./microtasks";
5
+ import { patchTimers } from "./timers";
6
+
7
+ describe("Microtasks patch", () => {
8
+ let als: AsyncLocalStorage<number>;
9
+
10
+ beforeEach(() => {
11
+ AsyncContextFrame.set(undefined);
12
+ als = new AsyncLocalStorage<number>();
13
+ patchMicrotasks();
14
+ patchTimers(); // Needed for tests that use setTimeout
15
+ });
16
+
17
+ describe("queueMicrotask", () => {
18
+ it("should preserve context through queueMicrotask", async () => {
19
+ const promise = new Promise<number | undefined>((resolve) => {
20
+ als.run(100, () => {
21
+ queueMicrotask(() => {
22
+ resolve(als.getStore());
23
+ });
24
+ });
25
+ });
26
+
27
+ const result = await promise;
28
+ expect(result).toBe(100);
29
+ });
30
+
31
+ it("should preserve context through chained queueMicrotask calls", async () => {
32
+ const stores: (number | undefined)[] = [];
33
+
34
+ const promise = new Promise<void>((resolve) => {
35
+ als.run(200, () => {
36
+ stores.push(als.getStore());
37
+ queueMicrotask(() => {
38
+ stores.push(als.getStore());
39
+ queueMicrotask(() => {
40
+ stores.push(als.getStore());
41
+ queueMicrotask(() => {
42
+ stores.push(als.getStore());
43
+ resolve();
44
+ });
45
+ });
46
+ });
47
+ });
48
+ });
49
+
50
+ await promise;
51
+ expect(stores).toEqual([200, 200, 200, 200]);
52
+ });
53
+
54
+ it("should isolate contexts in parallel queueMicrotask calls", async () => {
55
+ const results: number[] = [];
56
+
57
+ const promises = [1, 2, 3, 4, 5].map((value) => {
58
+ return new Promise<void>((resolve) => {
59
+ als.run(value, () => {
60
+ queueMicrotask(() => {
61
+ results.push(als.getStore()!);
62
+ resolve();
63
+ });
64
+ });
65
+ });
66
+ });
67
+
68
+ await Promise.all(promises);
69
+ expect(results.sort()).toEqual([1, 2, 3, 4, 5]);
70
+ });
71
+
72
+ it("should preserve context when microtask updates context", async () => {
73
+ const sequence: number[] = [];
74
+
75
+ const promise = new Promise<void>((resolve) => {
76
+ als.run(300, () => {
77
+ sequence.push(als.getStore()!);
78
+ queueMicrotask(() => {
79
+ sequence.push(als.getStore()!);
80
+ als.enterWith(400);
81
+ sequence.push(als.getStore()!);
82
+ queueMicrotask(() => {
83
+ sequence.push(als.getStore()!);
84
+ resolve();
85
+ });
86
+ });
87
+ });
88
+ });
89
+
90
+ await promise;
91
+ expect(sequence).toEqual([300, 300, 400, 400]);
92
+ });
93
+
94
+ it("should preserve context through microtasks scheduled from different contexts", async () => {
95
+ const results: Array<{ source: string; store: number | undefined }> = [];
96
+
97
+ const promise1 = new Promise<void>((resolve) => {
98
+ als.run(500, () => {
99
+ queueMicrotask(() => {
100
+ results.push({ source: "first", store: als.getStore() });
101
+ resolve();
102
+ });
103
+ });
104
+ });
105
+
106
+ const promise2 = new Promise<void>((resolve) => {
107
+ als.run(600, () => {
108
+ queueMicrotask(() => {
109
+ results.push({ source: "second", store: als.getStore() });
110
+ resolve();
111
+ });
112
+ });
113
+ });
114
+
115
+ await Promise.all([promise1, promise2]);
116
+
117
+ const first = results.find((r) => r.source === "first");
118
+ const second = results.find((r) => r.source === "second");
119
+
120
+ expect(first?.store).toBe(500);
121
+ expect(second?.store).toBe(600);
122
+ });
123
+
124
+ it("should preserve context through multiple queueMicrotask calls in sequence", async () => {
125
+ const sequence: Array<{ step: string; store: number | undefined }> = [];
126
+
127
+ const promise = new Promise<void>((resolve) => {
128
+ als.run(700, () => {
129
+ sequence.push({ step: "start", store: als.getStore() });
130
+
131
+ queueMicrotask(() => {
132
+ sequence.push({ step: "microtask1", store: als.getStore() });
133
+
134
+ queueMicrotask(() => {
135
+ sequence.push({ step: "microtask2", store: als.getStore() });
136
+
137
+ queueMicrotask(() => {
138
+ sequence.push({ step: "microtask3", store: als.getStore() });
139
+ resolve();
140
+ });
141
+ });
142
+ });
143
+ });
144
+ });
145
+
146
+ await promise;
147
+
148
+ expect(sequence).toEqual([
149
+ { step: "start", store: 700 },
150
+ { step: "microtask1", store: 700 },
151
+ { step: "microtask2", store: 700 },
152
+ { step: "microtask3", store: 700 },
153
+ ]);
154
+ });
155
+
156
+ it("should preserve context when queueMicrotask is called outside of run()", async () => {
157
+ const promise = new Promise<number | undefined>((resolve) => {
158
+ als.run(800, () => {
159
+ // Call queueMicrotask
160
+ queueMicrotask(() => {
161
+ resolve(als.getStore());
162
+ });
163
+ });
164
+ });
165
+
166
+ const result = await promise;
167
+ expect(result).toBe(800);
168
+ });
169
+
170
+ it("should execute microtasks before next macrotask", async () => {
171
+ const sequence: string[] = [];
172
+
173
+ const promise = new Promise<void>((resolve) => {
174
+ als.run(900, () => {
175
+ sequence.push("run");
176
+
177
+ setTimeout(() => {
178
+ sequence.push(`timeout:${als.getStore()}`);
179
+ resolve();
180
+ }, 10);
181
+
182
+ queueMicrotask(() => {
183
+ sequence.push(`microtask:${als.getStore()}`);
184
+ });
185
+
186
+ sequence.push("run-end");
187
+ });
188
+ });
189
+
190
+ await promise;
191
+
192
+ // Microtask should execute before timeout
193
+ expect(sequence[0]).toBe("run");
194
+ expect(sequence[1]).toBe("run-end");
195
+ expect(sequence[2]).toBe("microtask:900");
196
+ expect(sequence[3]).toBe("timeout:900");
197
+ });
198
+
199
+
200
+ it("should preserve context across mix of microtasks and macrotasks", async () => {
201
+ const sequence: string[] = [];
202
+
203
+ const promise = new Promise<void>((resolve) => {
204
+ als.run(1100, () => {
205
+ sequence.push(`start:${als.getStore()}`);
206
+
207
+ queueMicrotask(() => {
208
+ sequence.push(`microtask1:${als.getStore()}`);
209
+
210
+ setTimeout(() => {
211
+ sequence.push(`timeout:${als.getStore()}`);
212
+
213
+ queueMicrotask(() => {
214
+ sequence.push(`microtask2:${als.getStore()}`);
215
+ resolve();
216
+ });
217
+ }, 10);
218
+ });
219
+ });
220
+ });
221
+
222
+ await promise;
223
+
224
+ expect(sequence).toEqual([
225
+ "start:1100",
226
+ "microtask1:1100",
227
+ "timeout:1100",
228
+ "microtask2:1100",
229
+ ]);
230
+ });
231
+ });
232
+ });
@@ -0,0 +1,31 @@
1
+ import { AsyncLocalStorage } from "../async-local-storage";
2
+ import { patch, unpatch } from "./patch-helper";
3
+
4
+ /**
5
+ * Patch queueMicrotask to preserve async context.
6
+ * This ensures that microtasks scheduled via queueMicrotask
7
+ * maintain their async context when they execute.
8
+ */
9
+ export function patchMicrotasks(): void {
10
+ if (!globalThis.queueMicrotask) return;
11
+
12
+ patch(
13
+ globalThis as any,
14
+ "queueMicrotask",
15
+ (original: typeof queueMicrotask) => {
16
+ return function (callback: VoidFunction) {
17
+ const bound = AsyncLocalStorage.bind(callback);
18
+ return original(bound);
19
+ };
20
+ }
21
+ );
22
+ }
23
+
24
+ /**
25
+ * Remove the queueMicrotask patch, restoring original behavior.
26
+ */
27
+ export function unpatchMicrotasks(): void {
28
+ if (typeof globalThis.queueMicrotask !== "undefined") {
29
+ unpatch(globalThis as any, "queueMicrotask");
30
+ }
31
+ }