Motivation
Rust-style async function: Async functions that act in the Poll model run therefore user-land code can decide when to continue the execution, or even cancel the execution.
Design
I propose a syntax sugar to the generator function. In the old times, we use co and generator functions to simulate the async functions.
import co from 'co'
co(function* () {
yield Promise.resolve(true)
// eq to await Promise.resolve(true)
return 1
}).then(console.log)
co
is the userland scheduler and the generator is a cooperative async function. This is an old-time example.
WICG/scheduling-apis is the next generation Web scheduler API, here is an example of my proposal (to act with scheduling APIs).
lazy async function task() {
const a = await fetch("./task")
const b = await a.json()
return b.tasks
}
const res = scheduler.postTask(task, { priority: 'background' });
The scheduler can "pause" the async function to continue if there is a high-priority task that comes in. With this manner, this syntax is a natural replacement of scheduler.yield()
in the WICG/Scheduling-API proposal.
A userland example: cancellable async task
In many frameworks like React, Vue, and Angular, components have a "life circle". Async tasks should be canceled when the component is dismounted. This can be done with AbortSignal
or manually check but there are much noises inside the code.
For example:
useEffect(() => {
let stop = false
(async () => {
const data = await fetch('./data')
const json = await data.json()
if (stop) return
setState(json)
})()
return () => stop = true
})
With an AbortSignal
-friendly user land API (not real in React) it can be simpler:
useEffect(async signal => {
const data = await fetch('./data', { signal })
const json = await data.json()
if (signal.aborted) return
setState(json)
})
With this proposal, it can be much simpler:
useEffect(lazy async signal => {
const data = await fetch('./data', { signal })
const json = await data.json()
setState(json)
})
As you can see, there is no need to check if the signal has expired. Because the userland scheduler useEffect
will cancel the running async function.
Semantics: How it works
The lazy
modifier on the async function creates a lazy async function. When it is called, it behaves like a Generator<Promise<any>>
.
lazy async function task() {
console.log("start")
await true
console.log("then")
return fetch("/")
}
const t = task()
// t: Generator<suspended>
$ = t.next()
// Log: "start"
// { done: false, value: Promise<true> }
await $.value
$ = t.next()
// Log: "then"
// { done: false, value: Promise<fetch> }
await $.value
$ = t.next()
// { done: true, value: Resopnse }
- The value passed into the
next()
WILL BE ignored. Theawait
value will always be the result of the promise. The scheduler can only control the continue or not. It cannot replace theawait
result. - Return a
Promise
will be treated asreturn await $ret
Interact with other syntaxes
Await lazy async function in a normal async function:
async function a() {
await Promise.from(b())
}
lazy asnyc function b() {}
A new API like Promise.from
will convert a Generator<Promise<Yield>, Return>
into Promise<Return>
.
Await lazy async function in another lazy async function
lazy async function a() {
await b()
}
lazy async function b() {}
No need to do anything. It works like a yield* $value
, delegate all steps to the scheduler.
Why not generators?
- Semantics do not match
Benefits
- Stoppable async functions
- Native scheduler (the WICG one) or the userland scheduler (React) can schedule tasks that take priority knowledge in mind.