UNPKG

suspenders-js

Version:

Asynchronous programming library utilizing coroutines, functional reactive programming and structured concurrency.

175 lines (138 loc) 4.34 kB
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); });