UNPKG

@intuitionrobotics/thunderstorm

Version:
136 lines 6.3 kB
import { BadImplementationException, BeLogged, LogClient, LogClient_Function, LogClient_Terminal, Module } from "@intuitionrobotics/ts-common"; import { FirebaseFunction } from '@intuitionrobotics/firebase/backend-functions'; import {} from "../modules/server/HttpServer.js"; import { ApiFunction } from "../modules/server/ApiFunction.js"; import { apiDebug } from "../modules/server/server-api.js"; import { BaseStorm } from "./BaseStorm.js"; import {} from "express"; import { DefaultApiErrorMessageComposer } from "../modules/server/server-errors.js"; import { FirebaseModule } from "@intuitionrobotics/firebase/backend"; export class Storm extends BaseStorm { // v2.0: consumers build an Express Router by hand and hand it here. // The framework `app.use`s it during build(); there is no other path. routes; functions = []; express; logClient = LogClient_Function; onDestroy; onStart; apiFunctionOptions = {}; errorMessageComposer = DefaultApiErrorMessageComposer(); constructor() { super(); this.addModules(FirebaseModule); } setErrorMessageComposer(errorMessageComposer) { this.errorMessageComposer = errorMessageComposer; return this; } setApp(app) { this.express = app; return this; } // Runtime options (memory, region, generation, ...) for the Express "api" // HTTPS function that Storm builds internally. Replaces the old mutable // static Firebase_ExpressFunction.config. setApiFunctionOptions(options) { this.apiFunctionOptions = options; return this; } setLogClient(logClient) { this.logClient = logClient; return this; } getLogClient() { return this.logClient; } setOnDestroy(onDestroy) { this.onDestroy = onDestroy; return this; } setOnStart(onStart) { this.onStart = onStart; return this; } static getInstance() { return Storm.instance; } init() { BeLogged.addClient(process.env.GCLOUD_PROJECT && process.env.FUNCTIONS_EMULATOR ? LogClient_Terminal : this.logClient); apiDebug.enabled = !!this.config?.isDebug; // The backend NEVER eagerly initializes modules (deliberately not super.init()). // Registration only wires references; module resources resolve lazily on first // use, so a function's container only allocates what its handler actually touches. this.modules.forEach(module => module.setManager(this)); this.assertNoLegacyModuleInit(); this.modules.forEach(module => module.validate()); return this; } // Module.init() is never called on the backend — a module overriding it is broken // by definition (its setup would silently never run). Fail loudly at build time, // with an opt-in bridge for consumers mid-migration. assertNoLegacyModuleInit() { // @ts-expect-error init is protected; comparing prototype slots to detect overrides const offenders = this.modules.filter(module => module.init !== Module.prototype.init); if (offenders.length === 0) return; const names = offenders.map(module => module.getName()).join(", "); if (process.env.THUNDERSTORM_LEGACY_MODULE_INIT === "true") { this.logWarning(`LEGACY init() bridge active — eagerly initializing: [${names}]. ` + `Migrate these modules to constructor/lazy getters and drop init() (run 'thunderstorm-codemod check-module-init').`); offenders.forEach(module => { // @ts-expect-error TS struggles with this dynamic typing module.init(); // @ts-expect-error TS struggles with this dynamic typing module.initiated = true; }); return; } throw new BadImplementationException(`Backend modules must not override init() — Storm never calls it, so their setup would silently never run. ` + `Offenders: [${names}]. Move the work to the constructor (trivial, config-independent) or lazy getters (anything allocating), ` + `or set THUNDERSTORM_LEGACY_MODULE_INIT=true as a temporary bridge. See docs/migrating-to-v2.md.`); } // The caller passes a fully-built Express Router; Storm wires it onto the // HttpServer's express app at build() time. setRoutes(routes) { this.routes = routes; return this; } /** * @deprecated No-op. The URL prefix comes from the function name on Cloud * Functions (the `api` function serves `…/api/**`), so the router always * mounts at root — there is no standalone-server mode that needed a prefix. */ setInitialRoutePath(_initialPath) { return this; } build(onStarted) { // The Express API is a regular module now (ApiFunction) — Storm no longer // hand-builds it. When none is registered, an implicit default named "api" // is created from the legacy builder methods (setApp/setRoutes/ // setApiFunctionOptions); registering ANY ApiFunction instance disables // that default. const registeredApis = this.modules.filter(module => module instanceof ApiFunction); if (registeredApis.length === 0) this.addModules(new ApiFunction("api", { app: this.express, router: this.routes, options: this.apiFunctionOptions })); else if (this.routes) this.logWarning("Storm.setRoutes() is ignored when explicit ApiFunction instances are registered — pass each instance its own router."); this.functions = this.modules.filter((module) => module instanceof FirebaseFunction); if (this.onStart) this.functions.forEach(func => func.addOnStart(this.onStart)); if (this.onDestroy) this.functions.forEach(func => func.addOnDestroy(this.onDestroy)); this.init(); if (onStarted) onStarted().catch(e => this.logError(e)); return this.functions.reduce((toRet, _function) => { toRet[_function.getName()] = _function.getFunction(); return toRet; }, {}); } } //# sourceMappingURL=Storm.js.map