From f1c48ed2574b1377f005bfa02eb71071d673da8f Mon Sep 17 00:00:00 2001 From: James M Snell Date: Sat, 10 Dec 2022 08:59:08 -0800 Subject: [PATCH] fixup! Add async context tracking documentation to jsg/README.md --- src/workerd/api/node/async-hooks.h | 34 +++++++++++++++++++++++++++--- src/workerd/jsg/async-context.h | 34 ++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 3 deletions(-) diff --git a/src/workerd/api/node/async-hooks.h b/src/workerd/api/node/async-hooks.h index 97af60544a9..c7687fcb5df 100644 --- a/src/workerd/api/node/async-hooks.h +++ b/src/workerd/api/node/async-hooks.h @@ -94,9 +94,37 @@ class AsyncLocalStorage final: public jsg::Object { class AsyncResource final: public jsg::Object, public jsg::AsyncResource { - // The AsyncResource API allows applications to define their own Async contexts. - // For instance, if user code is using custom thenables instead of native promises, - // or user code wishes to attach event listeners to async contexts. + // The AsyncResource class is an object that user code can use to define its own + // async resources for the purpose of storage context propagation. For instance, + // lets imagine that we have an EventTarget and we want to register two event listeners + // on it that will share the same AsyncLocalStorage context. We can use AsyncResource + // to easily define the context and bind multiple event handler functions to it: + // + // const als = new AsyncLocalStorage(); + // const context = als.run(123, () => new AsyncResource('foo')); + // const target = new EventTarget(); + // target.addEventListener('abc', context.bind(() => console.log(als.getStore()))); + // target.addEventListener('xyz', context.bind(() => console.log(als.getStore()))); + // target.addEventListener('bar', () => console.log(als.getStore())); + // + // When the 'abc' and 'xyz' events are emitted, their event handlers will print 123 + // to the console. When the 'bar' event is emitted, undefined will be printed. + // + // Alternatively, we can use EventTarget's object event handler: + // + // const als = new AsyncLocalStorage(); + // + // class MyHandler extends AsyncResource { + // constructor() { super('foo'); } + // void handleEvent() { + // this.runInAsyncScope(() => console.log(als.getStore())); + // } + // } + // + // const handler = als.run(123, () => new MyHandler()); + // const target = new EventTarget(); + // target.addEventListener('abc', handler); + // target.addEventListener('xyz', handler); public: struct Options { jsg::Optional triggerAsyncId; diff --git a/src/workerd/jsg/async-context.h b/src/workerd/jsg/async-context.h index dca27ce3f7b..f25ff45e0a2 100644 --- a/src/workerd/jsg/async-context.h +++ b/src/workerd/jsg/async-context.h @@ -13,6 +13,40 @@ class AsyncResource { // this will be provided by V8 assuming that the AsyncContext proposal advances through // TC-39. For now, however, we implement a model that is very similar to that implemented // by Node.js. + // + // The term "resource" here comes from Node.js, which really doesn't take the time to + // define it properly. Conceptually, an "async resource" is some Thing that generates + // asynchronous activity over time. For instance, a timer is an async resource that + // invokes a callback after a certain period of time elapses; a promise is an async + // resource that may trigger scheduling of a microtask at some point in the future, + // and so forth. Whether or not "resource" is the best term to use to describe these, + // it's what we have because our intent here is to stay aligned with Node.js' model + // as closely as possible. + // + // An async resource has an "execution context" or "execution scope". We enter the + // execution scope immediately before the async resource performs whatever action + // it is going to perform (e.g. invoking a callback), and exit the execution scope + // immediately after. + // + // Execution scopes form a stack. The default execution scope is the Root (which + // we label as id = 0). When we enter the execution scope of a different async resource, + // we push it onto the stack, perform whatever task it is, then pop it back off the + // stack. The Root is associated with the Isolate itself such that every isolate + // always has at least one async resource on the stack at all times. + // + // Every async resource has a storage context. Whatever async resource is currently + // at the top of the stack determines the currently active storage context. So, for + // instance, when we start executing, the Root async resource's storage context is + // active. When a timeout elapses and a timer is going to fire, we enter the timers + // execution scope which makes the timers storage context active. Once the timer + // callback has completed, we return back to the Root async resource's execution + // scope and storage context. + // + // All async resources (except for the Root) are created within the scope of a + // parent, which by default is whichever async resource is at the top of the stack + // when the new resource is created. + // + // When the new resource is created, it inherits the storage context of the parent. public: const uint64_t id; const kj::Maybe parentId;