-
Notifications
You must be signed in to change notification settings - Fork 16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Suggestion: use syntax for async variables #113
Comments
To show how this all would play out in practice, here's most of the README examples: dynamic asyncVar;
// Sets the current value to 'top', and executes the `main` function.
{
dynamic using asyncVar = "top";
main()
}
function main() {
// Dynamic variable is maintained through other platform queueing.
setTimeout(() => {
console.log(asyncVar); // => 'top'
{
dynamic using asyncVar = 'A'
console.log(asyncVar); // => 'A'
setTimeout(() => {
console.log(asyncVar); // => 'A'
}, randomTimeout());
}
}, randomTimeout());
// Dynamic variable runs can be nested.
{
dynamic using asyncVar = "B";
console.log(asyncVar); // => 'B'
setTimeout(() => {
console.log(asyncVar); // => 'B'
}, randomTimeout());
}
// Dynamic variable was restored after the previous run.
console.log(asyncVar); // => 'top'
}
function randomTimeout() {
return Math.random() * 1000;
} dynamic asyncVar;
let snapshot;
{
dynamic using asyncVar = "A";
// Captures the state of all dynamic variables at this moment.
snapshot = function.context;
}
{
dynamic using asyncVar = "B";
console.log(asyncVar); // => 'B'
// The snapshot will restore all dynamic variables to their snapshot
// state and invoke the wrapped function. We pass a function which it will
// invoke.
dynamic using * from snapshot;
// Despite being lexically nested inside 'B', the snapshot restored us to
// to the snapshot 'A' state.
console.log(asyncVar); // => 'A'
} let queue = [];
export function enqueueCallback(cb: () => void) {
// Each callback is stored with the context at which it was enqueued.
const snapshot = function.context;
queue.push({snapshot, cb});
}
runWhenIdle(() => {
// All callbacks in the queue would be run with the current context if they
// hadn't been wrapped.
for (const {snapshot, cb} of queue) {
dynamic using * from snapshot;
cb();
}
queue = [];
}); // tracer.js
dynamic span;
export function run(cb) {
// (a)
dynamic using span = {
startTime: Date.now(),
traceId: randomUUID(),
spanId: randomUUID(),
};
cb();
}
export function end() {
// (b)
span?.endTime = Date.now();
} dynamic currentTask = { priority: "default" };
const scheduler = {
postTask(task, options) {
// In practice, the task execution may be deferred.
// Here we simply run the task immediately.
dynamic using currentTask = { priority: options.priority };
task();
},
currentTask() {
return currentTask;
},
};
const res = await scheduler.postTask(task, { priority: "background" });
console.log(res);
async function task() {
// Fetch remains background priority by referring to scheduler.currentTask().
const resp = await fetch("/hello");
const text = await resp.text();
scheduler.currentTask(); // => { priority: 'background' }
return doStuffs(text);
}
async function doStuffs(text) {
// Some async calculation...
return text;
} |
That's pretty much how I had originally thought async context in the spec should work, but some people were really against the idea of dynamic variable scoping and pushed hard against adding new syntax. It's really the most straightforward though, both from a semantic perspective and from a technical perspective. It's easier to understand from reading the code, and it's easier to optimize and control the allowable flow patterns by making it syntax. It does add to the work involved in the start and end of every block though, which some deemed too performance-sensitive to change things...so instead pushed the proposal to follow a much more expensive model on the premise that this is some very rarely needed thing and not a thing that actually most apps use at some level, either through abstract implementation or through runtime-provided tooling like AsyncLocalStorage in Node.js. |
|
Yep, not saying otherwise, just sharing that there was some prior art in this area that never really went anywhere as there were some strong voices that pushed back against the idea of adding syntax for this. I personally actually think syntax is the ideal way to design such a capability. When you're working with something that is just another object you have to contort it in various ways to fit how every other object in the language behaves, but when you make it syntax you can define the behaviour with a lot less limitations imposed on how it could conceivably work. |
IMO, the proposed syntax is not necessarily related to "dynamic scopes" even though it uses the term "dynamic". I'd prefer not confusing people with the term "dynamic". I can find the similarity with https://github.com/tc39/proposal-async-context/blob/master/MUTATION-SCOPE.md#the-set-semantic-with-scope-enforcement that utilizes a syntax to provide a scoped mutation without introducing a new function like |
I found a way to make everything but the variable get fast in this comment, and that one remaining optimization looks an awful lot like the inline caches used for types. The fallback path is about the only part that isn't a trivially inlined function.
And for semantics, there's concerns about excessive creation of async variables, and I do share them: #50
One way to quietly guide people to use async vars correctly (and to optimize async var usage in general) is to make them literal variable bindings. Also, admittedly,
.get()
is annoying boilerplate.And for performance, this glides right in and makes it all both trivially tree-shakeable and essentially zero-cost.
So, what about this syntax?
From a spec standpoint, this would be a unique type of reference, alongside property values. Module property accesses would return dynamic values as references to dynamic values.
The text was updated successfully, but these errors were encountered: