suspenders-js
Version:
Asynchronous programming library utilizing coroutines, functional reactive programming and structured concurrency.
175 lines (138 loc) • 4.34 kB
text/typescript
import { Channel } from "./Channel";
import { EventSubject, flowOf } from "./Flow";
import { Scope } from "./Scope";
import { race, wait } from "./Util";
// Structured concurrency
// Scopes have ownership of coroutines. Coroutines are launched in a scope. If that scope is
// canceled, then any coroutines and subscopes within it will be canceled as well. If a coroutine
// throws an error, it cancels it's owning scope with an error. Scopes canceled with an error
// will bubble the the error up to it's parent, canceling the whole tree.
// For example if scope1 is canceled all coroutines would be canceled in the graph below. But if
// scope2 is canceled, only coroutine3 and coroutine4 are canceled. If coroutine3 throws an error,
// all the coroutines and scopes are canceled.
// scope1
// |
// +- coroutine1
// |
// +- coroutine2
// |
// +- scope2
// |
// + coroutine3
// |
// + coroutine4
const scope = new Scope();
// Flows emit multiple async values. They provide a typical functional interface like map() and
// .filter(). Flows are cold, meaning they don't emit values unless they have an observer.
flowOf<number>((collector) => function*() {
for (let i = 1; i <= 200; i++) {
collector.emit(i);
}
})
.filter(x => x % 2 === 1)
.map(x => x + x)
.onEach(x => console.log(x))
// This will start the flow in scope.
.launchIn(scope);
// This starts a coroutine that consumes two flows in order.
scope.launch(function* () {
// suspends until flow completes
yield flowOf<number>((collector) => function*() {
for (let i = 1; i <= 200; i++) {
collector.emit(i);
}
})
.filter(x => x % 2 === 1)
.map(x => x + x)
// Collect() consumes the flow until it completes.
// Resumes the coroutine once the flow has completed.
.collect(x => console.log(x));
yield flowOf<number>((collector) => function*() {
for (let i = 1; i <= 200; i++) {
collector.emit(i);
}
})
.filter(x => x % 2 === 1)
.map(x => x + x)
.collect(x => console.log(x));
});
// Channels are for communication between coroutines.
const channel = new Channel<number>();
// Producer/consumer coroutines communicating through a channel.
scope.launch(function* () {
for (let i = 1; i <= 200; i++) {
yield channel.send(i);
}
});
scope.launch(function* () {
for (;;) {
const x = yield* this.suspend(channel.receive);
if (x % 2 === 1) {
const y = x + x;
console.log(y);
}
}
});
// Transform() is a powerful way to process values emitted by a flow. It takes a coroutine that can
// perform more asynchronous tasks and emit 0 or more values downstream.
const eventSubject = new EventSubject<number>();
scope.launch(function* () {
yield eventSubject
.transform<number>((x, collector) => function* () {
if (x % 2 === 1) {
yield wait(10);
collector.emit(x + x);
}
})
.collect(x => {
console.log(x);
});
})
// Pushes events to observers on eventSubject.
for (let i = 1; i <= 200; i++) {
eventSubject.emit(i);
}
// Calling another coroutine from a coroutine.
function* anotherCoroutine(this: Scope) {
yield wait(100);
// This doesn't wait for the result of the launched coroutine.
this.launch(function* () {
yield wait(200);
});
return 1;
}
scope.launch(function* () {
// This ensures all coroutines launched from anotherCoroutine() are completed before resuming.
const x = yield* this.call(anotherCoroutine);
console.log(x);
});
scope.launch(function* () {
// This will resume before all coroutines launched from anotherCoroutine() have completed.
const x = yield* anotherCoroutine.call(this);
console.log(x);
});
// Asynchronously call coroutines.
function* jobA(this: Scope) {
yield wait(100);
return 1;
}
function* jobB(this: Scope) {
yield wait(200);
return 2;
}
scope.launch<void>(function* () {
// Runs both jobs concurrently.
const [resultA, resultB] = yield* this.suspend2(
this.callAsync(jobA),
this.callAsync(jobB),
);
console.log(`${resultA} ${resultB}`);
});
scope.launch<void>(function* () {
// Races jobA with jobB to get the faster result. Cancels the slower job.
const fastestResult = yield* this.suspend(race(
this.callAsync(jobA),
this.callAsync(jobB),
));
console.log(fastestResult);
});