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
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { AsyncLocalStorage } from "../async-local-storage";
|
|
3
|
+
import { AsyncContextFrame } from "../async-context-frame";
|
|
4
|
+
import { patchObservers } from "./observers";
|
|
5
|
+
|
|
6
|
+
describe("Observers patch", () => {
|
|
7
|
+
let als: AsyncLocalStorage<number>;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
AsyncContextFrame.set(undefined);
|
|
11
|
+
als = new AsyncLocalStorage<number>();
|
|
12
|
+
patchObservers();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe("MutationObserver", () => {
|
|
16
|
+
it("should preserve context through MutationObserver", async () => {
|
|
17
|
+
if (typeof MutationObserver === "undefined") {
|
|
18
|
+
return; // Skip if not available
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const target = document.createElement("div");
|
|
22
|
+
document.body.appendChild(target);
|
|
23
|
+
|
|
24
|
+
const promise = new Promise<number | undefined>((resolve) => {
|
|
25
|
+
als.run(100, () => {
|
|
26
|
+
const observer = new MutationObserver(() => {
|
|
27
|
+
resolve(als.getStore());
|
|
28
|
+
observer.disconnect();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
observer.observe(target, { childList: true });
|
|
32
|
+
target.appendChild(document.createElement("span"));
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const result = await promise;
|
|
37
|
+
expect(result).toBe(100);
|
|
38
|
+
|
|
39
|
+
document.body.removeChild(target);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("should preserve context through multiple mutations", async () => {
|
|
43
|
+
if (typeof MutationObserver === "undefined") {
|
|
44
|
+
return; // Skip if not available
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const target = document.createElement("div");
|
|
48
|
+
document.body.appendChild(target);
|
|
49
|
+
const stores: (number | undefined)[] = [];
|
|
50
|
+
|
|
51
|
+
const promise = new Promise<void>((resolve) => {
|
|
52
|
+
als.run(200, () => {
|
|
53
|
+
let count = 0;
|
|
54
|
+
const observer = new MutationObserver(() => {
|
|
55
|
+
stores.push(als.getStore());
|
|
56
|
+
count++;
|
|
57
|
+
if (count >= 3) {
|
|
58
|
+
observer.disconnect();
|
|
59
|
+
resolve();
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
observer.observe(target, { childList: true });
|
|
64
|
+
target.appendChild(document.createElement("span"));
|
|
65
|
+
target.appendChild(document.createElement("div"));
|
|
66
|
+
target.appendChild(document.createElement("p"));
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
await promise;
|
|
71
|
+
expect(stores.length).toBeGreaterThanOrEqual(1);
|
|
72
|
+
stores.forEach((store) => {
|
|
73
|
+
expect(store).toBe(200);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
document.body.removeChild(target);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("should preserve context when observer is created in one context and fires in another", async () => {
|
|
80
|
+
if (typeof MutationObserver === "undefined") {
|
|
81
|
+
return; // Skip if not available
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const target = document.createElement("div");
|
|
85
|
+
document.body.appendChild(target);
|
|
86
|
+
|
|
87
|
+
const promise = new Promise<number | undefined>((resolve) => {
|
|
88
|
+
als.run(300, () => {
|
|
89
|
+
const observer = new MutationObserver(() => {
|
|
90
|
+
resolve(als.getStore());
|
|
91
|
+
observer.disconnect();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
observer.observe(target, { childList: true });
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Trigger mutation outside of run context
|
|
98
|
+
setTimeout(() => {
|
|
99
|
+
target.appendChild(document.createElement("span"));
|
|
100
|
+
}, 10);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const result = await promise;
|
|
104
|
+
expect(result).toBe(300);
|
|
105
|
+
|
|
106
|
+
document.body.removeChild(target);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("should receive mutation records", async () => {
|
|
110
|
+
if (typeof MutationObserver === "undefined") {
|
|
111
|
+
return; // Skip if not available
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const target = document.createElement("div");
|
|
115
|
+
document.body.appendChild(target);
|
|
116
|
+
|
|
117
|
+
const promise = new Promise<{
|
|
118
|
+
recordsCount: number;
|
|
119
|
+
store: number | undefined;
|
|
120
|
+
}>((resolve) => {
|
|
121
|
+
als.run(400, () => {
|
|
122
|
+
const observer = new MutationObserver((mutations) => {
|
|
123
|
+
resolve({
|
|
124
|
+
recordsCount: mutations.length,
|
|
125
|
+
store: als.getStore(),
|
|
126
|
+
});
|
|
127
|
+
observer.disconnect();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
observer.observe(target, { childList: true });
|
|
131
|
+
target.appendChild(document.createElement("span"));
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const result = await promise;
|
|
136
|
+
expect(result.recordsCount).toBeGreaterThan(0);
|
|
137
|
+
expect(result.store).toBe(400);
|
|
138
|
+
|
|
139
|
+
document.body.removeChild(target);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("should preserve class name as MutationObserver", () => {
|
|
143
|
+
if (typeof MutationObserver === "undefined") {
|
|
144
|
+
return; // Skip if not available
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
expect(MutationObserver.name).toBe("MutationObserver");
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe("ResizeObserver", () => {
|
|
152
|
+
it("should preserve context through ResizeObserver", async () => {
|
|
153
|
+
if (typeof ResizeObserver === "undefined") {
|
|
154
|
+
return; // Skip if not available
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const target = document.createElement("div");
|
|
158
|
+
document.body.appendChild(target);
|
|
159
|
+
|
|
160
|
+
const promise = new Promise<number | undefined>((resolve) => {
|
|
161
|
+
als.run(500, () => {
|
|
162
|
+
const observer = new ResizeObserver(() => {
|
|
163
|
+
resolve(als.getStore());
|
|
164
|
+
observer.disconnect();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
observer.observe(target);
|
|
168
|
+
// Trigger a resize by changing size
|
|
169
|
+
target.style.width = "100px";
|
|
170
|
+
target.style.height = "100px";
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// Give the observer time to trigger
|
|
175
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
176
|
+
const result = await Promise.race([
|
|
177
|
+
promise,
|
|
178
|
+
new Promise<undefined>((r) => setTimeout(() => r(undefined), 200)),
|
|
179
|
+
]);
|
|
180
|
+
|
|
181
|
+
// ResizeObserver might not be fully functional in test environment
|
|
182
|
+
if (result !== undefined) {
|
|
183
|
+
expect(result).toBe(500);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
document.body.removeChild(target);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("should receive resize entries", async () => {
|
|
190
|
+
if (typeof ResizeObserver === "undefined") {
|
|
191
|
+
return; // Skip if not available
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const target = document.createElement("div");
|
|
195
|
+
document.body.appendChild(target);
|
|
196
|
+
|
|
197
|
+
const promise = new Promise<{
|
|
198
|
+
hasEntries: boolean;
|
|
199
|
+
store: number | undefined;
|
|
200
|
+
}>((resolve) => {
|
|
201
|
+
als.run(600, () => {
|
|
202
|
+
const observer = new ResizeObserver((entries) => {
|
|
203
|
+
resolve({
|
|
204
|
+
hasEntries: entries.length > 0,
|
|
205
|
+
store: als.getStore(),
|
|
206
|
+
});
|
|
207
|
+
observer.disconnect();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
observer.observe(target);
|
|
211
|
+
target.style.width = "200px";
|
|
212
|
+
target.style.height = "200px";
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
217
|
+
const result = await Promise.race([
|
|
218
|
+
promise,
|
|
219
|
+
new Promise<{ hasEntries: boolean; store: undefined }>((r) =>
|
|
220
|
+
setTimeout(() => r({ hasEntries: false, store: undefined }), 200)
|
|
221
|
+
),
|
|
222
|
+
]);
|
|
223
|
+
|
|
224
|
+
if (result.store !== undefined) {
|
|
225
|
+
expect(result.hasEntries).toBe(true);
|
|
226
|
+
expect(result.store).toBe(600);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
document.body.removeChild(target);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("should preserve class name as ResizeObserver", () => {
|
|
233
|
+
if (typeof ResizeObserver === "undefined") {
|
|
234
|
+
return; // Skip if not available
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
expect(ResizeObserver.name).toBe("ResizeObserver");
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
describe("IntersectionObserver", () => {
|
|
242
|
+
it("should preserve context through IntersectionObserver", async () => {
|
|
243
|
+
if (typeof IntersectionObserver === "undefined") {
|
|
244
|
+
return; // Skip if not available
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const target = document.createElement("div");
|
|
248
|
+
document.body.appendChild(target);
|
|
249
|
+
|
|
250
|
+
const promise = new Promise<number | undefined>((resolve) => {
|
|
251
|
+
als.run(700, () => {
|
|
252
|
+
const observer = new IntersectionObserver(() => {
|
|
253
|
+
resolve(als.getStore());
|
|
254
|
+
observer.disconnect();
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
observer.observe(target);
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// Give the observer time to trigger
|
|
262
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
263
|
+
const result = await Promise.race([
|
|
264
|
+
promise,
|
|
265
|
+
new Promise<undefined>((r) => setTimeout(() => r(undefined), 200)),
|
|
266
|
+
]);
|
|
267
|
+
|
|
268
|
+
// IntersectionObserver might not trigger in test environment
|
|
269
|
+
if (result !== undefined) {
|
|
270
|
+
expect(result).toBe(700);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
document.body.removeChild(target);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("should preserve context with options", async () => {
|
|
277
|
+
if (typeof IntersectionObserver === "undefined") {
|
|
278
|
+
return; // Skip if not available
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const target = document.createElement("div");
|
|
282
|
+
document.body.appendChild(target);
|
|
283
|
+
|
|
284
|
+
const promise = new Promise<number | undefined>((resolve) => {
|
|
285
|
+
als.run(800, () => {
|
|
286
|
+
const observer = new IntersectionObserver(
|
|
287
|
+
() => {
|
|
288
|
+
resolve(als.getStore());
|
|
289
|
+
observer.disconnect();
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
threshold: 0.5,
|
|
293
|
+
}
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
observer.observe(target);
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
301
|
+
const result = await Promise.race([
|
|
302
|
+
promise,
|
|
303
|
+
new Promise<undefined>((r) => setTimeout(() => r(undefined), 200)),
|
|
304
|
+
]);
|
|
305
|
+
|
|
306
|
+
if (result !== undefined) {
|
|
307
|
+
expect(result).toBe(800);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
document.body.removeChild(target);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it("should receive intersection entries", async () => {
|
|
314
|
+
if (typeof IntersectionObserver === "undefined") {
|
|
315
|
+
return; // Skip if not available
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const target = document.createElement("div");
|
|
319
|
+
document.body.appendChild(target);
|
|
320
|
+
|
|
321
|
+
const promise = new Promise<{
|
|
322
|
+
hasEntries: boolean;
|
|
323
|
+
store: number | undefined;
|
|
324
|
+
}>((resolve) => {
|
|
325
|
+
als.run(900, () => {
|
|
326
|
+
const observer = new IntersectionObserver((entries) => {
|
|
327
|
+
resolve({
|
|
328
|
+
hasEntries: entries.length > 0,
|
|
329
|
+
store: als.getStore(),
|
|
330
|
+
});
|
|
331
|
+
observer.disconnect();
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
observer.observe(target);
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
339
|
+
const result = await Promise.race([
|
|
340
|
+
promise,
|
|
341
|
+
new Promise<{ hasEntries: boolean; store: undefined }>((r) =>
|
|
342
|
+
setTimeout(() => r({ hasEntries: false, store: undefined }), 200)
|
|
343
|
+
),
|
|
344
|
+
]);
|
|
345
|
+
|
|
346
|
+
if (result.store !== undefined) {
|
|
347
|
+
expect(result.hasEntries).toBe(true);
|
|
348
|
+
expect(result.store).toBe(900);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
document.body.removeChild(target);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it("should preserve class name as IntersectionObserver", () => {
|
|
355
|
+
if (typeof IntersectionObserver === "undefined") {
|
|
356
|
+
return; // Skip if not available
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
expect(IntersectionObserver.name).toBe("IntersectionObserver");
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
describe("PerformanceObserver", () => {
|
|
364
|
+
it("should preserve context through PerformanceObserver", async () => {
|
|
365
|
+
if (typeof PerformanceObserver === "undefined") {
|
|
366
|
+
return; // Skip if not available
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const promise = new Promise<number | undefined>((resolve) => {
|
|
370
|
+
als.run(1000, () => {
|
|
371
|
+
try {
|
|
372
|
+
const observer = new PerformanceObserver(() => {
|
|
373
|
+
resolve(als.getStore());
|
|
374
|
+
observer.disconnect();
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
observer.observe({ entryTypes: ["measure"] });
|
|
378
|
+
|
|
379
|
+
// Create a performance measure to trigger the observer
|
|
380
|
+
performance.mark("test-start");
|
|
381
|
+
performance.mark("test-end");
|
|
382
|
+
performance.measure("test-measure", "test-start", "test-end");
|
|
383
|
+
} catch (e) {
|
|
384
|
+
// PerformanceObserver might not be fully supported
|
|
385
|
+
resolve(undefined);
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
// Give the observer time to trigger
|
|
391
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
392
|
+
const result = await Promise.race([
|
|
393
|
+
promise,
|
|
394
|
+
new Promise<undefined>((r) => setTimeout(() => r(undefined), 200)),
|
|
395
|
+
]);
|
|
396
|
+
|
|
397
|
+
// PerformanceObserver might not be fully functional in test environment
|
|
398
|
+
if (result !== undefined) {
|
|
399
|
+
expect(result).toBe(1000);
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it("should receive performance entries", async () => {
|
|
404
|
+
if (typeof PerformanceObserver === "undefined") {
|
|
405
|
+
return; // Skip if not available
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const promise = new Promise<{
|
|
409
|
+
hasEntries: boolean;
|
|
410
|
+
store: number | undefined;
|
|
411
|
+
}>((resolve) => {
|
|
412
|
+
als.run(1100, () => {
|
|
413
|
+
try {
|
|
414
|
+
const observer = new PerformanceObserver((list) => {
|
|
415
|
+
resolve({
|
|
416
|
+
hasEntries: list.getEntries().length > 0,
|
|
417
|
+
store: als.getStore(),
|
|
418
|
+
});
|
|
419
|
+
observer.disconnect();
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
observer.observe({ entryTypes: ["measure"] });
|
|
423
|
+
|
|
424
|
+
performance.mark("start-1100");
|
|
425
|
+
performance.mark("end-1100");
|
|
426
|
+
performance.measure("measure-1100", "start-1100", "end-1100");
|
|
427
|
+
} catch (e) {
|
|
428
|
+
resolve({ hasEntries: false, store: undefined });
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
434
|
+
const result = await Promise.race([
|
|
435
|
+
promise,
|
|
436
|
+
new Promise<{ hasEntries: boolean; store: undefined }>((r) =>
|
|
437
|
+
setTimeout(() => r({ hasEntries: false, store: undefined }), 200)
|
|
438
|
+
),
|
|
439
|
+
]);
|
|
440
|
+
|
|
441
|
+
if (result.store !== undefined) {
|
|
442
|
+
expect(result.hasEntries).toBe(true);
|
|
443
|
+
expect(result.store).toBe(1100);
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it("should preserve context with different entry types", async () => {
|
|
448
|
+
if (typeof PerformanceObserver === "undefined") {
|
|
449
|
+
return; // Skip if not available
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const promise = new Promise<number | undefined>((resolve) => {
|
|
453
|
+
als.run(1200, () => {
|
|
454
|
+
try {
|
|
455
|
+
const observer = new PerformanceObserver(() => {
|
|
456
|
+
resolve(als.getStore());
|
|
457
|
+
observer.disconnect();
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
observer.observe({ entryTypes: ["mark", "measure"] });
|
|
461
|
+
|
|
462
|
+
performance.mark("test-mark-1200");
|
|
463
|
+
} catch (e) {
|
|
464
|
+
resolve(undefined);
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
470
|
+
const result = await Promise.race([
|
|
471
|
+
promise,
|
|
472
|
+
new Promise<undefined>((r) => setTimeout(() => r(undefined), 200)),
|
|
473
|
+
]);
|
|
474
|
+
|
|
475
|
+
if (result !== undefined) {
|
|
476
|
+
expect(result).toBe(1200);
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it("should preserve class name as PerformanceObserver", () => {
|
|
481
|
+
if (typeof PerformanceObserver === "undefined") {
|
|
482
|
+
return; // Skip if not available
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
expect(PerformanceObserver.name).toBe("PerformanceObserver");
|
|
486
|
+
});
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
describe("Multiple observers", () => {
|
|
490
|
+
it("should preserve context for multiple different observers", async () => {
|
|
491
|
+
if (
|
|
492
|
+
typeof MutationObserver === "undefined" ||
|
|
493
|
+
typeof PerformanceObserver === "undefined"
|
|
494
|
+
) {
|
|
495
|
+
return; // Skip if not available
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const target = document.createElement("div");
|
|
499
|
+
document.body.appendChild(target);
|
|
500
|
+
|
|
501
|
+
const results: Array<{ type: string; store: number | undefined }> = [];
|
|
502
|
+
|
|
503
|
+
const promise = new Promise<void>((resolve) => {
|
|
504
|
+
als.run(1300, () => {
|
|
505
|
+
const mutationObserver = new MutationObserver(() => {
|
|
506
|
+
results.push({ type: "mutation", store: als.getStore() });
|
|
507
|
+
mutationObserver.disconnect();
|
|
508
|
+
if (results.length >= 2) resolve();
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
mutationObserver.observe(target, { childList: true });
|
|
512
|
+
|
|
513
|
+
try {
|
|
514
|
+
const performanceObserver = new PerformanceObserver(() => {
|
|
515
|
+
results.push({ type: "performance", store: als.getStore() });
|
|
516
|
+
performanceObserver.disconnect();
|
|
517
|
+
if (results.length >= 2) resolve();
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
performanceObserver.observe({ entryTypes: ["measure"] });
|
|
521
|
+
|
|
522
|
+
performance.mark("multi-start");
|
|
523
|
+
performance.mark("multi-end");
|
|
524
|
+
performance.measure("multi-measure", "multi-start", "multi-end");
|
|
525
|
+
} catch (e) {
|
|
526
|
+
// PerformanceObserver might not work, just resolve
|
|
527
|
+
resolve();
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
target.appendChild(document.createElement("span"));
|
|
531
|
+
});
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
535
|
+
await Promise.race([
|
|
536
|
+
promise,
|
|
537
|
+
new Promise<void>((r) => setTimeout(() => r(), 200)),
|
|
538
|
+
]);
|
|
539
|
+
|
|
540
|
+
results.forEach((result) => {
|
|
541
|
+
expect(result.store).toBe(1300);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
document.body.removeChild(target);
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
it("should isolate contexts for observers in different runs", async () => {
|
|
548
|
+
if (typeof MutationObserver === "undefined") {
|
|
549
|
+
return; // Skip if not available
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const target1 = document.createElement("div");
|
|
553
|
+
const target2 = document.createElement("div");
|
|
554
|
+
document.body.appendChild(target1);
|
|
555
|
+
document.body.appendChild(target2);
|
|
556
|
+
|
|
557
|
+
const results: number[] = [];
|
|
558
|
+
|
|
559
|
+
const promise = new Promise<void>((resolve) => {
|
|
560
|
+
let count = 0;
|
|
561
|
+
|
|
562
|
+
als.run(1400, () => {
|
|
563
|
+
const observer = new MutationObserver(() => {
|
|
564
|
+
results.push(als.getStore()!);
|
|
565
|
+
observer.disconnect();
|
|
566
|
+
count++;
|
|
567
|
+
if (count >= 2) resolve();
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
observer.observe(target1, { childList: true });
|
|
571
|
+
target1.appendChild(document.createElement("span"));
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
als.run(1500, () => {
|
|
575
|
+
const observer = new MutationObserver(() => {
|
|
576
|
+
results.push(als.getStore()!);
|
|
577
|
+
observer.disconnect();
|
|
578
|
+
count++;
|
|
579
|
+
if (count >= 2) resolve();
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
observer.observe(target2, { childList: true });
|
|
583
|
+
target2.appendChild(document.createElement("span"));
|
|
584
|
+
});
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
await promise;
|
|
588
|
+
expect(results.sort()).toEqual([1400, 1500]);
|
|
589
|
+
|
|
590
|
+
document.body.removeChild(target1);
|
|
591
|
+
document.body.removeChild(target2);
|
|
592
|
+
});
|
|
593
|
+
});
|
|
594
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "../async-local-storage";
|
|
2
|
+
import { patch, unpatch } from "./patch-helper";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Patch Observer APIs to preserve async context.
|
|
6
|
+
* This ensures that observer callbacks maintain their async context when they execute.
|
|
7
|
+
*
|
|
8
|
+
* Patched observers:
|
|
9
|
+
* - MutationObserver
|
|
10
|
+
* - ResizeObserver
|
|
11
|
+
* - IntersectionObserver
|
|
12
|
+
* - PerformanceObserver
|
|
13
|
+
*/
|
|
14
|
+
export function patchObservers(): void {
|
|
15
|
+
// Patch MutationObserver
|
|
16
|
+
if (typeof MutationObserver !== "undefined") {
|
|
17
|
+
patch(
|
|
18
|
+
globalThis as any,
|
|
19
|
+
"MutationObserver",
|
|
20
|
+
(OriginalMutationObserver: typeof MutationObserver) => {
|
|
21
|
+
const PatchedClass = class extends OriginalMutationObserver {
|
|
22
|
+
constructor(callback: MutationCallback) {
|
|
23
|
+
super(AsyncLocalStorage.bind(callback));
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
// Preserve the original class name
|
|
27
|
+
Object.defineProperty(PatchedClass, "name", {
|
|
28
|
+
value: "MutationObserver",
|
|
29
|
+
});
|
|
30
|
+
return PatchedClass as any;
|
|
31
|
+
}
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Patch ResizeObserver
|
|
36
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
37
|
+
patch(
|
|
38
|
+
globalThis as any,
|
|
39
|
+
"ResizeObserver",
|
|
40
|
+
(OriginalResizeObserver: typeof ResizeObserver) => {
|
|
41
|
+
const PatchedClass = class extends OriginalResizeObserver {
|
|
42
|
+
constructor(callback: ResizeObserverCallback) {
|
|
43
|
+
super(AsyncLocalStorage.bind(callback));
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
Object.defineProperty(PatchedClass, "name", {
|
|
47
|
+
value: "ResizeObserver",
|
|
48
|
+
});
|
|
49
|
+
return PatchedClass as any;
|
|
50
|
+
}
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Patch IntersectionObserver
|
|
55
|
+
if (typeof IntersectionObserver !== "undefined") {
|
|
56
|
+
patch(
|
|
57
|
+
globalThis as any,
|
|
58
|
+
"IntersectionObserver",
|
|
59
|
+
(OriginalIntersectionObserver: typeof IntersectionObserver) => {
|
|
60
|
+
const PatchedClass = class extends OriginalIntersectionObserver {
|
|
61
|
+
constructor(
|
|
62
|
+
callback: IntersectionObserverCallback,
|
|
63
|
+
options?: IntersectionObserverInit
|
|
64
|
+
) {
|
|
65
|
+
super(AsyncLocalStorage.bind(callback), options);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
Object.defineProperty(PatchedClass, "name", {
|
|
69
|
+
value: "IntersectionObserver",
|
|
70
|
+
});
|
|
71
|
+
return PatchedClass as any;
|
|
72
|
+
}
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Patch PerformanceObserver
|
|
77
|
+
if (typeof PerformanceObserver !== "undefined") {
|
|
78
|
+
patch(
|
|
79
|
+
globalThis as any,
|
|
80
|
+
"PerformanceObserver",
|
|
81
|
+
(OriginalPerformanceObserver: typeof PerformanceObserver) => {
|
|
82
|
+
const PatchedClass = class extends OriginalPerformanceObserver {
|
|
83
|
+
constructor(callback: PerformanceObserverCallback) {
|
|
84
|
+
super(AsyncLocalStorage.bind(callback));
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
Object.defineProperty(PatchedClass, "name", {
|
|
88
|
+
value: "PerformanceObserver",
|
|
89
|
+
});
|
|
90
|
+
return PatchedClass as any;
|
|
91
|
+
}
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Remove all Observer patches, restoring original behavior.
|
|
98
|
+
*/
|
|
99
|
+
export function unpatchObservers(): void {
|
|
100
|
+
if (typeof MutationObserver !== "undefined") {
|
|
101
|
+
unpatch(globalThis as any, "MutationObserver");
|
|
102
|
+
}
|
|
103
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
104
|
+
unpatch(globalThis as any, "ResizeObserver");
|
|
105
|
+
}
|
|
106
|
+
if (typeof IntersectionObserver !== "undefined") {
|
|
107
|
+
unpatch(globalThis as any, "IntersectionObserver");
|
|
108
|
+
}
|
|
109
|
+
if (typeof PerformanceObserver !== "undefined") {
|
|
110
|
+
unpatch(globalThis as any, "PerformanceObserver");
|
|
111
|
+
}
|
|
112
|
+
}
|