import { fileURLToPath } from "node:url"; import { ServiceProvider, type Application, Router, HttpContext, RequestContext, Pipeline, registerFileRouteResolver, FrameworkEvents, config, } from "@zerotal/core"; import { WebSocketConnected, WebSocketDisconnected, FlowActionHandled, } from "../frameworkEvents.ts"; import { installFlowObservability } from "../observability.ts"; import { DEFAULT_PERSISTENT_MIDDLEWARE } from "../config.ts"; import type { AppEnvironment, Container, MiddlewareClass, NextFn } from "@zerotal/core"; import type { WebSocketHandlers } from "@zerotal/core"; import { isProdLike, deployEnv } from "@zerotal/core"; import type { CallFrame, Snapshot } from "../types.ts"; import { getPage, getAllowedMethods, getRenderlessMethods, BUILTIN_ACTIONS } from "../registry.ts"; import { hydrate, dehydrate, encodeSnapshotDelta, FlowIntegrityError, FlowSnapshotOwnershipError, warnIfLarge, type SnapshotDelta, } from "../dehydrate.ts"; import { _renderFlowPage } from "../jsx-runtime.ts"; import { populatePresence } from "../presence.ts"; import { populateShared, snapshotSharedValues, commitShared } from "../shared.ts"; import { persistDurable } from "../durable.ts"; import type { Component } from "../Component.ts"; import { flowRoute, registerFlowFileRoute, _persistSessionProps } from "../router.ts"; import type { PageClassWithMeta } from "../registry.ts"; import { getTaskMethods } from "../decorators.ts"; import { ValidationError } from "../validation.ts"; import { makeSignedRef } from "../uploads/TemporaryUploadedFile.ts"; import { isAllowedOrigin } from "@zerotal/core/http"; // ── In-memory runtime.js bundle (built once at boot) ───────────────────────── /** * The `window.__zerotalRoutes = {...}` line prepended to the served runtime * bundle, which `installClientRoutes()` reads back on load. * * Served as JavaScript, not embedded in HTML, so the `` hazard the * state island guards against does not apply here — this response is never * parsed as markup. Plain `JSON.stringify` is enough. * * @internal exported for tests. */ export function _routeTablePrelude(): string { const table = Object.fromEntries(Router.namedRoutes); return `window.__zerotalRoutes=${JSON.stringify(table)};\n`; } /** True when this process is the `serve --dev-worker` (dev fast refresh gates on it). */ const _isDevWorker = Bun.argv.includes("--dev-worker"); // ── Running @task registry (for cancellation) ───────────────────────────────── // componentId → the AbortController of its currently-running @task, plus the connection // that started it. A `$cancel` frame (sent out-of-band by the client, bypassing the // per-component send queue) aborts it; the task observes `this.signal` / `this.cancelled` // and stops cooperatively. // // The owning socket is recorded because `$cancel` runs before component lookup, before // hydrate() — the only HMAC check — and before rate limiting, and this map is // process-global keyed only by component id. Without the owner check, any connected user // could kill any other user's running task given its 32-bit id, which leaks in screenshots // and bug reports. interface RunningTask { controller: AbortController; owner: FlowBunWs; } const _runningTasks = new Map(); /** How often (ms) a running @task flushes a throttled streaming patch of its changed state. */ const TASK_FLUSH_MS = 80; /** Dev-only error detail carried on the error frame so the client can render the error overlay. */ interface DevErrorInfo { name: string; message: string; stack?: string | undefined; action: string; } /** Extract overlay-ready detail from a thrown value. @internal (exported for tests). */ export function _devErrorInfo(error: unknown, action: string): DevErrorInfo { const e = error instanceof Error ? error : new Error(String(error)); return { name: e.name || "Error", message: e.message, stack: e.stack, action }; } let _runtimeBundle: string | null = null; /** * The `/__flow/enhance.js` bundle — the standalone plain-form enhancement. * * Separate from the runtime bundle on purpose. `/__flow/runtime.js` is injected * only by `Router.flow` routes, and the whole point of `
` is a * page with no Flow component on it, which is therefore never served that script. * It is also tiny and dependency-free, so an app that wants a form not to flash * does not pay for Alpine and a WebSocket bridge to get it. */ let _enhanceBundle: string | null = null; /** * CSP-safe mode flag (no 'unsafe-eval'). Set `flow.cspSafe` in `config/flow.ts`, * or the ZT_FLOW_CSP_SAFE / APP_CSP_SAFE env flag when there is no config file. */ function _isCspSafe(): boolean { const fromEnv = Bun.env["ZT_FLOW_CSP_SAFE"] ?? Bun.env["APP_CSP_SAFE"]; return config.safe("flow.cspSafe", fromEnv === "true" || fromEnv === "1"); } /** @internal — read by router.ts to build