UNPKG

cf-containers

Version:

TypeScript helper class for PartyKit durable object containers

962 lines (955 loc) 31.7 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { Container: () => Container, getContainer: () => getContainer, loadBalance: () => loadBalance }); module.exports = __toCommonJS(index_exports); // node_modules/nanoid/index.js var import_node_crypto = require("crypto"); // node_modules/nanoid/url-alphabet/index.js var urlAlphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict"; // node_modules/nanoid/index.js var POOL_SIZE_MULTIPLIER = 128; var pool; var poolOffset; function fillPool(bytes) { if (!pool || pool.length < bytes) { pool = Buffer.allocUnsafe(bytes * POOL_SIZE_MULTIPLIER); import_node_crypto.webcrypto.getRandomValues(pool); poolOffset = 0; } else if (poolOffset + bytes > pool.length) { import_node_crypto.webcrypto.getRandomValues(pool); poolOffset = 0; } poolOffset += bytes; } function nanoid(size = 21) { fillPool(size |= 0); let id = ""; for (let i = poolOffset - size; i < poolOffset; i++) { id += urlAlphabet[pool[i] & 63]; } return id; } // src/lib/container.ts var import_cloudflare_workers = require("cloudflare:workers"); function isNoInstanceError(error) { if (!(error instanceof Error)) { return false; } return error.message.includes( "there is no container instance that can be provided to this durable object" ); } function isRuntimeSignalledError(error) { if (!(error instanceof Error)) { return false; } return error.message.includes(runtimeSignalledError); } function isNotListeningError(error) { if (!(error instanceof Error)) { return false; } return error.message.includes(notListeningError); } function isContainerExitNonZeroError(error) { if (!(error instanceof Error)) { return false; } return error.message.includes(containerExitWithError); } var runtimeSignalledError = "runtime signalled the container to exit:"; var containerExitWithError = "container exited with unexpected exit code:"; var notListeningError = "the container is not listening"; function getExitCodeFromError(error) { if (!(error instanceof Error)) { return null; } if (isRuntimeSignalledError(error)) { return +error.message.slice( error.message.indexOf(runtimeSignalledError) + runtimeSignalledError.length + 1 ); } if (isContainerExitNonZeroError(error)) { return +error.message.slice( error.message.indexOf(containerExitWithError) + containerExitWithError.length + 1 ); } return null; } var containerStateKey = "__CF_CONTAINER_STATE"; var ContainerState = class { constructor(storage) { this.storage = storage; } status; async setRunning() { this.status = { lastChange: Date.now(), status: "running" }; await this.update(); } async update() { if (!this.status) throw new Error("status should be init"); await this.storage.put(containerStateKey, this.status); } async setHealthy() { this.status = { lastChange: Date.now(), status: "healthy" }; await this.update(); } async setStopping() { this.status = { status: "stopping", lastChange: Date.now() }; await this.update(); } async setStopped() { this.status = { status: "stopped", lastChange: Date.now() }; await this.update(); } async setStoppedWithCode(exitCode) { this.status = { status: "stopped_with_code", lastChange: Date.now(), exitCode }; await this.update(); } async getState() { if (!this.status) { const state = await this.storage.get(containerStateKey); if (!state) { this.status = { status: "stopped", lastChange: Date.now() }; await this.update(); } else { this.status = state; } } return this.status; } }; var Container = class extends import_cloudflare_workers.DurableObject { // Default port for the container (undefined means no default port) defaultPort; // Timeout after which the container will sleep if no activity // // The signal sent to the container by default is a SIGTERM. // The container won't get a SIGKILL if this threshold is triggered. sleepAfter = "5m"; #sleepAfterMs = 0; // Internal tracking for sleep timeout task #sleepTimeoutTaskId = null; // Whether to require manual container start (if true, it won't start automatically) manualStart = false; /** * Container configuration properties * Set these properties directly in your container instance */ envVars = {}; entrypoint; enableInternet = true; /** * Execute SQL queries against the Container's database */ sql(strings, ...values) { let query = ""; try { query = strings.reduce((acc, str, i) => acc + str + (i < values.length ? "?" : ""), ""); return [...this.ctx.storage.sql.exec(query, ...values)]; } catch (e) { console.error(`Failed to execute SQL query: ${query}`, e); throw this.onError(e); } } container; state; constructor(ctx, env, options) { super(ctx, env); this.state = new ContainerState(this.ctx.storage); this.ctx.blockConcurrencyWhile(async () => { this.renewActivityTimeout(); await this.#scheduleNextAlarm(); }); if (ctx.container === void 0) { throw new Error( "Container is not enabled for this durable object class. Have you correctly setup your wrangler.toml?" ); } this.container = ctx.container; if (options) { if (options.defaultPort !== void 0) this.defaultPort = options.defaultPort; if (options.sleepAfter !== void 0) this.sleepAfter = options.sleepAfter; if (options.explicitContainerStart !== void 0) this.manualStart = options.explicitContainerStart; } this.sql` CREATE TABLE IF NOT EXISTS container_schedules ( id TEXT PRIMARY KEY NOT NULL DEFAULT (randomblob(9)), callback TEXT NOT NULL, payload TEXT, type TEXT NOT NULL CHECK(type IN ('scheduled', 'delayed')), time INTEGER NOT NULL, delayInSeconds INTEGER, created_at INTEGER DEFAULT (unixepoch()) ) `; this.ctx.blockConcurrencyWhile(async () => { if (this.shouldAutoStart()) { await this.startAndWaitForPorts(); } else if (this.container.running && !this.monitor) { this.monitor = this.container.monitor(); this.setupMonitor(); } }); } /** * Determine if container should auto-start */ shouldAutoStart() { return !this.manualStart; } monitor; /** * Start the container if it's not running and set up monitoring * * This method handles the core container startup process without waiting for ports to be ready. * It will automatically retry if the container fails to start, up to maxTries attempts. * * It's useful when you need to: * - Start a container without blocking until a port is available * - Initialize a container that doesn't expose ports * - Perform custom port availability checks separately * * The method applies the container configuration from your instance properties by default, but allows * overriding these values for this specific startup: * - Environment variables (defaults to this.envVars) * - Custom entrypoint commands (defaults to this.entrypoint) * - Internet access settings (defaults to this.enableInternet) * * It also sets up monitoring to track container lifecycle events and automatically * calls the onStop handler when the container terminates. * * @example * // Basic usage in a custom Container implementation * async customInitialize() { * // Start the container without waiting for a port * await this.startContainer(); * * // Perform additional initialization steps * // that don't require port access * } * * @example * // Start with custom configuration * await this.startContainer({ * envVars: { DEBUG: 'true', NODE_ENV: 'development' }, * entrypoint: ['npm', 'run', 'dev'], * enableInternet: false * }); * * @param options - Optional configuration to override instance defaults * @returns A promise that resolves when the container start command has been issued * @throws Error if no container context is available or if all start attempts fail */ async startContainer(options) { await this.#startContainerIfNotRunning(options); this.setupMonitor(); } async #startContainerIfNotRunning(options) { if (this.container.running) { if (!this.monitor) { this.monitor = this.container.monitor(); } return this.monitor; } await this.state.setRunning(); for (let tries = 0; ; tries++) { const envVars = options?.envVars ?? this.envVars; const entrypoint = options?.entrypoint ?? this.entrypoint; const enableInternet = options?.enableInternet ?? this.enableInternet; const startConfig = { enableInternet }; if (envVars && Object.keys(envVars).length > 0) startConfig.env = envVars; if (entrypoint) startConfig.entrypoint = entrypoint; await this.#cancelSleepTimeout(); const handleError = async () => { const err = await this.monitor?.catch((err2) => err2); if (typeof err === "number") { const toThrow = new Error( `Error starting container, early exit code 0 before we could check for healthiness, did it crash early?` ); try { await this.onError(toThrow); } catch { } throw toThrow; } else if (!isNoInstanceError(err)) { try { await this.onError(err); } catch { } throw err; } }; if (!this.container.running) { if (tries > 0) { await handleError(); } this.container.start(startConfig); this.monitor = this.container.monitor(); } this.renewActivityTimeout(); const port = this.container.getTcpPort(33); try { await port.fetch("http://containerstarthealthcheck"); return this.monitor; } catch (error) { if (isNotListeningError(error) && this.container.running) { return; } if (!this.container.running && isNotListeningError(error)) { try { await this.onError(new Error(`container crashed when checking if it was ready`)); } catch { } throw error; } console.warn( "Error checking if container is ready:", error instanceof Error ? error.message : String(error) ); await new Promise((res) => setTimeout(res, 500)); continue; } } } /** * Required ports that should be checked for availability during container startup * Override this in your subclass to specify ports that must be ready */ requiredPorts; // synchronises container state with the container source of truth to process events async #syncPendingStoppedEvents() { const state = await this.state.getState(); if (!this.container.running && state.status === "healthy") { await new Promise( (res) => ( // setTimeout to process monitor() just in case setTimeout(async () => { await this.ctx.blockConcurrencyWhile(async () => { const newState = await this.state.getState(); if (newState.status !== state.status) { return; } await this.onStop({ exitCode: 0, reason: "exit" }); await this.state.setStopped(); }); res(true); }) ) ); return; } if (!this.container.running && state.status === "stopped_with_code") { await new Promise( (res) => ( // setTimeout to process monitor() just in case setTimeout(async () => { await this.ctx.blockConcurrencyWhile(async () => { const newState = await this.state.getState(); if (newState.status !== state.status) { return; } await this.onStop({ exitCode: state.exitCode ?? 0, reason: "exit" }); await this.state.setStopped(); res(true); }); }) ) ); return; } } setupMonitor() { this.monitor?.then(async () => { await this.ctx.blockConcurrencyWhile(async () => { await this.state.setStoppedWithCode(0); await this.onStop({ exitCode: 0, reason: "exit" }); await this.state.setStopped(); }); }).catch(async (error) => { if (isNoInstanceError(error)) { return; } const exitCode = getExitCodeFromError(error); if (exitCode !== null) { this.ctx.blockConcurrencyWhile(async () => { await this.state.setStoppedWithCode(exitCode); await this.onStop({ exitCode, reason: isRuntimeSignalledError(error) ? "runtime_signal" : "exit" }); await this.state.setStopped(); }); return; } try { await this.onError(error); } catch { } }).finally(() => { this.alarmSleepResolve("monitor finally"); }); } /** * Start the container and wait for ports to be available * Based on containers-starter-go implementation * * This method builds on startContainer by adding port availability verification: * 1. Calls startContainer to ensure the container is running * 2. If no ports are specified and requiredPorts is not set, it uses defaultPort (if set) * 3. If no ports can be determined, it calls onStart and renewActivityTimeout immediately * 4. For each specified port, it polls until the port is available or maxTries is reached * 5. When all ports are available, it triggers onStart and renewActivityTimeout * * The method prioritizes port sources in this order: * 1. Ports specified directly in the method call * 2. requiredPorts class property (if set) * 3. defaultPort (if neither of the above is specified) * * @param ports - The ports to wait for (if undefined, uses requiredPorts or defaultPort) * @param maxTries - Maximum number of attempts to connect to each port before failing * @throws Error if port checks fail after maxTries attempts */ async startAndWaitForPorts(ports, maxTries = 10) { const state = await this.state.getState(); if (state.status === "healthy" && this.container.running) { if (this.container.running && !this.monitor) { await this.#startContainerIfNotRunning(); this.setupMonitor(); } return; } await this.#syncPendingStoppedEvents(); await this.ctx.blockConcurrencyWhile(async () => { await this.#startContainerIfNotRunning(); let portsToCheck = []; if (ports !== void 0) { portsToCheck = Array.isArray(ports) ? ports : [ports]; } else if (this.requiredPorts && this.requiredPorts.length > 0) { portsToCheck = [...this.requiredPorts]; } else if (this.defaultPort !== void 0) { portsToCheck = [this.defaultPort]; } for (const port of portsToCheck) { const tcpPort = this.container.getTcpPort(port); let portReady = false; for (let i = 0; i < maxTries && !portReady; i++) { try { await tcpPort.fetch("http://ping"); portReady = true; console.log(`Port ${port} is ready`); } catch (e) { const errorMessage = e instanceof Error ? e.message : String(e); console.warn(`Error checking ${port}: ${errorMessage}`); if (!this.container.running) { try { await this.onError( new Error( `Container crashed while checking for ports, did you setup the entrypoint correctly?` ) ); } catch { } throw e; } if (i === maxTries - 1) { try { this.onError( `Failed to verify port ${port} is available after ${maxTries} attempts, last error: ${errorMessage}` ); } catch { } throw e; } await new Promise((resolve) => setTimeout(resolve, 300)); } } } }); this.setupMonitor(); await this.ctx.blockConcurrencyWhile(async () => { await this.onStart(); await this.state.setHealthy(); }); } /** * Send a request to the container (HTTP or WebSocket) using standard fetch API signature * Based on containers-starter-go implementation * * This method handles both HTTP and WebSocket requests to the container. * For WebSocket requests, it sets up bidirectional message forwarding with proper * activity timeout renewal. * * Method supports multiple signatures to match standard fetch API: * - containerFetch(request: Request, port?: number) * - containerFetch(url: string | URL, init?: RequestInit, port?: number) * * @param requestOrUrl The request object or URL string/object to send to the container * @param portOrInit Port number or fetch RequestInit options * @param portParam Optional port number when using URL+init signature * @returns A Response from the container, or WebSocket connection */ async containerFetch(requestOrUrl, portOrInit, portParam) { let request; let port; if (requestOrUrl instanceof Request) { request = requestOrUrl; port = typeof portOrInit === "number" ? portOrInit : void 0; } else { const url = typeof requestOrUrl === "string" ? requestOrUrl : requestOrUrl.toString(); const init = typeof portOrInit === "number" ? {} : portOrInit || {}; port = typeof portOrInit === "number" ? portOrInit : typeof portParam === "number" ? portParam : void 0; request = new Request(url, init); } if (port === void 0 && this.defaultPort === void 0) { throw new Error( "No port specified for container fetch. Set defaultPort or specify a port parameter." ); } const targetPort = port ?? this.defaultPort; const state = await this.state.getState(); if (!this.container.running || state.status !== "healthy") { try { await this.startAndWaitForPorts(targetPort); } catch (e) { return new Response( `Failed to start container: ${e instanceof Error ? e.message : String(e)}`, { status: 500 } ); } } const tcpPort = this.container.getTcpPort(targetPort); const containerUrl = request.url.replace("https:", "http:"); try { this.renewActivityTimeout(); const res = await tcpPort.fetch(containerUrl, request); if (res.webSocket) { this.websocketCount++; res.webSocket.addEventListener("close", async () => { this.websocketCount--; if (this.websocketCount === 0) { await this.#scheduleSleepTimeout(); } }); } return res; } catch (e) { console.error(`Error proxying request to container ${this.ctx.id}:`, e); return new Response( `Error proxying request to container: ${e instanceof Error ? e.message : String(e)}`, { status: 500 } ); } } // websocketCount keeps track of the number of websocket connections to the container websocketCount = 0; /** * Shuts down the container. */ async stopContainer(signal = 15) { this.container.signal(signal); } /** * Destroys the container. It will trigger onError instead of onStop. */ async destroyContainer() { await this.container.destroy(); } /** * Lifecycle method called when container starts successfully * Override this method in subclasses to handle container start events */ onStart() { } /** * Lifecycle method called when container shuts down * Override this method in subclasses to handle Container stopped events */ onStop(_) { } /** * Error handler for container errors * Override this method in subclasses to handle container errors */ onError(error) { console.error("Container error:", error); throw error; } /** * Try-catch wrapper for async operations */ async #tryCatch(fn) { try { return await fn(); } catch (e) { this.onError(e); throw e; } } /** * Parse a time expression into seconds * @private * @param timeExpression Time expression (number or string like "5m", "30s", "1h") * @returns Number of seconds */ #parseTimeExpression(timeExpression) { if (typeof timeExpression === "number") { return timeExpression; } if (typeof timeExpression === "string") { const match = timeExpression.match(/^(\d+)([smh])$/); if (!match) { throw new Error(`invalid time expression ${timeExpression}`); } const value = parseInt(match[1]); const unit = match[2]; switch (unit) { case "s": return value; case "m": return value * 60; case "h": return value * 60 * 60; default: throw new Error(`unknown time unit ${unit}`); } } throw new Error(`invalid type for a time expression: ${typeof timeExpression}`); } /** * Schedule a Container stopped after the specified sleep timeout * @private */ async #scheduleSleepTimeout() { const timeoutInSeconds = this.#parseTimeExpression(this.sleepAfter); await this.#cancelSleepTimeout(); const { taskId } = await this.schedule(timeoutInSeconds, "stopDueToInactivity"); this.#sleepTimeoutTaskId = taskId; } /** * Cancel the scheduled sleep timeout if one exists * @private */ async #cancelSleepTimeout() { if (this.#sleepTimeoutTaskId) { try { await this.unschedule(this.#sleepTimeoutTaskId); } catch (e) { } this.#sleepTimeoutTaskId = null; } } /** * Schedule a task to be executed in the future * @template T Type of the payload data * @param when When to execute the task (Date object or number of seconds delay) * @param callback Name of the method to call * @param payload Data to pass to the callback * @returns Schedule object representing the scheduled task */ async schedule(when, callback, payload) { const id = nanoid(9); if (typeof callback !== "string") { throw new Error("Callback must be a string (method name)"); } if (typeof this[callback] !== "function") { throw new Error(`this.${callback} is not a function`); } if (when instanceof Date) { const timestamp = Math.floor(when.getTime() / 1e3); this.sql` INSERT OR REPLACE INTO container_schedules (id, callback, payload, type, time) VALUES (${id}, ${callback}, ${JSON.stringify(payload)}, 'scheduled', ${timestamp}) `; await this.#scheduleNextAlarm(); return { taskId: id, callback, payload, time: timestamp, type: "scheduled" }; } if (typeof when === "number") { const time = Math.floor(Date.now() / 1e3 + when); this.sql` INSERT OR REPLACE INTO container_schedules (id, callback, payload, type, delayInSeconds, time) VALUES (${id}, ${callback}, ${JSON.stringify(payload)}, 'delayed', ${when}, ${time}) `; await this.#scheduleNextAlarm(); return { taskId: id, callback, payload, delayInSeconds: when, time, type: "delayed" }; } throw new Error("Invalid schedule type. 'when' must be a Date or number of seconds"); } /** * Schedule the next alarm based on upcoming tasks * @private */ async #scheduleNextAlarm(ms = 1e3) { const existingAlarm = await this.ctx.storage.getAlarm(); const nextTime = ms + Date.now(); if (existingAlarm === null || existingAlarm > nextTime || existingAlarm < Date.now()) { await this.ctx.storage.setAlarm(nextTime); await this.ctx.storage.sync(); this.alarmSleepResolve("scheduling next alarm"); } } /** * Cancel a scheduled task * @param id ID of the task to cancel */ async unschedule(id) { this.sql`DELETE FROM container_schedules WHERE id = ${id}`; await this.#scheduleNextAlarm(); } /** * Get a scheduled task by ID * @template T Type of the payload data * @param id ID of the scheduled task * @returns The Schedule object or undefined if not found */ async getSchedule(id) { const result = this.sql` SELECT * FROM container_schedules WHERE id = ${id} LIMIT 1 `; if (!result || result.length === 0) { return void 0; } const schedule = result[0]; let payload; try { payload = JSON.parse(schedule.payload); } catch (e) { console.error(`Error parsing payload for schedule ${id}:`, e); payload = void 0; } if (schedule.type === "delayed") { return { taskId: schedule.id, callback: schedule.callback, payload, type: "delayed", time: schedule.time, delayInSeconds: schedule.delayInSeconds }; } return { taskId: schedule.id, callback: schedule.callback, payload, type: "scheduled", time: schedule.time }; } /** * Get scheduled tasks matching the given criteria * @template T Type of the payload data * @param criteria Criteria to filter schedules * @returns Array of matching Schedule objects */ getSchedules(criteria = {}) { let query = "SELECT * FROM container_schedules WHERE 1=1"; const params = []; if (criteria.id) { query += " AND id = ?"; params.push(criteria.id); } if (criteria.type) { query += " AND type = ?"; params.push(criteria.type); } if (criteria.timeRange) { if (criteria.timeRange.start) { query += " AND time >= ?"; params.push(Math.floor(criteria.timeRange.start.getTime() / 1e3)); } if (criteria.timeRange.end) { query += " AND time <= ?"; params.push(Math.floor(criteria.timeRange.end.getTime() / 1e3)); } } const result = this.ctx.storage.sql.exec(query, ...params); return result.toArray().map((row) => { let payload; try { payload = JSON.parse(row.payload); } catch (e) { console.error(`Error parsing payload for schedule ${row.id}:`, e); payload = void 0; } if (row.type === "delayed") { return { taskId: row.id, callback: row.callback, payload, type: "delayed", time: row.time, delayInSeconds: row.delayInSeconds }; } return { taskId: row.id, callback: row.callback, payload, type: "scheduled", time: row.time }; }); } /** * Method called when an alarm fires * Executes any scheduled tasks that are due */ async alarm(alarmProps) { const maxRetries = 3; if (alarmProps.isRetry && alarmProps.retryCount > maxRetries) { await this.#scheduleNextAlarm(); return; } await this.#tryCatch(async () => { const now = Math.floor(Date.now() / 1e3); const result = this.sql` SELECT * FROM container_schedules; `; let maxTime = 0; for (const row of result) { if (row.time > now) { maxTime = Math.max(maxTime, row.time * 1e3); continue; } const callback = this[row.callback]; if (!callback || typeof callback !== "function") { console.error(`Callback ${row.callback} not found or is not a function`); continue; } const schedule = this.getSchedule(row.id); try { const payload = row.payload ? JSON.parse(row.payload) : void 0; await callback.call(this, payload, await schedule); } catch (e) { console.error(`Error executing scheduled callback "${row.callback}":`, e); } this.sql`DELETE FROM container_schedules WHERE id = ${row.id}`; } await this.#syncPendingStoppedEvents(); if (!this.container.running) { return; } await this.#scheduleNextAlarm(); if (this.isActivityExpired()) { await this.stopDueToInactivity(); return; } let resolve = (_) => { }; this.alarmSleepPromise = new Promise((res) => { this.alarmSleepResolve = (val) => { res(val); }; resolve = res; }); maxTime = maxTime === 0 ? Date.now() + 60 * 3 * 1e3 : maxTime; maxTime = Math.min(maxTime, this.#sleepAfterMs); const timeout = Math.max(0, maxTime - Date.now()); const timeoutRef = setTimeout(() => { resolve("setTimeout"); }, timeout); await this.alarmSleepPromise; clearTimeout(timeoutRef); }); } alarmSleepPromise; alarmSleepResolve = (_) => { }; /** * Renew the container's activity timeout * Call this method whenever there is activity on the container */ renewActivityTimeout() { const timeoutInMs = this.#parseTimeExpression(this.sleepAfter) * 1e3; this.#sleepAfterMs = Date.now() + timeoutInMs; } isActivityExpired() { return this.#sleepAfterMs <= Date.now(); } /** * Method called by scheduled task to stop the container due to inactivity */ async stopDueToInactivity() { if (!this.container.running) { return; } if (this.websocketCount > 0) { return; } await this.stopContainer(); } /** * Handle fetch requests to the Container * Default implementation forwards all HTTP and WebSocket requests to the container * Override this in your subclass to specify a port or implement custom request handling * * @param request The request to handle */ async fetch(request) { if (this.defaultPort === void 0) { return new Response( "No default port configured for this container. Override the fetch method or set defaultPort in your Container subclass.", { status: 500 } ); } return await this.containerFetch(request, this.defaultPort); } }; // src/lib/utils.ts async function loadBalance(binding, instances = 3) { const id = Math.floor(Math.random() * instances).toString(); const objectId = binding.idFromName(`instance-${id}`); return binding.get(objectId); } var singletonContainerId = "cf-singleton-container"; function getContainer(binding) { const objectId = binding.idFromName(singletonContainerId); return binding.get(objectId); } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { Container, getContainer, loadBalance });