cf-containers
Version:
TypeScript helper class for PartyKit durable object containers
962 lines (955 loc) • 31.7 kB
JavaScript
"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
});