UNPKG

alepha

Version:

Easy-to-use modern TypeScript framework for building many kind of applications.

121 lines (120 loc) 4.56 kB
import { $hook, $inject, $module, Alepha } from "alepha"; import { $logger } from "alepha/logger"; //#region ../../src/background/providers/BackgroundTaskProvider.ts /** * Runs fire-and-forget work that should outlive the request that scheduled it * (sending a welcome email, an audit write, a webhook) without blocking the * response. * * On a long-lived runtime (Node, Vercel) the event loop keeps the promise * alive, so the only job here is to detach the task from the caller and never * leak an unhandled rejection. On serverless/edge runtimes that freeze the * isolate once the response is returned (Cloudflare Workers) the * {@link WorkerdBackgroundTaskProvider} variant additionally wraps the task in * `executionCtx.waitUntil` — call sites stay platform-agnostic and only ever * call {@link defer}. * * In-flight tasks are tracked and awaited on shutdown (`flush`), so graceful * stop, `run({ once })`, and unit tests don't silently drop work. */ var BackgroundTaskProvider = class { alepha = $inject(Alepha); log = $logger(); /** In-flight deferred tasks, awaited on stop. */ pending = /* @__PURE__ */ new Set(); /** * Schedule `task` to run in the background. Returns immediately — the task is * detached onto a microtask, so the caller (e.g. an HTTP handler) is never * blocked and a synchronous throw inside `task` can't escape into it. Errors * are caught and logged. */ defer(task) { const promise = Promise.resolve().then(task).then(() => void 0, (error) => { this.log.warn("Background task failed", error); }); this.pending.add(promise); promise.finally(() => { this.pending.delete(promise); }); this.keepAlive(promise); } /** * Hook for platform variants to keep the runtime alive until `promise` * settles. No-op on long-lived runtimes (the event loop already does). The * Cloudflare variant overrides this with `executionCtx.waitUntil`. */ keepAlive(_promise) {} /** * Await all in-flight tasks (best-effort). Called on `stop` so shutdown and * tests don't drop deferred work. */ async flush() { await Promise.allSettled([...this.pending]); } onStop = $hook({ on: "stop", handler: () => this.flush() }); }; //#endregion //#region ../../src/background/providers/WorkerdBackgroundTaskProvider.ts /** * Cloudflare Workers variant of {@link BackgroundTaskProvider}. * * Cloudflare freezes the isolate once the response is returned, so a plain * fire-and-forget promise would be killed mid-flight. The platform exposes a * per-request `executionCtx.waitUntil(promise)` that keeps the isolate alive * until `promise` settles; the request entry point (generated by the Cloudflare * build) stashes it in the Alepha store under `cloudflare.waitUntil`. * * This is the ONE place that reads that key — call sites use * {@link BackgroundTaskProvider.defer} and stay platform-agnostic. * * `waitUntil` is request-scoped, so `defer()` must be called within the * request's async context (which is where background work is scheduled). Outside * a request (e.g. a cron tick) the key is absent and we fall back to the base * fire-and-track behaviour. */ var WorkerdBackgroundTaskProvider = class extends BackgroundTaskProvider { keepAlive(promise) { const waitUntil = this.alepha.store.get("cloudflare.waitUntil"); if (typeof waitUntil !== "function") return; try { waitUntil(promise); } catch (error) { this.log.debug("waitUntil rejected — falling back to fire-and-track", error); } } }; //#endregion //#region ../../src/background/index.ts /** * Fire-and-forget background work that should outlive the request that * scheduled it, without blocking the response. * * Inject {@link BackgroundTaskProvider} and call `defer(() => …)`: * * ```ts * protected readonly background = $inject(BackgroundTaskProvider); * * createUser = $action({ handler: async ({ body }) => { * const user = await this.users.create(body); * this.background.defer(() => this.email.send(welcome(user))); // don't block * return user; * }}); * ``` * * On Node/Vercel the event loop keeps the task alive. On Cloudflare Workers the * `workerd` build swaps in {@link WorkerdBackgroundTaskProvider}, which wraps * the task in `executionCtx.waitUntil` so the isolate isn't frozen at response * time — the call site is identical either way. * * @module alepha.background */ const AlephaBackground = $module({ name: "alepha.background", services: [BackgroundTaskProvider] }); //#endregion export { AlephaBackground, BackgroundTaskProvider, WorkerdBackgroundTaskProvider }; //# sourceMappingURL=index.js.map