@intuitionrobotics/thunderstorm
Version:
136 lines • 6.3 kB
JavaScript
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