Skip to content
File

Blob: src/workerd/api/node/async-hooks.h

cpp261 lines
1// Copyright (c) 2017-2022 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4#pragma once
5 
6#include <workerd/jsg/async-context.h>
7#include <workerd/jsg/jsg.h>
8 
9namespace workerd::api::node {
10 
11// Implements a subset of the Node.js AsyncLocalStorage API.
12//
13// Example:
14//
15// import * as async_hooks from 'node:async_hooks';
16// const als = new async_hooks.AsyncLocalStorage();
17//
18// async function doSomethingAsync() {
19// await scheduler.wait(100);
20// console.log(als.getStore()); // 1
21// }
22//
23// als.run(1, async () => {
24// console.log(als.getStore()); // 1
25// await doSomethingAsync();
26// console.log(als.getStore()); // 1
27// });
28// console.log(als.getStore()); // undefined
29class AsyncLocalStorage final: public jsg::Object {
30 public:
31 struct AsyncLocalStorageOptions {
32 jsg::Optional<jsg::JsRef<jsg::JsValue>> defaultValue;
33 jsg::Optional<kj::String> name;
34 JSG_STRUCT(defaultValue, name)
35 };
36 
37 AsyncLocalStorage(jsg::Optional<AsyncLocalStorageOptions> options = kj::none)
38 : key(kj::refcounted<jsg::AsyncContextFrame::StorageKey>()) {
39 KJ_IF_SOME(opt, options) {
40 defaultValue = kj::mv(opt.defaultValue);
41 name = kj::mv(opt.name);
42 }
43 }
44 
45 ~AsyncLocalStorage() noexcept(false) {
46 key->reset();
47 }
48 
49 static jsg::Ref<AsyncLocalStorage> constructor(
50 jsg::Lock& js, jsg::Optional<AsyncLocalStorageOptions> options);
51 
52 v8::Local<v8::Value> run(jsg::Lock& js,
53 v8::Local<v8::Value> store,
54 jsg::Function<v8::Local<v8::Value>(jsg::Arguments<jsg::Value>)> callback,
55 jsg::Arguments<jsg::Value> args);
56 
57 v8::Local<v8::Value> exit(jsg::Lock& js,
58 jsg::Function<v8::Local<v8::Value>(jsg::Arguments<jsg::Value>)> callback,
59 jsg::Arguments<jsg::Value> args);
60 
61 v8::Local<v8::Value> getStore(jsg::Lock& js);
62 
63 // Binds the given function to the current async context frame such that
64 // whenever the function is called, the bound frame is entered.
65 static v8::Local<v8::Function> bind(jsg::Lock& js, v8::Local<v8::Function> fn);
66 
67 // Returns a function bound to the current async context frame that calls
68 // the function passed to it as the only argument within that frame.
69 // Equivalent to AsyncLocalStorage.bind((cb, ...args) => cb(...args)).
70 static v8::Local<v8::Function> snapshot(jsg::Lock& js);
71 
72 inline void enterWith(jsg::Lock&, v8::Local<v8::Value>) {
73 JSG_FAIL_REQUIRE(Error, "asyncLocalStorage.enterWith() is not implemented");
74 }
75 
76 inline void disable(jsg::Lock&) {
77 JSG_FAIL_REQUIRE(Error, "asyncLocalStorage.disable() is not implemented");
78 }
79 
80 kj::StringPtr getName();
81 
82 JSG_RESOURCE_TYPE(AsyncLocalStorage) {
83 JSG_METHOD(run);
84 JSG_METHOD(exit);
85 JSG_METHOD(getStore);
86 JSG_METHOD(enterWith);
87 JSG_METHOD(disable);
88 JSG_STATIC_METHOD(bind);
89 JSG_STATIC_METHOD(snapshot);
90 JSG_READONLY_PROTOTYPE_PROPERTY(name, getName);
91 
92 JSG_TS_OVERRIDE(AsyncLocalStorage<T> {
93 constructor(options?: AsyncLocalStorageAsyncLocalStorageOptions);
94 readonly name: string;
95 getStore(): T | undefined;
96 run<R, TArgs extends any[]>(store: T, callback: (...args: TArgs) => R, ...args: TArgs): R;
97 exit<R, TArgs extends any[]>(callback: (...args: TArgs) => R, ...args: TArgs): R;
98 enterWith(store: T): never;
99 disable(): never;
100 static bind<Func extends (...args: any[]) => any>(fn: Func): Func;
101 static snapshot<R, TArgs extends any[]>(): (fn: (...args: TArgs) => R, ...args: TArgs) => R;
102 });
103 }
104 
105 kj::Own<jsg::AsyncContextFrame::StorageKey> getKey();
106 
107 private:
108 kj::Own<jsg::AsyncContextFrame::StorageKey> key;
109 kj::Maybe<jsg::JsRef<jsg::JsValue>> defaultValue;
110 kj::Maybe<kj::String> name;
111};
112 
113// Note: The AsyncResource class is provided for Node.js backwards compatibility.
114// The class can be replaced entirely for async context tracking using the
115// AsyncLocalStorage.bind() and AsyncLocalStorage.snapshot() APIs.
116//
117// The AsyncResource class is an object that user code can use to define its own
118// async resources for the purpose of storage context propagation. For instance,
119// let's imagine that we have an EventTarget and we want to register two event listeners
120// on it that will share the same AsyncLocalStorage context. We can use AsyncResource
121// to easily define the context and bind multiple event handler functions to it:
122//
123// const als = new AsyncLocalStorage();
124// const context = als.run(123, () => new AsyncResource('foo'));
125// const target = new EventTarget();
126// target.addEventListener('abc', context.bind(() => console.log(als.getStore())));
127// target.addEventListener('xyz', context.bind(() => console.log(als.getStore())));
128// target.addEventListener('bar', () => console.log(als.getStore()));
129//
130// When the 'abc' and 'xyz' events are emitted, their event handlers will print 123
131// to the console. When the 'bar' event is emitted, undefined will be printed.
132//
133// Alternatively, we can use EventTarget's object event handler:
134//
135// const als = new AsyncLocalStorage();
136//
137// class MyHandler extends AsyncResource {
138// constructor() { super('foo'); }
139// void handleEvent() {
140// this.runInAsyncScope(() => console.log(als.getStore()));
141// }
142// }
143//
144// const handler = als.run(123, () => new MyHandler());
145// const target = new EventTarget();
146// target.addEventListener('abc', handler);
147// target.addEventListener('xyz', handler);
148class AsyncResource final: public jsg::Object {
149 public:
150 struct Options {
151 // Node.js' API allows user code to create AsyncResource instances within an
152 // explicitly specified parent execution context (what we call an "Async Context
153 // Frame") that is specified by a numeric ID. We do not track our context frames
154 // by ID and always create new AsyncResource instances within the current Async
155 // Context Frame. To prevent subtle bugs, we'll throw explicitly if user code
156 // tries to set the triggerAsyncId option.
157 //
158 // Node.js also has an additional `requireManualDestroy` boolean option
159 // that we do not implement. We can simply omit it here. There's no risk of
160 // bugs or unexpected behavior by doing so.
161 jsg::WontImplement triggerAsyncId;
162 
163 JSG_STRUCT(triggerAsyncId);
164 };
165 
166 AsyncResource(jsg::Lock& js);
167 
168 // While Node.js' API expects the first argument passed to the `new AsyncResource(...)`
169 // constructor to be a string specifying the resource type, we do not actually use it
170 // for anything. We'll just ignore the value and not store it, but we at least need to
171 // accept the argument and validate that it is a string.
172 static jsg::Ref<AsyncResource> constructor(
173 jsg::Lock& js, jsg::Optional<kj::String> type, jsg::Optional<Options> options = kj::none);
174 
175 static v8::Local<v8::Function> staticBind(jsg::Lock& js,
176 v8::Local<v8::Function> fn,
177 jsg::Optional<kj::String> type,
178 jsg::Optional<v8::Local<v8::Value>> thisArg,
179 const jsg::TypeHandler<jsg::Ref<AsyncResource>>& handler);
180 
181 // Binds the given function to this async context.
182 v8::Local<v8::Function> bind(jsg::Lock& js,
183 v8::Local<v8::Function> fn,
184 jsg::Optional<v8::Local<v8::Value>> thisArg,
185 const jsg::TypeHandler<jsg::Ref<AsyncResource>>& handler);
186 
187 // Calls the given function within this async context.
188 v8::Local<v8::Value> runInAsyncScope(jsg::Lock& js,
189 jsg::Function<v8::Local<v8::Value>(jsg::Arguments<jsg::Value>)> fn,
190 jsg::Optional<v8::Local<v8::Value>> thisArg,
191 jsg::Arguments<jsg::Value>);
192 
193 // The Node.js API uses numeric identifiers for all async resources. We do not
194 // implement that part of their API. Always returns 0.
195 inline int asyncId() {
196 return 0;
197 }
198 
199 // The Node.js API uses numeric identifiers for all async resources. We do not
200 // implement that part of their API. Always returns 0.
201 inline int triggerAsyncId() {
202 return 0;
203 }
204 
205 // No-op. We do not track resource lifetimes. This is provided only for API compatibility.
206 void emitDestroy(jsg::Lock&) {};
207 
208 JSG_RESOURCE_TYPE(AsyncResource) {
209 JSG_STATIC_METHOD_NAMED(bind, staticBind);
210 JSG_METHOD(asyncId);
211 JSG_METHOD(triggerAsyncId);
212 JSG_METHOD(bind);
213 JSG_METHOD(runInAsyncScope);
214 JSG_METHOD(emitDestroy);
215 
216 JSG_TS_OVERRIDE(AsyncResource {
217 constructor(type: string, options?: AsyncResourceOptions);
218 static bind<Func extends (this: ThisArg, ...args: any[]) => any, ThisArg>(fn: Func, type?: string, thisArg?: ThisArg): Func;
219 bind<Func extends (...args: any[]) => any>(fn: Func): Func;
220 runInAsyncScope<This, Result>(fn: (this: This, ...args: any[]) => Result, thisArg?: This, ...args: any[]): Result;
221 asyncId(): number;
222 triggerAsyncId(): number;
223 emitDestroy(): void;
224 });
225 }
226 
227 // Returns the jsg::AsyncContextFrame captured when the AsyncResource was created, if any.
228 kj::Maybe<jsg::AsyncContextFrame&> getFrame();
229 
230 void visitForMemoryInfo(jsg::MemoryTracker& tracker) const {
231 tracker.trackField("frame", frame);
232 }
233 
234 private:
235 kj::Maybe<jsg::Ref<jsg::AsyncContextFrame>> frame;
236 
237 inline void visitForGc(jsg::GcVisitor& visitor) {
238 visitor.visit(frame);
239 }
240};
241 
242// We have no intention of fully-implementing the Node.js async_hooks module.
243// We provide this because AsyncLocalStorage is exposed via async_hooks in
244// Node.js.
245class AsyncHooksModule final: public jsg::Object {
246 public:
247 AsyncHooksModule() = default;
248 AsyncHooksModule(jsg::Lock&, const jsg::Url&) {}
249 
250 JSG_RESOURCE_TYPE(AsyncHooksModule) {
251 JSG_NESTED_TYPE(AsyncLocalStorage);
252 JSG_NESTED_TYPE(AsyncResource);
253 }
254};
255 
256#define EW_NODE_ASYNCHOOKS_ISOLATE_TYPES \
257 api::node::AsyncHooksModule, api::node::AsyncResource, api::node::AsyncResource::Options, \
258 api::node::AsyncLocalStorage, api::node::AsyncLocalStorage::AsyncLocalStorageOptions
259 
260} // namespace workerd::api::node