Skip to content

Commit 3665359

Browse files
committed
feat: adding partial implementation of async_hooks to local dev
1 parent 84a5de8 commit 3665359

File tree

1 file changed

+232
-14
lines changed

1 file changed

+232
-14
lines changed
+232-14
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,252 @@
11
/* eslint-disable */
2+
/**
3+
* async_hooks.js
4+
* AsyncLocalStorage and AsyncResource
5+
* This polyfill aims to simulate the behavior of AsyncLocalStorage and AsyncResource for the Edge runtime.
6+
* it may behave differently than Node.js
7+
*
8+
* this polyfill was based on the Bun.js implementation
9+
* https://github.com/oven-sh/bun/blob/main/src/js/node/async_hooks.ts
10+
*/
11+
const asyncHooksEnabled = true;
12+
let asyncContext = [];
13+
14+
function setContext(contextValue) {
15+
asyncContext[0] = contextValue;
16+
}
17+
18+
function getContext() {
19+
return asyncContext[0];
20+
}
21+
222
export class AsyncLocalStorage {
23+
#disabled = false;
24+
325
constructor() {
4-
this.store = new Map();
26+
if (asyncHooksEnabled) {
27+
const uid = Math.random().toString(36).slice(2, 8);
28+
this.__id__ = uid;
29+
}
30+
}
31+
32+
static bind(fn, ...args) {
33+
return this.snapshot().bind(null, fn, ...args);
34+
}
35+
36+
static snapshot() {
37+
const context = getContext();
38+
return (fn, ...args) => {
39+
const prev = getContext();
40+
setContext(context);
41+
try {
42+
return fn(...args);
43+
} catch (error) {
44+
throw error;
45+
} finally {
46+
setContext(prev);
47+
}
48+
};
49+
}
50+
51+
enterWith(store) {
52+
this.#disabled = false;
53+
const context = getContext();
54+
if (!context) {
55+
setContext([this, store]);
56+
return;
57+
}
58+
const length = context.length;
59+
for (let i = 0; i < length; i += 2) {
60+
if (context[i] === this) {
61+
let clone = context.slice();
62+
clone[i + 1] = store;
63+
setContext(clone);
64+
return;
65+
}
66+
}
67+
setContext([...context, this, store]);
68+
}
69+
70+
exit(cb, ...args) {
71+
return this.run(undefined, cb, ...args);
572
}
673

7-
run(storeData, callback) {
8-
const key = Symbol();
9-
this.store.set(key, storeData);
10-
const restoreData = Object.fromEntries(this.store);
74+
run(storeValue, callback, ...args) {
75+
const contextWasAlreadyInit = !getContext();
76+
const wasDisabled = this.#disabled;
77+
this.#disabled = false;
78+
79+
if (contextWasAlreadyInit) {
80+
setContext([this, storeValue]);
81+
} else {
82+
const context = getContext().slice();
83+
const i = context.indexOf(this);
84+
85+
if (i > -1) {
86+
context[i + 1] = storeValue;
87+
} else {
88+
context.push(this, storeValue);
89+
}
90+
91+
setContext(context);
92+
}
93+
94+
try {
95+
return callback(...args);
96+
} finally {
97+
if (!wasDisabled) {
98+
const originalContext = getContext();
99+
setContext(originalContext);
100+
}
101+
}
102+
}
11103

12-
return callback(restoreData);
104+
disable() {
105+
if (this.#disabled) return;
106+
this.#disabled = true;
107+
let context = getContext().slice();
108+
if (context) {
109+
const length = context.length;
110+
for (let i = 0; i < length; i += 2) {
111+
if (context[i] === this) {
112+
context.splice(i, 2);
113+
setContext(context.length ? context : undefined);
114+
break;
115+
}
116+
}
117+
}
13118
}
14119

15120
getStore() {
16-
const allEntries = [...this.store.entries()];
17-
if (allEntries.length === 0) return undefined;
121+
if (this.#disabled) return;
122+
const context = getContext();
123+
if (!context) return;
124+
const length = context.length;
125+
for (let i = length - 2; i >= 0; i -= 2) {
126+
if (context[i] === this) {
127+
return context[i + 1];
128+
}
129+
}
130+
}
131+
}
132+
133+
export class AsyncResource {
134+
#snapshot;
135+
136+
constructor(type, options) {
137+
if (typeof type !== 'string') {
138+
throw new TypeError(
139+
`The "type" argument must be of type string. Received type ${typeof type}`,
140+
);
141+
}
142+
this.#snapshot = getContext();
143+
}
144+
145+
emitBefore() {
146+
return true;
147+
}
148+
149+
emitAfter() {
150+
return true;
151+
}
152+
153+
asyncId() {
154+
return 0;
155+
}
156+
157+
triggerAsyncId() {
158+
return 0;
159+
}
18160

19-
const [_, lastValue] = allEntries[allEntries.length - 1];
20-
return lastValue;
161+
emitDestroy() {
162+
//
21163
}
22164

23-
enterWith(storeData) {
24-
this.store = new Map(Object.entries(storeData));
165+
runInAsyncScope(fn, thisArg, ...args) {
166+
const prev = getContext();
167+
setContext(this.#snapshot);
168+
try {
169+
return fn.apply(thisArg, args);
170+
} catch (error) {
171+
throw error;
172+
} finally {
173+
setContext(prev);
174+
}
25175
}
26176

27-
exit() {
28-
this.store = new Map();
177+
bind(fn, thisArg) {
178+
return this.runInAsyncScope.bind(this, fn, thisArg || this);
179+
}
180+
181+
static bind(fn, type, thisArg) {
182+
type = type || fn.name;
183+
return new AsyncResource(type || 'bound-anonymous-fn').bind(fn, thisArg);
29184
}
30185
}
31186

187+
export const asyncWrapProviders = {
188+
NONE: 0,
189+
DIRHANDLE: 1,
190+
DNSCHANNEL: 2,
191+
ELDHISTOGRAM: 3,
192+
FILEHANDLE: 4,
193+
FILEHANDLECLOSEREQ: 5,
194+
FIXEDSIZEBLOBCOPY: 6,
195+
FSEVENTWRAP: 7,
196+
FSREQCALLBACK: 8,
197+
FSREQPROMISE: 9,
198+
GETADDRINFOREQWRAP: 10,
199+
GETNAMEINFOREQWRAP: 11,
200+
HEAPSNAPSHOT: 12,
201+
HTTP2SESSION: 13,
202+
HTTP2STREAM: 14,
203+
HTTP2PING: 15,
204+
HTTP2setContextTINGS: 16,
205+
HTTPINCOMINGMESSAGE: 17,
206+
HTTPCLIENTREQUEST: 18,
207+
JSSTREAM: 19,
208+
JSUDPWRAP: 20,
209+
MESSAGEPORT: 21,
210+
PIPECONNECTWRAP: 22,
211+
PIPESERVERWRAP: 23,
212+
PIPEWRAP: 24,
213+
PROCESSWRAP: 25,
214+
PROMISE: 26,
215+
QUERYWRAP: 27,
216+
SHUTDOWNWRAP: 28,
217+
SIGNALWRAP: 29,
218+
STATWATCHER: 30,
219+
STREAMPIPE: 31,
220+
TCPCONNECTWRAP: 32,
221+
TCPSERVERWRAP: 33,
222+
TCPWRAP: 34,
223+
TTYWRAP: 35,
224+
UDPSENDWRAP: 36,
225+
UDPWRAP: 37,
226+
SIGINTWATCHDOG: 38,
227+
WORKER: 39,
228+
WORKERHEAPSNAPSHOT: 40,
229+
WRITEWRAP: 41,
230+
ZLIB: 42,
231+
CHECKPRIMEREQUEST: 43,
232+
PBKDF2REQUEST: 44,
233+
KEYPAIRGENREQUEST: 45,
234+
KEYGENREQUEST: 46,
235+
KEYEXPORTREQUEST: 47,
236+
CIPHERREQUEST: 48,
237+
DERIVEBITSREQUEST: 49,
238+
HASHREQUEST: 50,
239+
RANDOMBYTESREQUEST: 51,
240+
RANDOMPRIMEREQUEST: 52,
241+
SCRYPTREQUEST: 53,
242+
SIGNREQUEST: 54,
243+
TLSWRAP: 55,
244+
VERIFYREQUEST: 56,
245+
INSPECTORJSBINDING: 57,
246+
};
247+
32248
export default {
33249
AsyncLocalStorage,
250+
asyncWrapProviders,
251+
AsyncResource,
34252
};

0 commit comments

Comments
 (0)