Skip to content

Commit dbf9bc6

Browse files
committed
Add AsyncLocalStorage.bind/snapshot
These are proposed (in progress) additions to AsyncLocalStorage in Node.js that serve a dual purpose: 1. They align the API closer to the expected AsyncContext.wrap api (AsyncLocalStorage.bind == AsyncContext.wrap). It uses the existing naming from AsyncResource for consistency with the existing API. 2. They eliminate the need to use AsyncResource. We will keep AsyncResource for backwards compatibility as part of the larger Node.js compat story, but these cover all of the key use cases of AsyncResource for context tracking. This should not land until nodejs/node#46387 lands.
1 parent e497feb commit dbf9bc6

File tree

4 files changed

+56
-1
lines changed

4 files changed

+56
-1
lines changed

src/workerd/api/node/async-hooks.c++

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,18 @@ v8::Local<v8::Value> AsyncLocalStorage::getStore(jsg::Lock& js) {
5757
return v8::Undefined(js.v8Isolate);
5858
}
5959

60+
v8::Local<v8::Function> AsyncLocalStorage::bind(jsg::Lock& js, v8::Local<v8::Function> fn) {
61+
KJ_IF_MAYBE(frame, jsg::AsyncContextFrame::current(js)) {
62+
return frame->wrap(js, fn);
63+
} else {
64+
return jsg::AsyncContextFrame::wrapRoot(js, fn);
65+
}
66+
}
67+
68+
v8::Local<v8::Function> AsyncLocalStorage::snapshot(jsg::Lock& js) {
69+
return jsg::AsyncContextFrame::wrapSnapshot(js);
70+
}
71+
6072
namespace {
6173
kj::Maybe<jsg::Ref<jsg::AsyncContextFrame>> tryGetFrameRef(jsg::Lock& js) {
6274
return jsg::AsyncContextFrame::current(js).map(

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,15 @@ class AsyncLocalStorage final: public jsg::Object {
4646

4747
v8::Local<v8::Value> getStore(jsg::Lock& js);
4848

49+
static v8::Local<v8::Function> bind(jsg::Lock& js, v8::Local<v8::Function> fn);
50+
// Binds the given function to the current async context frame such that
51+
// whenever the function is called, the bound frame is entered.
52+
53+
static v8::Local<v8::Function> snapshot(jsg::Lock& js);
54+
// Returns a function bound to the current async context frame that calls
55+
// the function passed to it as the only argument within that frame.
56+
// Equivalent to AsyncLocalStorage.bind((cb, ...args) => cb(...args)).
57+
4958
inline void enterWith(jsg::Lock&, v8::Local<v8::Value>) {
5059
KJ_UNIMPLEMENTED("asyncLocalStorage.enterWith() is not implemented");
5160
}
@@ -60,6 +69,8 @@ class AsyncLocalStorage final: public jsg::Object {
6069
JSG_METHOD(getStore);
6170
JSG_METHOD(enterWith);
6271
JSG_METHOD(disable);
72+
JSG_STATIC_METHOD(bind);
73+
JSG_STATIC_METHOD(snapshot);
6374

6475
if (flags.getNodeJsCompat()) {
6576
JSG_TS_OVERRIDE(AsyncLocalStorage<T> {
@@ -68,6 +79,8 @@ class AsyncLocalStorage final: public jsg::Object {
6879
exit<R, TArgs extends any[]>(callback: (...args: TArgs) => R, ...args: TArgs): R;
6980
disable(): void;
7081
enterWith(store: T): void;
82+
static bind<Func extends (...args: any[]) => any>(fn: Func): Func;
83+
static snapshot<R, TArgs extends any[]>() : ((...args: TArgs) => R, ...args: TArgs) => R;
7184
});
7285
} else {
7386
JSG_TS_OVERRIDE(type AsyncLocalStorage = never);
@@ -80,6 +93,10 @@ class AsyncLocalStorage final: public jsg::Object {
8093

8194

8295
class AsyncResource final: public jsg::Object {
96+
// Note: The AsyncResource class is provided for Node.js backwards compatibility.
97+
// The class can be replaced entirely for async context tracking using the
98+
// AsyncLocalStorage.bind() and AsyncLocalStorage.snapshot() APIs.
99+
//
83100
// The AsyncResource class is an object that user code can use to define its own
84101
// async resources for the purpose of storage context propagation. For instance,
85102
// let's imagine that we have an EventTarget and we want to register two event listeners

src/workerd/jsg/async-context.c++

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,28 @@ v8::Local<v8::Function> AsyncContextFrame::wrap(
6969
return wrap(js, fn.getHandle(js), thisArg);
7070
}
7171

72+
v8::Local<v8::Function> AsyncContextFrame::wrapSnapshot(Lock& js) {
73+
auto isolate = js.v8Isolate;
74+
auto context = isolate->GetCurrentContext();
75+
76+
return js.wrapReturningFunction(context, JSG_VISITABLE_LAMBDA(
77+
(frame = AsyncContextFrame::currentRef(js)),
78+
(frame),
79+
(Lock& js, const v8::FunctionCallbackInfo<v8::Value>& args) {
80+
auto context = js.v8Isolate->GetCurrentContext();
81+
JSG_REQUIRE(args[0]->IsFunction(), TypeError, "The first argument must be a function");
82+
auto fn = args[0].As<v8::Function>();
83+
kj::Vector<v8::Local<v8::Value>> argv(args.Length() - 1);
84+
for (int n = 1; n < args.Length(); n++) {
85+
argv.add(args[n]);
86+
}
87+
88+
AsyncContextFrame::Scope scope(js, frame);
89+
return check(fn->Call(context, context->Global(), argv.size(), argv.begin()));
90+
}
91+
));
92+
}
93+
7294
v8::Local<v8::Function> AsyncContextFrame::wrap(
7395
Lock& js,
7496
v8::Local<v8::Function> fn,
@@ -93,7 +115,6 @@ v8::Local<v8::Function> AsyncContextFrame::wrap(
93115
}
94116

95117
AsyncContextFrame::Scope scope(js, *frame.get());
96-
v8::Local<v8::Value> result;
97118
return check(function->Call(context, thisArg.getHandle(js), args.Length(), argv.begin()));
98119
}));
99120
}

src/workerd/jsg/async-context.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,11 @@ class AsyncContextFrame final: public Wrappable {
125125
// Wraps the given JavaScript function such that whenever the wrapper function is called,
126126
// the root AsyncContextFrame will be entered.
127127

128+
static v8::Local<v8::Function> wrapSnapshot(Lock& js);
129+
// Returns a function that captures the current frame and calls the function passed
130+
// in as an argument within that captured context. Equivalent to wrapping a function
131+
// with the signature (cb, ...args) => cb(...args).
132+
128133
v8::Local<v8::Function> wrap(
129134
Lock& js, V8Ref<v8::Function>& fn,
130135
kj::Maybe<v8::Local<v8::Value>> thisArg = nullptr);

0 commit comments

Comments
 (0)