@lunora/container 1.0.0-alpha.63 → 1.0.0-alpha.64
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -0
- package/dist/do/index.d.mts +65 -15
- package/dist/do/index.d.ts +65 -15
- package/dist/do/index.mjs +1 -1
- package/dist/index.d.mts +56 -8
- package/dist/index.d.ts +56 -8
- package/dist/index.mjs +1 -1
- package/dist/packem_shared/CONTAINER_EXEC_PATH-7Hcd6-zs.mjs +1 -0
- package/dist/packem_shared/{ContainerProxy-fTW_pXWY.mjs → ContainerProxy-CfXjS6ZI.mjs} +7 -7
- package/dist/packem_shared/DURABLE_OBJECT_POLICY_FORBIDDEN_KEYS-Dk6cuK7G.mjs +1 -0
- package/dist/packem_shared/createContainerContext-DP-kZaQ1.mjs +1 -0
- package/dist/packem_shared/exec-BYLW70sR.mjs +1 -0
- package/dist/packem_shared/{jurisdiction.d-DPvNG2h5.d.mts → jurisdiction.d-CoLtDVr_.d.mts} +113 -34
- package/dist/packem_shared/{jurisdiction.d-DPvNG2h5.d.ts → jurisdiction.d-CoLtDVr_.d.ts} +113 -34
- package/package.json +2 -2
- package/dist/packem_shared/CONTAINER_EXEC_PATH-ZfG5wgai.mjs +0 -1
- package/dist/packem_shared/containerBindingName-C7SonAIK.mjs +0 -1
- package/dist/packem_shared/createContainerContext-CLvjjJLs.mjs +0 -1
- package/dist/packem_shared/exec-WbCOLswq.mjs +0 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { D as DurableObjectJurisdiction,
|
|
2
|
-
export type { B as BuildImageSource,
|
|
1
|
+
import { C as ContainerSnapshot, a as ContainerRuntimeInstanceType, D as DurableObjectJurisdiction, b as DefaultScheduledContainerConfig, c as DurableObjectScheduledContainerConfig, d as ContainerDefinition, e as ContainerImageSource, N as NormalizedContainerImage } from "./packem_shared/jurisdiction.d-CoLtDVr_.js";
|
|
2
|
+
export type { B as BuildImageSource, f as ContainerConfig, g as ContainerConfigBase, h as ContainerInstanceType, i as ContainerNamedImageSource, j as ContainerReadinessCheck, k as ContainerRollout, l as CustomContainerInstanceType, m as NamedContainerInstanceType, R as RegistryImageSource } from "./packem_shared/jurisdiction.d-CoLtDVr_.js";
|
|
3
3
|
/**
|
|
4
4
|
* Per-call options for `ContainerHandle.exec`.
|
|
5
5
|
*/
|
|
@@ -89,8 +89,26 @@ interface ContainerStartOptions {
|
|
|
89
89
|
* for as long as the instance exists.
|
|
90
90
|
*/
|
|
91
91
|
envVars?: Record<string, string>;
|
|
92
|
+
/**
|
|
93
|
+
* The image to start (`schedulingPolicy: "durable_object"` only): a key of
|
|
94
|
+
* the definition's `images`, or a Cloudflare-managed `"cloudflare/…"` image.
|
|
95
|
+
* Persisted like `envVars`, so the implicit restart after a sleep boots the
|
|
96
|
+
* same image. Mutually exclusive with `snapshot`.
|
|
97
|
+
*/
|
|
98
|
+
image?: string;
|
|
99
|
+
/**
|
|
100
|
+
* The instance size (`schedulingPolicy: "durable_object"` only). Persisted
|
|
101
|
+
* with `image`; defaults to the definition's `instanceType`, else `"lite"`.
|
|
102
|
+
*/
|
|
103
|
+
instanceType?: ContainerRuntimeInstanceType;
|
|
92
104
|
/** Metadata labels attached for metrics/observability. */
|
|
93
105
|
labels?: Record<string, string>;
|
|
106
|
+
/**
|
|
107
|
+
* Restore a filesystem saved by `snapshot()` instead of starting from an
|
|
108
|
+
* image (`schedulingPolicy: "durable_object"` only). Applies to this start
|
|
109
|
+
* only — a later restart after a sleep boots the image again.
|
|
110
|
+
*/
|
|
111
|
+
snapshot?: ContainerSnapshot;
|
|
94
112
|
}
|
|
95
113
|
/**
|
|
96
114
|
* A container instance's runtime state, as returned by `getState()`. Structural — the platform adds fields over time.
|
|
@@ -113,6 +131,10 @@ interface ContainerStubLike {
|
|
|
113
131
|
getState?: () => Promise<ContainerInstanceState>;
|
|
114
132
|
/** The container DO's exec entry (`LunoraContainer.lunoraExec`), the only way a request reaches `/__lunora/exec`. */
|
|
115
133
|
lunoraExec?: (request: Request) => Promise<Response>;
|
|
134
|
+
/** The container DO's snapshot entry (`LunoraContainer.lunoraSnapshot`). */
|
|
135
|
+
lunoraSnapshot?: (options?: {
|
|
136
|
+
name?: string;
|
|
137
|
+
}) => Promise<ContainerSnapshot>;
|
|
116
138
|
removeAllowedHost?: (hostname: string) => Promise<void>;
|
|
117
139
|
removeDeniedHost?: (hostname: string) => Promise<void>;
|
|
118
140
|
renewActivityTimeout?: () => Promise<void>;
|
|
@@ -199,6 +221,15 @@ interface ContainerInstanceHandle extends ContainerHandle {
|
|
|
199
221
|
* nor a WS message — e.g. a long out-of-band job running inside it.
|
|
200
222
|
*/
|
|
201
223
|
renewActivityTimeout: () => Promise<void>;
|
|
224
|
+
/**
|
|
225
|
+
* Save the running container's filesystem and return a handle to restore it
|
|
226
|
+
* with `start({ snapshot })` — here or in another instance of the same
|
|
227
|
+
* container (`schedulingPolicy: "durable_object"` only; Cloudflare beta).
|
|
228
|
+
* Memory and processes are not captured.
|
|
229
|
+
*/
|
|
230
|
+
snapshot: (options?: {
|
|
231
|
+
name?: string;
|
|
232
|
+
}) => Promise<ContainerSnapshot>;
|
|
202
233
|
/** Explicitly start the instance, optionally with per-instance env/entrypoint. */
|
|
203
234
|
start: (options?: ContainerStartOptions) => Promise<void>;
|
|
204
235
|
/** Stop the instance (optionally with a signal); it can start again on the next request. */
|
|
@@ -358,6 +389,14 @@ type ContainerTestHandler = (request: Request, instance: {
|
|
|
358
389
|
* ```
|
|
359
390
|
*/
|
|
360
391
|
declare const createContainerTestContext: (handlers: Record<string, ContainerTestHandler>) => Record<string, ContainerAccessor>;
|
|
392
|
+
/** Whether an image name is a Cloudflare-managed image (`cloudflare/debian-trixie`), which needs no `images` entry. */
|
|
393
|
+
declare const isManagedImage: (name: string) => boolean;
|
|
394
|
+
/**
|
|
395
|
+
* The default-policy keys the `durable_object` policy refuses — wrangler rejects
|
|
396
|
+
* `max_instances` under it, and a rollout has nothing to roll. One table, read by
|
|
397
|
+
* `defineContainer` and by codegen's static discovery.
|
|
398
|
+
*/
|
|
399
|
+
declare const DURABLE_OBJECT_POLICY_FORBIDDEN_KEYS: readonly ["maxInstances", "rollout"];
|
|
361
400
|
/**
|
|
362
401
|
* Normalize a `ContainerImageSource` into the shape wrangler wants: a
|
|
363
402
|
* Dockerfile path + build context for local builds, or a fully-qualified
|
|
@@ -391,10 +430,19 @@ declare const containerBindingName: (exportName: string) => string;
|
|
|
391
430
|
* three derive the tag from this one helper and can never disagree.
|
|
392
431
|
*/
|
|
393
432
|
declare const containerBuildTag: (exportName: string) => string;
|
|
433
|
+
/** Whether a registry reference is digest-pinned in the Cloudflare registry — the only kind a named image accepts. */
|
|
434
|
+
declare const isCloudflareRegistryDigest: (reference: unknown) => boolean;
|
|
394
435
|
/**
|
|
395
436
|
* `defineContainer` is part of the experimental `@lunora/container` API and may change without a major version bump.
|
|
396
437
|
*/
|
|
397
|
-
declare const defineContainer:
|
|
438
|
+
declare const defineContainer: {
|
|
439
|
+
(config: DefaultScheduledContainerConfig): DefaultScheduledContainerConfig & {
|
|
440
|
+
readonly isLunoraContainer: true;
|
|
441
|
+
};
|
|
442
|
+
(config: DurableObjectScheduledContainerConfig): DurableObjectScheduledContainerConfig & {
|
|
443
|
+
readonly isLunoraContainer: true;
|
|
444
|
+
};
|
|
445
|
+
};
|
|
398
446
|
/**
|
|
399
447
|
* True when a value is a `defineContainer` result (the runtime brand check).
|
|
400
448
|
*/
|
|
@@ -428,7 +476,7 @@ type ContainerAccessor,
|
|
|
428
476
|
* (which pulls in `@cloudflare/containers` → `cloudflare:workers`) lives
|
|
429
477
|
* behind the `@lunora/container/do` subpath.
|
|
430
478
|
*/
|
|
431
|
-
type ContainerBindingSpec, type
|
|
479
|
+
type ContainerBindingSpec, type ContainerDefinition,
|
|
432
480
|
/**
|
|
433
481
|
* `@lunora/container` — Cloudflare Containers for Lunora.
|
|
434
482
|
*
|
|
@@ -483,7 +531,7 @@ type ContainerInstanceState,
|
|
|
483
531
|
* (which pulls in `@cloudflare/containers` → `cloudflare:workers`) lives
|
|
484
532
|
* behind the `@lunora/container/do` subpath.
|
|
485
533
|
*/
|
|
486
|
-
type ContainerNamespaceLike,
|
|
534
|
+
type ContainerNamespaceLike, type ContainerRuntimeInstanceType, type ContainerSnapshot,
|
|
487
535
|
/**
|
|
488
536
|
* `@lunora/container` — Cloudflare Containers for Lunora.
|
|
489
537
|
*
|
|
@@ -505,7 +553,7 @@ type ContainerStartOptions,
|
|
|
505
553
|
* (which pulls in `@cloudflare/containers` → `cloudflare:workers`) lives
|
|
506
554
|
* behind the `@lunora/container/do` subpath.
|
|
507
555
|
*/
|
|
508
|
-
type ContainerTestHandler,
|
|
556
|
+
type ContainerTestHandler, DURABLE_OBJECT_POLICY_FORBIDDEN_KEYS, type DefaultScheduledContainerConfig,
|
|
509
557
|
/**
|
|
510
558
|
* `@lunora/container` — Cloudflare Containers for Lunora.
|
|
511
559
|
*
|
|
@@ -516,7 +564,7 @@ type ContainerTestHandler,
|
|
|
516
564
|
* (which pulls in `@cloudflare/containers` → `cloudflare:workers`) lives
|
|
517
565
|
* behind the `@lunora/container/do` subpath.
|
|
518
566
|
*/
|
|
519
|
-
type DurableObjectJurisdiction,
|
|
567
|
+
type DurableObjectJurisdiction, type DurableObjectScheduledContainerConfig,
|
|
520
568
|
/**
|
|
521
569
|
* `@lunora/container` — Cloudflare Containers for Lunora.
|
|
522
570
|
*
|
|
@@ -538,4 +586,4 @@ type InstanceRetryOptions, type NormalizedContainerImage,
|
|
|
538
586
|
* (which pulls in `@cloudflare/containers` → `cloudflare:workers`) lives
|
|
539
587
|
* behind the `@lunora/container/do` subpath.
|
|
540
588
|
*/
|
|
541
|
-
type PoolOptions, containerBindingName, containerBuildTag, containerClassName, createContainerContext, createContainerTestContext, defineContainer, isContainerDefinition, normalizeContainerImage, resolveContainerEnvVariables as resolveContainerEnvVars };
|
|
589
|
+
type PoolOptions, containerBindingName, containerBuildTag, containerClassName, createContainerContext, createContainerTestContext, defineContainer, isCloudflareRegistryDigest, isContainerDefinition, isManagedImage, normalizeContainerImage, resolveContainerEnvVariables as resolveContainerEnvVars };
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createContainerContext as
|
|
1
|
+
import{createContainerContext as a,createContainerTestContext as i}from"./packem_shared/createContainerContext-DP-kZaQ1.mjs";import{DURABLE_OBJECT_POLICY_FORBIDDEN_KEYS as t,containerBindingName as o,containerBuildTag as C,containerClassName as s,defineContainer as m,isCloudflareRegistryDigest as E,isContainerDefinition as g,isManagedImage as f,normalizeContainerImage as l,resolveContainerEnvVars as _}from"./packem_shared/DURABLE_OBJECT_POLICY_FORBIDDEN_KEYS-Dk6cuK7G.mjs";import{C as d}from"./packem_shared/exec-BYLW70sR.mjs";export{d as CONTAINER_EXEC_PATH,t as DURABLE_OBJECT_POLICY_FORBIDDEN_KEYS,o as containerBindingName,C as containerBuildTag,s as containerClassName,a as createContainerContext,i as createContainerTestContext,m as defineContainer,E as isCloudflareRegistryDigest,g as isContainerDefinition,f as isManagedImage,l as normalizeContainerImage,_ as resolveContainerEnvVars};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import"./abort-deadline-jwbW9Ala.mjs";import{a as e,C as A,D as T,e as s,p as C}from"./exec-BYLW70sR.mjs";export{e as CONTAINER_EXEC_HEADER,A as CONTAINER_EXEC_PATH,T as DEFAULT_EXEC_MAX_OUTPUT_BYTES,s as execViaFetch,C as pathMatchesAnyDecoding};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{WorkerEntrypoint as
|
|
1
|
+
import{WorkerEntrypoint as D,DurableObject as $}from"cloudflare:workers";function W(i=9){const t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",e=new Uint8Array(i);crypto.getRandomValues(e);let n="";for(let s=0;s<i;s++)n+=t[e[s]%t.length];return n}function U(i){if(typeof i=="number")return i;if(typeof i=="string"){const t=i.match(/^(\d+)([smh])$/);if(!t)throw new Error(`invalid time expression ${i}`);const e=parseInt(t[1]),n=t[2];switch(n){case"s":return e;case"m":return e*60;case"h":return e*60*60;default:throw new Error(`unknown time unit ${n}`)}}throw new Error(`invalid type for a time expression: ${typeof i}`)}const B="there is no container instance that can be provided to this durable object",q="you are requesting too many containers per second",T="runtime signalled the container to exit:",S="container exited with unexpected exit code:",j="container is not listening",x="__CF_CONTAINER_STATE",N="OUTBOUND_CONFIGURATION",G=3,O=100,V=180*1e3,P=5e3,K="10m",E=300,I=8e3,M=2e4,k=33;function nt(i,t){return t}const b=new Map,C=new Map,A=new Map,X={SIGINT:2,SIGTERM:15,SIGKILL:9};function v(i,t){return(i instanceof Error?i.message:String(i)).toLowerCase().includes(t)}const H=i=>v(i,B),J=i=>v(i,q),Y=i=>v(i,T),_=i=>v(i,j),z=i=>v(i,S);function Z(i){return i instanceof Error?Y(i)?+i.message.toLowerCase().slice(i.message.toLowerCase().indexOf(T)+T.length+1):z(i)?+i.message.toLowerCase().slice(i.message.toLowerCase().indexOf(S)+S.length+1):null:null}function L(i,t){const e=new AbortController;if(i?.aborted)return e.abort(),e.signal;i?.addEventListener("abort",()=>e.abort());const n=setTimeout(()=>e.abort(),t);return e.signal.addEventListener("abort",()=>clearTimeout(n)),e.signal}function R(i,t){const e=i.split("*");if(e.length===1)return i===t;if(!t.startsWith(e[0])||!t.endsWith(e[e.length-1]))return!1;let n=e[0].length;for(let s=1;s<e.length-1;s++){const o=t.indexOf(e[s],n);if(o===-1)return!1;n=o+e[s].length}return n<=t.length-e[e.length-1].length}function F(i,t){return t.some(e=>R(e,i))}function Q(i){let t=i.length;for(;t>0&&i[t-1]===".";)t--;return i.slice(0,t)}class tt{storage;status;constructor(t){this.storage=t}async setRunning(){await this.setStatusAndupdate("running")}async setHealthy(){await this.setStatusAndupdate("healthy")}async setStopping(){await this.setStatusAndupdate("stopping")}async setStopped(){await this.setStatusAndupdate("stopped")}async setStoppedIfUnchanged(t){this.status===t&&await this.setStopped()}async setStoppedWithCode(t){this.status={status:"stopped_with_code",lastChange:Date.now(),exitCode:t},await this.update()}async getState(){if(!this.status){const t=await this.storage.get(x);t?this.status=t:(this.status={status:"stopped",lastChange:Date.now()},await this.update())}return this.status}async setStatusAndupdate(t){this.status={status:t,lastChange:Date.now()},await this.update()}async update(){if(!this.status)throw new Error("status should be init");await this.storage.put(x,this.status)}}class st extends D{async fetch(t){const e=new URL(t.url),n=Q(e.hostname),{className:s,containerId:o,outboundByHostOverrides:a,outboundHandlerOverride:c,enableInternet:d,allowedHosts:r,deniedHosts:h,interceptAll:f}=this.ctx.props,p={containerId:o,className:s};if(h&&F(n,h))return new Response("Origin is disallowed",{status:520});if(r&&!F(n,r))return new Response("Origin is disallowed",{status:520});const g=b.get(s);if(a&&g){const y=a[n]??Object.entries(a).find(([l])=>l!==n&&R(l,n))?.[1];if(y&&g[y.method])return g[y.method](t,this.env,{...p,params:y.params})}const w=A.get(s);if(w){const y=w[n]??Object.entries(w).find(([l])=>l!==n&&R(l,n))?.[1];if(y)return y(t,this.env,p)}if(!f)return r||d?fetch(t):new Response("Origin is disallowed",{status:520});if(c&&g?.[c.method])return g[c.method](t,this.env,{...p,params:c.params});const u=C.get(s);return u&&g?.[u]?g[u](t,this.env,p):r||d?fetch(t):new Response("Origin is disallowed",{status:520})}}class it extends ${static get outboundByHost(){return A.get(this.name)}static set outboundByHost(t){A.set(this.name,t)}static get outboundHandlers(){return b.get(this.name)}static set outboundHandlers(t){const e=b.get(this.name)??{};b.set(this.name,{...e,...t})}static get outbound(){const t=C.get(this.name);if(t)return b.get(this.name)?.[t]}static set outbound(t){const e="__outbound__",n=b.get(this.name)??{};b.set(this.name,{...n,[e]:t}),C.set(this.name,e)}static get outboundProxies(){return this.outboundHandlers}static set outboundProxies(t){this.outboundHandlers=t}static get outboundProxy(){return this.outbound}static set outboundProxy(t){this.outbound=t}defaultPort;requiredPorts;sleepAfter=K;envVars={};entrypoint;enableInternet=!0;labels={};interceptHttps=!1;allowedHosts;deniedHosts;pingEndpoint="ping";applyOutboundInterceptionPromise=Promise.resolve();usingInterception=!1;constructor(t,e,n){if(super(t,e),t.container===void 0)throw new Error("Containers have not been enabled for this Durable Object class. Have you correctly setup your Wrangler config? More info: https://developers.cloudflare.com/containers/get-started/#configuration");this.state=new tt(this.ctx.storage);const s=this.restoreOutboundConfiguration();this.ctx.blockConcurrencyWhile(async()=>{await this.scheduleNextAlarm(),this.renewActivityTimeout();const o=this.constructor;(s!==void 0||o.outboundByHost!==void 0||o.outbound!==void 0||o.outboundHandlers!==void 0||this.effectiveAllowedHosts!==void 0||this.effectiveDeniedHosts!==void 0)&&(this.usingInterception=!0),this.container.running&&(this.applyOutboundInterceptionPromise=this.applyOutboundInterception())}),this.container=t.container,n&&(n.defaultPort!==void 0&&(this.defaultPort=n.defaultPort),n.sleepAfter!==void 0&&(this.sleepAfter=n.sleepAfter),n.envVars!==void 0&&(this.envVars=n.envVars),n.entrypoint!==void 0&&(this.entrypoint=n.entrypoint),n.enableInternet!==void 0&&(this.enableInternet=n.enableInternet)),this.sql`
|
|
2
2
|
CREATE TABLE IF NOT EXISTS container_schedules (
|
|
3
3
|
id TEXT PRIMARY KEY NOT NULL DEFAULT (randomblob(9)),
|
|
4
4
|
callback TEXT NOT NULL,
|
|
@@ -8,20 +8,20 @@ import{WorkerEntrypoint as B,DurableObject as D}from"cloudflare:workers";functio
|
|
|
8
8
|
delayInSeconds INTEGER,
|
|
9
9
|
created_at INTEGER DEFAULT (unixepoch())
|
|
10
10
|
)
|
|
11
|
-
`,this.container.running&&(this.monitor=this.container.monitor(),this.setupMonitorCallbacks())}async getState(){return{...await this.state.getState()}}async setOutboundHandler(t,...e){this.validateOutboundHandlerMethodName(t),this.outboundHandlerOverride=e.length===0?{method:t}:{method:t,params:e[0]},await this.refreshOutboundInterception()}async setOutboundByHost(t,e,...n){this.validateOutboundHandlerMethodName(e),this.outboundByHostOverrides[t]=n.length===0?{method:e}:{method:e,params:n[0]},await this.refreshOutboundInterception()}async removeOutboundByHost(t){delete this.outboundByHostOverrides[t],await this.refreshOutboundInterception()}async setOutboundByHosts(t){for(const e of Object.values(t)){const n=typeof e=="string"?e:e.method;this.validateOutboundHandlerMethodName(n)}this.outboundByHostOverrides=Object.fromEntries(Object.entries(t).map(([e,n])=>[e,typeof n=="string"?{method:n}:n])),await this.refreshOutboundInterception()}async setAllowedHosts(t){this.allowedHostsOverride=[...t],this.usingInterception=!0,await this.refreshOutboundInterception()}async setDeniedHosts(t){this.deniedHostsOverride=[...t],this.usingInterception=!0,await this.refreshOutboundInterception()}async allowHost(t){const e=this.effectiveAllowedHosts??[];e.includes(t)||(this.allowedHostsOverride=[...e,t]),this.usingInterception=!0,await this.refreshOutboundInterception()}async denyHost(t){const e=this.effectiveDeniedHosts??[];e.includes(t)||(this.deniedHostsOverride=[...e,t]),this.usingInterception=!0,await this.refreshOutboundInterception()}async removeAllowedHost(t){this.allowedHostsOverride=(this.effectiveAllowedHosts??[]).filter(e=>e!==t),await this.refreshOutboundInterception()}async removeDeniedHost(t){this.deniedHostsOverride=(this.effectiveDeniedHosts??[]).filter(e=>e!==t),await this.refreshOutboundInterception()}async start(t,e){const n=e?.portToCheck??this.defaultPort??(this.requiredPorts?this.requiredPorts[0]:
|
|
11
|
+
`,this.container.running&&(this.monitor=this.container.monitor(),this.setupMonitorCallbacks())}async getState(){return{...await this.state.getState()}}async setOutboundHandler(t,...e){this.validateOutboundHandlerMethodName(t),this.outboundHandlerOverride=e.length===0?{method:t}:{method:t,params:e[0]},await this.refreshOutboundInterception()}async setOutboundByHost(t,e,...n){this.validateOutboundHandlerMethodName(e),this.outboundByHostOverrides[t]=n.length===0?{method:e}:{method:e,params:n[0]},await this.refreshOutboundInterception()}async removeOutboundByHost(t){delete this.outboundByHostOverrides[t],await this.refreshOutboundInterception()}async setOutboundByHosts(t){for(const e of Object.values(t)){const n=typeof e=="string"?e:e.method;this.validateOutboundHandlerMethodName(n)}this.outboundByHostOverrides=Object.fromEntries(Object.entries(t).map(([e,n])=>[e,typeof n=="string"?{method:n}:n])),await this.refreshOutboundInterception()}async setAllowedHosts(t){this.allowedHostsOverride=[...t],this.usingInterception=!0,await this.refreshOutboundInterception()}async setDeniedHosts(t){this.deniedHostsOverride=[...t],this.usingInterception=!0,await this.refreshOutboundInterception()}async allowHost(t){const e=this.effectiveAllowedHosts??[];e.includes(t)||(this.allowedHostsOverride=[...e,t]),this.usingInterception=!0,await this.refreshOutboundInterception()}async denyHost(t){const e=this.effectiveDeniedHosts??[];e.includes(t)||(this.deniedHostsOverride=[...e,t]),this.usingInterception=!0,await this.refreshOutboundInterception()}async removeAllowedHost(t){this.allowedHostsOverride=(this.effectiveAllowedHosts??[]).filter(e=>e!==t),await this.refreshOutboundInterception()}async removeDeniedHost(t){this.deniedHostsOverride=(this.effectiveDeniedHosts??[]).filter(e=>e!==t),await this.refreshOutboundInterception()}async start(t,e){const n=e?.portToCheck??this.defaultPort??(this.requiredPorts?this.requiredPorts[0]:k),s=e?.waitInterval??E;await this.startContainerIfNotRunning({signal:e?.signal,waitInterval:s,retries:e?.retries??Math.ceil(I/s),portToCheck:n},t),this.setupMonitorCallbacks(),await this.ctx.blockConcurrencyWhile(async()=>{await this.onStart()})}async startAndWaitForPorts(t,e,n){let s,o,a;typeof t=="object"&&t!==null&&!Array.isArray(t)?(s=t.ports,o=t.cancellationOptions,a=t.startOptions):(s=t,o=e,a=n);const c=await this.getPortsToCheck(s);await this.syncPendingStoppedEvents(),o??={};const d=o.instanceGetTimeoutMS??I,r=o.waitInterval??E,h=Math.ceil(d/r),f={signal:o.abort,retries:h,waitInterval:r,portToCheck:c[0]},p=await this.startContainerIfNotRunning(f,a);let w=Math.ceil((o.portReadyTimeoutMS??M)/r)-p;for(const u of c)w=await this.waitForPort({signal:o.abort,waitInterval:r,retries:w,portToCheck:u});this.setupMonitorCallbacks(),await this.ctx.blockConcurrencyWhile(async()=>{await this.state.setHealthy(),await this.onStart()})}async waitForPort(t){const e=t.portToCheck,n=this.container.getTcpPort(e),s=new Promise(c=>{t.signal?.addEventListener("abort",()=>{c(!0)})}),o=t.waitInterval??E,a=t.retries??Math.ceil(M/o);for(let c=0;c<a;c++)try{const d=L(t.signal,P);await n.fetch(`http://${this.pingEndpoint}`,{signal:d});break}catch(d){const r=d instanceof Error?d.message:String(d);if(!this.container.running){try{await this.onError(new Error("Container crashed while checking for ports, did you start the container and setup the entrypoint correctly?"))}catch{}throw d}if(c===a-1){try{await this.onError(`Failed to verify port ${e} is available after ${(c+1)*o}ms, last error: ${r}`)}catch{}throw d}if(await Promise.any([new Promise(h=>setTimeout(h,o)),s]),t.signal?.aborted)throw new Error("Container request aborted.",{cause:d})}return a}async stop(t="SIGTERM"){this.container.running&&this.container.signal(typeof t=="string"?X[t]:t),await this.syncPendingStoppedEvents()}async destroy(){await this.container.destroy()}onStart(){}onStop(t){}async onActivityExpired(){console.log("Activity expired, signalling container to stop"),this.container.running&&await this.stop()}onError(t){throw console.error("Container error:",t),t}renewActivityTimeout(){const t=U(this.sleepAfter)*1e3;this.sleepAfterMs=Date.now()+t}decrementInflight(){this.inflightRequests=Math.max(0,this.inflightRequests-1),this.inflightRequests===0&&this.renewActivityTimeout()}async schedule(t,e,n){const s=W(9);if(typeof e!="string")throw new Error("Callback must be a string (method name)");if(typeof this[e]!="function")throw new Error(`this.${e} is not a function`);if(t instanceof Date){const o=Math.floor(t.getTime()/1e3);return this.sql`
|
|
12
12
|
INSERT OR REPLACE INTO container_schedules (id, callback, payload, type, time)
|
|
13
13
|
VALUES (${s}, ${e}, ${JSON.stringify(n)}, 'scheduled', ${o})
|
|
14
14
|
`,await this.scheduleNextAlarm(),{taskId:s,callback:e,payload:n,time:o,type:"scheduled"}}if(typeof t=="number"){const o=Math.floor(Date.now()/1e3+t);return this.sql`
|
|
15
15
|
INSERT OR REPLACE INTO container_schedules (id, callback, payload, type, delayInSeconds, time)
|
|
16
16
|
VALUES (${s}, ${e}, ${JSON.stringify(n)}, 'delayed', ${t}, ${o})
|
|
17
|
-
`,await this.scheduleNextAlarm(),{taskId:s,callback:e,payload:n,delayInSeconds:t,time:o,type:"delayed"}}throw new Error("Invalid schedule type. 'when' must be a Date or number of seconds")}async containerFetch(t,e,n){const{request:s,port:o}=this.requestAndPortFromContainerFetchArgs(t,e,n),a=await this.state.getState();if(!this.container.running||a.status!=="healthy")try{await this.startAndWaitForPorts(o,{abort:s.signal})}catch(r){return
|
|
17
|
+
`,await this.scheduleNextAlarm(),{taskId:s,callback:e,payload:n,delayInSeconds:t,time:o,type:"delayed"}}throw new Error("Invalid schedule type. 'when' must be a Date or number of seconds")}async containerFetch(t,e,n){const{request:s,port:o}=this.requestAndPortFromContainerFetchArgs(t,e,n),a=await this.state.getState();if(!this.container.running||a.status!=="healthy")try{await this.startAndWaitForPorts(o,{abort:s.signal})}catch(r){return H(r)?new Response(`There is no Container instance available at this time.
|
|
18
18
|
This is likely because you have reached your max concurrent instance count (set in wrangler config) or are you currently provisioning the Container.
|
|
19
|
-
If you are deploying your Container for the first time, check your dashboard to see provisioning status, this may take a few minutes.`,{status:503}):X(r)?new Response(r instanceof Error?r.message:String(r),{status:429}):new Response(`Failed to start container: ${r instanceof Error?r.message:String(r)}`,{status:500})}const c=this.container.getTcpPort(o),d=s.url.replace("https:","http:");this.inflightRequests++;try{this.renewActivityTimeout();const r=await c.fetch(d,s);if(r.webSocket!==null){const l=r.webSocket,[p,f]=Object.values(new WebSocketPair);let g=!1;const h=()=>{g||(g=!0,this.decrementInflight())};return l.accept(),f.accept(),f.addEventListener("message",async u=>{this.renewActivityTimeout();try{const y=u.data instanceof Blob?await u.data.arrayBuffer():u.data;l.send(y)}catch{f.close(1011,"Failed to forward message to container")}}),l.addEventListener("message",async u=>{this.renewActivityTimeout();try{const y=u.data instanceof Blob?await u.data.arrayBuffer():u.data;f.send(y)}catch{l.close(1011,"Failed to forward message to client")}}),f.addEventListener("close",u=>{h();const y=u.code===1005||u.code===1006?1e3:u.code;l.close(y,u.reason)}),l.addEventListener("close",u=>{h();const y=u.code===1005||u.code===1006?1e3:u.code;f.close(y,u.reason)}),f.addEventListener("error",()=>{h(),l.close(1011,"Client WebSocket error")}),l.addEventListener("error",()=>{h(),f.close(1011,"Container WebSocket error")}),new Response(null,{status:r.status,webSocket:p,headers:r.headers})}if(r.body!==null){const{readable:l,writable:p}=new IdentityTransformStream;return r.body?.pipeTo(p).finally(()=>{this.decrementInflight()}),new Response(l,r)}return this.decrementInflight(),r}catch(r){if(this.decrementInflight(),!(r instanceof Error))throw r;return r.message.includes("Network connection lost.")?new Response("Container suddenly disconnected, try again",{status:500}):(console.error(`Error proxying request to container ${this.ctx.id}:`,r),new Response(`Error proxying request to container: ${r instanceof Error?r.message:String(r)}`,{status:500}))}}async fetch(t){if(this.defaultPort===void 0&&!t.headers.has("cf-container-target-port"))throw new Error("No port configured for this container. Set the `defaultPort` in your Container subclass, or specify a port with `container.fetch(switchPort(request, port))`.");let e=this.defaultPort;if(t.headers.has("cf-container-target-port")){const n=parseInt(t.headers.get("cf-container-target-port")??"");if(isNaN(n))throw new Error("port value from switchPort is not a number");e=n}return await this.containerFetch(t,e)}container;onStopCalled=!1;state;monitor;startInFlight;monitoredPromise;sleepAfterMs=0;inflightRequests=0;outboundByHostOverrides={};outboundHandlerOverride;allowedHostsOverride;deniedHostsOverride;hasInterceptAllRegistration=!1;validateOutboundHandlerMethodName(t){const e=w.get(this.constructor.name);if(!e||!(t in e))throw new Error(`Outbound handler method '${t}' not found in outboundHandlers for ${this.constructor.name}`)}get effectiveAllowedHosts(){return this.allowedHostsOverride??this.allowedHosts}get effectiveDeniedHosts(){return this.deniedHostsOverride??this.deniedHosts}getOutboundConfiguration(){return{outboundByHostOverrides:Object.keys(this.outboundByHostOverrides).length>0?this.outboundByHostOverrides:void 0,outboundHandlerOverride:this.outboundHandlerOverride,allowedHosts:this.effectiveAllowedHosts,deniedHosts:this.effectiveDeniedHosts,hasInterceptAllRegistration:this.hasInterceptAllRegistration||void 0}}persistOutboundConfiguration(t){this.ctx.storage.kv.put(x,{...t,allowedHosts:this.allowedHostsOverride,deniedHosts:this.deniedHostsOverride})}restoreOutboundConfiguration(){const t=this.ctx.storage.kv.get(x);if(t){if(this.outboundHandlerOverride=void 0,t.outboundHandlerOverride!==void 0)try{this.validateOutboundHandlerMethodName(t.outboundHandlerOverride.method),this.outboundHandlerOverride=t.outboundHandlerOverride}catch(e){console.warn("Ignoring invalid persisted outbound handler override:",e)}this.outboundByHostOverrides={};for(const[e,n]of Object.entries(t.outboundByHostOverrides??{}))try{this.validateOutboundHandlerMethodName(n.method),this.outboundByHostOverrides[e]=n}catch(s){console.warn(`Ignoring invalid persisted outbound override for ${e}:`,s)}return this.hasInterceptAllRegistration=t.hasInterceptAllRegistration===!0,t.allowedHosts&&(this.allowedHostsOverride=t.allowedHosts),t.deniedHosts&&(this.deniedHostsOverride=t.deniedHosts),this.getOutboundConfiguration()}}needsCatchAllInterception(){return this.constructor.outbound!==void 0||this.outboundHandlerOverride!==void 0}hasMutableOutboundConfiguration(){return Object.keys(this.outboundByHostOverrides).length>0||this.allowedHostsOverride!==void 0||this.deniedHostsOverride!==void 0}shouldInterceptAllOutbound(){return this.hasInterceptAllRegistration||this.needsCatchAllInterception()||this.effectiveAllowedHosts!==void 0||this.effectiveDeniedHosts!==void 0||this.hasMutableOutboundConfiguration()}getStaticOutboundByHostKeys(){const t=this.constructor;return t.outboundByHost?Object.keys(t.outboundByHost):[]}getHostsToIntercept(){const t=new Set,e=this.constructor;if(e.outboundByHost)for(const n of Object.keys(e.outboundByHost))t.add(n);for(const n of Object.keys(this.outboundByHostOverrides))t.add(n);return[...t]}async refreshOutboundInterception(){this.usingInterception&&(this.applyOutboundInterceptionPromise=this.applyOutboundInterception(),await this.applyOutboundInterceptionPromise)}async applyOutboundInterception(){const t=this.ctx;if(t.exports===void 0)throw new Error("ctx.exports is undefined, please try to update your compatibility date or export ContainerProxy from the containers package in your worker entrypoint");if(t.exports.ContainerProxy===void 0)throw new Error("ctx.exports.ContainerProxy is undefined, export ContainerProxy from the containers package in your worker entrypoint");const e=this.shouldInterceptAllOutbound();e&&(this.hasInterceptAllRegistration=e);const n=this.getOutboundConfiguration();this.persistOutboundConfiguration(n);const s=this.getHostsToIntercept(),o={enableInternet:this.enableInternet,containerId:this.ctx.id.toString(),className:this.constructor.name,outboundByHostOverrides:n.outboundByHostOverrides,outboundHandlerOverride:n.outboundHandlerOverride,allowedHosts:n.allowedHosts,deniedHosts:n.deniedHosts,interceptAll:e},a=t.exports.ContainerProxy({props:o});if(e){for(const c of this.getStaticOutboundByHostKeys())await this.container.interceptOutboundHttp(c,a),this.interceptHttps&&await this.container.interceptOutboundHttps(c,a);this.interceptHttps&&await this.container.interceptOutboundHttps("*",a),await this.container.interceptAllOutboundHttp(a)}else for(const c of s)await this.container.interceptOutboundHttp(c,a),this.interceptHttps&&await this.container.interceptOutboundHttps(c,a)}sql(t,...e){const n=t.reduce((s,o,a)=>s+o+(a<e.length?"?":""),"");return[...this.ctx.storage.sql.exec(n,...e)]}requestAndPortFromContainerFetchArgs(t,e,n){let s,o;if(t instanceof Request)s=t,o=typeof e=="number"?e:void 0;else{const a=typeof t=="string"?t:t.toString(),c=typeof e=="number"?{}:e||{};o=typeof e=="number"?e:typeof n=="number"?n:void 0,s=new Request(a,c)}if(o??=this.defaultPort,o===void 0)throw new Error("No port specified for container fetch. Set defaultPort or specify a port parameter.");return{request:s,port:o}}async getPortsToCheck(t){return t!==void 0?Array.isArray(t)?t:[t]:this.requiredPorts&&this.requiredPorts.length>0?[...this.requiredPorts]:[this.defaultPort??M]}async startContainerIfNotRunning(t,e){if(this.startInFlight)return this.startInFlight;if(this.container.running)return this.monitor||(this.monitor=this.container.monitor()),0;const n=this.doStartContainer(t,e);this.startInFlight=n;try{return await n}finally{this.startInFlight===n&&(this.startInFlight=void 0)}}async doStartContainer(t,e){const n=new Promise(a=>{t.signal?.addEventListener("abort",()=>{a(!0)})}),s=t.waitInterval??v,o=t.retries??Math.ceil(O/s);for(let a=0;a<o;a++){const c=e?.envVars??this.envVars,d=e?.entrypoint??this.entrypoint,r=e?.enableInternet??this.enableInternet,l=e?.labels??this.labels,p={enableInternet:r};c&&Object.keys(c).length>0&&(p.env=c),d&&(p.entrypoint=d),l&&Object.keys(l).length>0&&(p.labels=l),this.renewActivityTimeout();const f=async()=>{const h=await this.monitor?.catch(u=>u);if(typeof h=="number"){const u=new Error(`Container exited before we could determine the container health, exit code: ${h}`);await this.state.setStoppedWithCode(h),this.monitor=void 0;try{await this.onError(u)}catch{}throw u}else if(!I(h)){await this.state.setStopped(),this.monitor=void 0;try{await this.onError(h)}catch{}throw h}};a>0&&!this.container.running&&await f(),await this.scheduleNextAlarm(),this.container.running?await this.scheduleNextAlarm():(await this.refreshOutboundInterception(),this.container.start(p),this.monitor=this.container.monitor(),await this.state.setRunning()),this.renewActivityTimeout();const g=this.container.getTcpPort(t.portToCheck);try{const h=_(t.signal,N);return await g.fetch("http://containerstarthealthcheck",{signal:h}),a}catch(h){if(k(h)&&this.container.running)return a;if(!this.container.running&&k(h)&&await f(),await Promise.any([new Promise(u=>setTimeout(u,t.waitInterval)),n]),t.signal?.aborted)throw new Error("Aborted waiting for container to start as we received a cancellation signal",{cause:h});if(o===a+1)throw h instanceof Error&&h.message.includes("Network connection lost")&&this.ctx.abort(),await f(),await this.state.setStopped(),this.monitor=void 0,new Error(F,{cause:h});continue}}throw new Error(`Container did not start after ${o*s}ms`)}setupMonitorCallbacks(){const t=this.monitor;!t||this.monitoredPromise===t||(this.monitoredPromise=t,t.then(async()=>{await this.ctx.blockConcurrencyWhile(async()=>{this.monitor===t&&await this.state.setStoppedWithCode(0)})}).catch(async e=>{if(this.monitor!==t)return;if(I(e)){await this.ctx.blockConcurrencyWhile(async()=>{this.monitor===t&&await this.state.setStopped()});return}const n=z(e);if(n!==null){await this.ctx.blockConcurrencyWhile(async()=>{this.monitor===t&&await this.state.setStoppedWithCode(n)});return}if(await this.ctx.blockConcurrencyWhile(async()=>{this.monitor===t&&await this.state.setStopped()}),this.monitor===t)try{await this.onError(e)}catch{}}).finally(()=>{this.monitor===t&&(this.monitoredPromise=void 0,this.monitor=void 0)}))}deleteSchedules(t){this.sql`DELETE FROM container_schedules WHERE callback = ${t}`}async alarm(t){if(t!==void 0&&t.isRetry&&t.retryCount>j){((Number(this.sql`SELECT COUNT(*) as count FROM container_schedules`[0]?.count)||0)>0||this.container.running)&&await this.scheduleNextAlarm();return}const e=this.sql`
|
|
19
|
+
If you are deploying your Container for the first time, check your dashboard to see provisioning status, this may take a few minutes.`,{status:503}):J(r)?new Response(r instanceof Error?r.message:String(r),{status:429}):new Response(`Failed to start container: ${r instanceof Error?r.message:String(r)}`,{status:500})}const c=this.container.getTcpPort(o),d=s.url.replace("https:","http:");this.inflightRequests++;try{this.renewActivityTimeout();const r=await c.fetch(d,s);if(r.webSocket!==null){const h=r.webSocket,[f,p]=Object.values(new WebSocketPair);let g=!1;const w=()=>{g||(g=!0,this.decrementInflight())};return h.accept(),p.accept(),p.addEventListener("message",async u=>{this.renewActivityTimeout();try{const y=u.data instanceof Blob?await u.data.arrayBuffer():u.data;h.send(y)}catch{p.close(1011,"Failed to forward message to container")}}),h.addEventListener("message",async u=>{this.renewActivityTimeout();try{const y=u.data instanceof Blob?await u.data.arrayBuffer():u.data;p.send(y)}catch{h.close(1011,"Failed to forward message to client")}}),p.addEventListener("close",u=>{w();const y=u.code===1005||u.code===1006?1e3:u.code;h.close(y,u.reason)}),h.addEventListener("close",u=>{w();const y=u.code===1005||u.code===1006?1e3:u.code;p.close(y,u.reason)}),p.addEventListener("error",()=>{w(),h.close(1011,"Client WebSocket error")}),h.addEventListener("error",()=>{w(),p.close(1011,"Container WebSocket error")}),new Response(null,{status:r.status,webSocket:f,headers:r.headers})}if(r.body!==null){const{readable:h,writable:f}=new IdentityTransformStream;return r.body?.pipeTo(f).finally(()=>{this.decrementInflight()}),new Response(h,r)}return this.decrementInflight(),r}catch(r){if(this.decrementInflight(),!(r instanceof Error))throw r;return r.message.includes("Network connection lost.")?new Response("Container suddenly disconnected, try again",{status:500}):(console.error(`Error proxying request to container ${this.ctx.id}:`,r),new Response(`Error proxying request to container: ${r instanceof Error?r.message:String(r)}`,{status:500}))}}async fetch(t){if(this.defaultPort===void 0&&!t.headers.has("cf-container-target-port"))throw new Error("No port configured for this container. Set the `defaultPort` in your Container subclass, or specify a port with `container.fetch(switchPort(request, port))`.");let e=this.defaultPort;if(t.headers.has("cf-container-target-port")){const n=parseInt(t.headers.get("cf-container-target-port")??"");if(isNaN(n))throw new Error("port value from switchPort is not a number");e=n}return await this.containerFetch(t,e)}container;onStopCalled=!1;state;monitor;startInFlight;monitoredPromise;sleepAfterMs=0;inflightRequests=0;outboundByHostOverrides={};outboundHandlerOverride;allowedHostsOverride;deniedHostsOverride;hasInterceptAllRegistration=!1;validateOutboundHandlerMethodName(t){const e=b.get(this.constructor.name);if(!e||!(t in e))throw new Error(`Outbound handler method '${t}' not found in outboundHandlers for ${this.constructor.name}`)}get effectiveAllowedHosts(){return this.allowedHostsOverride??this.allowedHosts}get effectiveDeniedHosts(){return this.deniedHostsOverride??this.deniedHosts}getOutboundConfiguration(){return{outboundByHostOverrides:Object.keys(this.outboundByHostOverrides).length>0?this.outboundByHostOverrides:void 0,outboundHandlerOverride:this.outboundHandlerOverride,allowedHosts:this.effectiveAllowedHosts,deniedHosts:this.effectiveDeniedHosts,hasInterceptAllRegistration:this.hasInterceptAllRegistration||void 0}}persistOutboundConfiguration(t){this.ctx.storage.kv.put(N,{...t,allowedHosts:this.allowedHostsOverride,deniedHosts:this.deniedHostsOverride})}restoreOutboundConfiguration(){const t=this.ctx.storage.kv.get(N);if(t){if(this.outboundHandlerOverride=void 0,t.outboundHandlerOverride!==void 0)try{this.validateOutboundHandlerMethodName(t.outboundHandlerOverride.method),this.outboundHandlerOverride=t.outboundHandlerOverride}catch(e){console.warn("Ignoring invalid persisted outbound handler override:",e)}this.outboundByHostOverrides={};for(const[e,n]of Object.entries(t.outboundByHostOverrides??{}))try{this.validateOutboundHandlerMethodName(n.method),this.outboundByHostOverrides[e]=n}catch(s){console.warn(`Ignoring invalid persisted outbound override for ${e}:`,s)}return this.hasInterceptAllRegistration=t.hasInterceptAllRegistration===!0,t.allowedHosts&&(this.allowedHostsOverride=t.allowedHosts),t.deniedHosts&&(this.deniedHostsOverride=t.deniedHosts),this.getOutboundConfiguration()}}needsCatchAllInterception(){return this.constructor.outbound!==void 0||this.outboundHandlerOverride!==void 0}hasMutableOutboundConfiguration(){return Object.keys(this.outboundByHostOverrides).length>0||this.allowedHostsOverride!==void 0||this.deniedHostsOverride!==void 0}shouldInterceptAllOutbound(){return this.hasInterceptAllRegistration||this.needsCatchAllInterception()||this.effectiveAllowedHosts!==void 0||this.effectiveDeniedHosts!==void 0||this.hasMutableOutboundConfiguration()}getStaticOutboundByHostKeys(){const t=this.constructor;return t.outboundByHost?Object.keys(t.outboundByHost):[]}getHostsToIntercept(){const t=new Set,e=this.constructor;if(e.outboundByHost)for(const n of Object.keys(e.outboundByHost))t.add(n);for(const n of Object.keys(this.outboundByHostOverrides))t.add(n);return[...t]}async refreshOutboundInterception(){this.usingInterception&&(this.applyOutboundInterceptionPromise=this.applyOutboundInterception(),await this.applyOutboundInterceptionPromise)}async applyOutboundInterception(){const t=this.ctx;if(t.exports===void 0)throw new Error("ctx.exports is undefined, please try to update your compatibility date or export ContainerProxy from the containers package in your worker entrypoint");if(t.exports.ContainerProxy===void 0)throw new Error("ctx.exports.ContainerProxy is undefined, export ContainerProxy from the containers package in your worker entrypoint");const e=this.shouldInterceptAllOutbound();e&&(this.hasInterceptAllRegistration=e);const n=this.getOutboundConfiguration();this.persistOutboundConfiguration(n);const s=this.getHostsToIntercept(),o={enableInternet:this.enableInternet,containerId:this.ctx.id.toString(),className:this.constructor.name,outboundByHostOverrides:n.outboundByHostOverrides,outboundHandlerOverride:n.outboundHandlerOverride,allowedHosts:n.allowedHosts,deniedHosts:n.deniedHosts,interceptAll:e},a=t.exports.ContainerProxy({props:o});if(e){for(const c of this.getStaticOutboundByHostKeys())await this.container.interceptOutboundHttp(c,a),this.interceptHttps&&await this.container.interceptOutboundHttps(c,a);this.interceptHttps&&await this.container.interceptOutboundHttps("*",a),await this.container.interceptAllOutboundHttp(a)}else for(const c of s)await this.container.interceptOutboundHttp(c,a),this.interceptHttps&&await this.container.interceptOutboundHttps(c,a)}sql(t,...e){const n=t.reduce((s,o,a)=>s+o+(a<e.length?"?":""),"");return[...this.ctx.storage.sql.exec(n,...e)]}requestAndPortFromContainerFetchArgs(t,e,n){let s,o;if(t instanceof Request)s=t,o=typeof e=="number"?e:void 0;else{const a=typeof t=="string"?t:t.toString(),c=typeof e=="number"?{}:e||{};o=typeof e=="number"?e:typeof n=="number"?n:void 0,s=new Request(a,c)}if(o??=this.defaultPort,o===void 0)throw new Error("No port specified for container fetch. Set defaultPort or specify a port parameter.");return{request:s,port:o}}async getPortsToCheck(t){return t!==void 0?Array.isArray(t)?t:[t]:this.requiredPorts&&this.requiredPorts.length>0?[...this.requiredPorts]:[this.defaultPort??k]}async startContainerIfNotRunning(t,e){if(this.startInFlight)return this.startInFlight;if(this.container.running)return this.monitor||(this.monitor=this.container.monitor()),0;const n=this.doStartContainer(t,e);this.startInFlight=n;try{return await n}finally{this.startInFlight===n&&(this.startInFlight=void 0)}}async doStartContainer(t,e){const n=new Promise(a=>{t.signal?.addEventListener("abort",()=>{a(!0)})}),s=t.waitInterval??E,o=t.retries??Math.ceil(I/s);for(let a=0;a<o;a++){const c=e?.envVars??this.envVars,d=e?.entrypoint??this.entrypoint,r=e?.enableInternet??this.enableInternet,h=e?.labels??this.labels,f={enableInternet:r};c&&Object.keys(c).length>0&&(f.env=c),d&&(f.entrypoint=d),h&&Object.keys(h).length>0&&(f.labels=h);const p=e?.containerSnapshot,g=e?.image??this.image,w=e?.instance??this.instance;p?f.containerSnapshot=p:g&&(f.image=g),w&&(f.instance=w),this.renewActivityTimeout();const u=async()=>{const l=await this.monitor?.catch(m=>m);if(typeof l=="number"){const m=new Error(`Container exited before we could determine the container health, exit code: ${l}`);await this.state.setStoppedWithCode(l),this.monitor=void 0;try{await this.onError(m)}catch{}throw m}else if(!H(l)){await this.state.setStopped(),this.monitor=void 0;try{await this.onError(l)}catch{}throw l}};a>0&&!this.container.running&&await u(),await this.scheduleNextAlarm(),this.container.running?await this.scheduleNextAlarm():(await this.refreshOutboundInterception(),this.container.start(f),this.monitor=this.container.monitor(),await this.state.setRunning()),this.renewActivityTimeout();const y=this.container.getTcpPort(t.portToCheck);try{const l=L(t.signal,P);return await y.fetch("http://containerstarthealthcheck",{signal:l}),a}catch(l){if(_(l)&&this.container.running)return a;if(!this.container.running&&_(l)&&await u(),await Promise.any([new Promise(m=>setTimeout(m,t.waitInterval)),n]),t.signal?.aborted)throw new Error("Aborted waiting for container to start as we received a cancellation signal",{cause:l});if(o===a+1)throw l instanceof Error&&l.message.includes("Network connection lost")&&this.ctx.abort(),await u(),await this.state.setStopped(),this.monitor=void 0,new Error(B,{cause:l});continue}}throw new Error(`Container did not start after ${o*s}ms`)}setupMonitorCallbacks(){const t=this.monitor;!t||this.monitoredPromise===t||(this.monitoredPromise=t,t.then(async()=>{await this.ctx.blockConcurrencyWhile(async()=>{this.monitor===t&&await this.state.setStoppedWithCode(0)})}).catch(async e=>{if(this.monitor!==t)return;if(H(e)){await this.ctx.blockConcurrencyWhile(async()=>{this.monitor===t&&await this.state.setStopped()});return}const n=Z(e);if(n!==null){await this.ctx.blockConcurrencyWhile(async()=>{this.monitor===t&&await this.state.setStoppedWithCode(n)});return}if(await this.ctx.blockConcurrencyWhile(async()=>{this.monitor===t&&await this.state.setStopped()}),this.monitor===t)try{await this.onError(e)}catch{}}).finally(()=>{this.monitor===t&&(this.monitoredPromise=void 0,this.monitor=void 0)}))}deleteSchedules(t){this.sql`DELETE FROM container_schedules WHERE callback = ${t}`}async alarm(t){if(t!==void 0&&t.isRetry&&t.retryCount>G){((Number(this.sql`SELECT COUNT(*) as count FROM container_schedules`[0]?.count)||0)>0||this.container.running)&&await this.scheduleNextAlarm();return}const e=this.sql`
|
|
20
20
|
SELECT * FROM container_schedules;
|
|
21
|
-
`;let n=Date.now()+
|
|
21
|
+
`;let n=Date.now()+V;const s=Date.now()/1e3;for(const d of e){if(d.time>s)continue;const r=this[d.callback];if(!r||typeof r!="function"){console.error(`Callback ${d.callback} not found or is not a function`);continue}const h=this.getSchedule(d.id);try{const f=d.payload?JSON.parse(d.payload):void 0;await r.call(this,f,await h)}catch(f){console.error(`Error executing scheduled callback "${d.callback}":`,f)}this.sql`DELETE FROM container_schedules WHERE id = ${d.id}`}const o=this.sql`
|
|
22
22
|
SELECT * FROM container_schedules;
|
|
23
|
-
`,a=Math.min(...o.map(d=>d.time*1e3));if(!this.container.running){await this.syncPendingStoppedEvents(),o.length==0?await this.ctx.storage.deleteAlarm():await this.ctx.storage.setAlarm(a);return}if(this.isActivityExpired()){await this.onActivityExpired(),this.renewActivityTimeout(),await this.ctx.storage.setAlarm(Date.now()+
|
|
23
|
+
`,a=Math.min(...o.map(d=>d.time*1e3));if(!this.container.running){await this.syncPendingStoppedEvents(),o.length==0?await this.ctx.storage.deleteAlarm():await this.ctx.storage.setAlarm(a);return}if(this.isActivityExpired()){await this.onActivityExpired(),this.renewActivityTimeout(),await this.ctx.storage.setAlarm(Date.now()+O);return}n=Math.min(a,n,this.sleepAfterMs);const c=Math.max(n,Date.now()+O);await this.ctx.storage.setAlarm(c)}async syncPendingStoppedEvents(){const t=await this.state.getState();if(!this.container.running&&(t.status==="healthy"||t.status==="running")){await this.callOnStop({exitCode:0,reason:"exit"},t);return}if(!this.container.running&&t.status==="stopped_with_code"){await this.callOnStop({exitCode:t.exitCode??0,reason:"exit"},t);return}}async callOnStop(t,e){if(this.onStopCalled)return;this.onStopCalled=!0;const n=this.onStop(t);n instanceof Promise?await n.finally(()=>{this.onStopCalled=!1}):this.onStopCalled=!1,await this.state.setStoppedIfUnchanged(e)}async scheduleNextAlarm(t=1e3){const e=Date.now()+Math.max(t,O),n=await this.ctx.storage.getAlarm();n!==null&&n<=e||(await this.ctx.storage.setAlarm(e),await this.ctx.storage.sync())}async listSchedules(t){const e=this.sql`
|
|
24
24
|
SELECT * FROM container_schedules WHERE callback = ${t} LIMIT 1
|
|
25
25
|
`;return!e||e.length===0?[]:e.map(this.toSchedule)}toSchedule(t){let e;try{e=JSON.parse(t.payload)}catch(n){console.error(`Error parsing payload for schedule ${t.id}:`,n),e=void 0}return t.type==="delayed"?{taskId:t.id,callback:t.callback,payload:e,type:"delayed",time:t.time,delayInSeconds:t.delayInSeconds}:{taskId:t.id,callback:t.callback,payload:e,type:"scheduled",time:t.time}}async getSchedule(t){const e=this.sql`
|
|
26
26
|
SELECT * FROM container_schedules WHERE id = ${t} LIMIT 1
|
|
27
|
-
`;if(!e||e.length===0)return;const n=e[0];return this.toSchedule(n)}isActivityExpired(){return this.inflightRequests>0?(this.renewActivityTimeout(),!1):this.sleepAfterMs<=Date.now()}}export{
|
|
27
|
+
`;if(!e||e.length===0)return;const n=e[0];return this.toSchedule(n)}isActivityExpired(){return this.inflightRequests>0?(this.renewActivityTimeout(),!1):this.sleepAfterMs<=Date.now()}}export{it as Container,st as ContainerProxy,nt as outboundParams};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as b}from"@lunora/errors";const d=new Set(["basic","lite","standard-1","standard-2","standard-3","standard-4"]),l=new Set(["lite","standard-1","standard-2","standard-3","standard-4"]),f="cloudflare/",w=e=>e.startsWith(f),g=["maxInstances","rollout"],c=100,u=128,s=/^[A-Z_]\w*$/i,y=/^\d+[smh]$/,E={h:3600,m:60,s:1},M=e=>{if(typeof e=="number")return Math.floor(e);if(y.exec(e)===null)throw new TypeError(`Invalid duration "${e}" — expected a number of seconds or "<n>[smh]"`);return Number(e.slice(0,-1))*(E[e.slice(-1)]??1)},T=e=>{const t=e.endsWith("/")?e.slice(0,-1):e,n=t.lastIndexOf("/");return n===-1?t:t.slice(n+1)},v=e=>{const t=e.lastIndexOf("/");return t===-1?".":e.slice(0,t)||"/"},D=e=>{if(typeof e!="string")return"build"in e?{buildDir:e.build.endsWith("/")?e.build.slice(0,-1):e.build,kind:"build"}:{kind:"registry",reference:e.registry};if(T(e).startsWith("Dockerfile"))return{buildContext:v(e),dockerfilePath:e,kind:"dockerfile"};const t=e.endsWith("/")?e.slice(0,-1):e;return{buildContext:t,dockerfilePath:`${t}/Dockerfile`,kind:"dockerfile"}},j=e=>`${e.charAt(0).toUpperCase()}${e.slice(1)}Container`,x=e=>`CONTAINER_${e.replaceAll(/(?<=[a-z0-9])(?=[A-Z])/g,"_").toUpperCase()}`,R=e=>`lunora-${e.replaceAll(/(?<=[a-z0-9])(?=[A-Z])/g,"-").toLowerCase()}:build`,m=e=>{if(typeof e=="string"){if(e.length===0)throw new TypeError("defineContainer: `image` must be a non-empty path or a { registry } reference");if(e.includes(":"))throw new TypeError(`defineContainer: \`image\` string "${e}" looks like a registry reference — pass it as { registry: "${e}" } instead. Plain strings are local Dockerfile paths.`);return}if("build"in e){if(typeof e.build!="string"||e.build.length===0)throw new TypeError("defineContainer: `image.build` must be a non-empty source directory for Railpack to build");return}if(typeof e.registry!="string"||e.registry.length===0)throw new TypeError("defineContainer: `image.registry` must be a non-empty fully-qualified image reference")},C=(e,t,n)=>{for(const[r,o]of Object.entries(e.secretsStore??{})){if(!s.test(r))throw new TypeError(`defineContainer: secretsStore env name "${r}" is not a valid environment variable name`);if(typeof o!="string"||o.trim().length===0)throw new TypeError(`defineContainer: \`secretsStore["${r}"]\` must be a non-empty Secrets Store binding name`);if(t.has(r)||n.has(r))throw new TypeError(`defineContainer: "${r}" is declared in both \`secretsStore\` and \`env\`/\`secrets\` — pick one source for the value`)}},$=e=>{for(const r of Object.keys(e.env??{}))if(!s.test(r))throw new TypeError(`defineContainer: env variable name "${r}" is not a valid environment variable name`);for(const r of Object.keys(e.buildArgs??{}))if(!s.test(r))throw new TypeError(`defineContainer: buildArg name "${r}" is not a valid environment variable name`);const t=new Set(Object.keys(e.env??{})),n=new Set(e.secrets);for(const r of e.secrets??[]){if(!s.test(r))throw new TypeError(`defineContainer: secret name "${r}" is not a valid environment variable name`);if(t.has(r))throw new TypeError(`defineContainer: "${r}" is declared in both \`env\` and \`secrets\` — a secret would silently overwrite the static env value; pick one`)}C(e,t,n)},i=(e,t)=>{if(!Number.isInteger(e)||e<1||e>65535)throw new TypeError(`defineContainer: \`${t}\` must be an integer in 1–65535 (got ${String(e)})`)},S=e=>{for(const t of e.readyOn??[]){if(typeof t.path!="string"||t.path.trim().length===0)throw new TypeError("defineContainer: `readyOn[].path` must be a non-empty HTTP path string");if(t.path!==t.path.trim())throw new TypeError("defineContainer: `readyOn[].path` must not have leading or trailing whitespace");if(t.port!==void 0&&i(t.port,"readyOn[].port"),t.status!==void 0&&(!Number.isInteger(t.status)||t.status<100||t.status>599))throw new TypeError(`defineContainer: \`readyOn[].status\` must be an HTTP status code in 100–599 (got ${String(t.status)})`)}},p=(e,t)=>{if(e!==void 0){if(typeof e=="string"){if(!y.test(e))throw new TypeError(`defineContainer: \`${t}\` string "${e}" must be a number of seconds followed by a unit, e.g. "30s", "5m", or "1h"`)}else if(!Number.isInteger(e)||e<1)throw new TypeError(`defineContainer: \`${t}\` must be a positive integer number of seconds or a duration string like "5m" (got ${String(e)})`)}},I=e=>{for(const t of["allowedHosts","deniedHosts"])if(e[t]?.some(r=>typeof r!="string"||r.trim().length===0))throw new TypeError(`defineContainer: \`${t}\` must be an array of non-empty hostname patterns`);if(e.interceptHttps!==void 0&&typeof e.interceptHttps!="boolean")throw new TypeError("defineContainer: `interceptHttps` must be a boolean, or omitted")},N=e=>{if(e.requiredPorts!==void 0){if(e.requiredPorts.length===0)throw new TypeError("defineContainer: `requiredPorts` must be a non-empty array of ports, or omitted");for(const t of e.requiredPorts)i(t,"requiredPorts[]")}if(e.entrypoint!==void 0&&(e.entrypoint.length===0||e.entrypoint.some(t=>typeof t!="string"||t.trim().length===0)))throw new TypeError("defineContainer: `entrypoint` must be a non-empty array of non-empty strings, or omitted");if(I(e),e.pingEndpoint!==void 0&&(typeof e.pingEndpoint!="string"||e.pingEndpoint.trim().length===0))throw new TypeError("defineContainer: `pingEndpoint` must be a non-empty path string");for(const[t,n]of Object.entries(e.labels??{}))if(t.trim().length===0||typeof n!="string")throw new TypeError("defineContainer: `labels` must be a record of non-empty keys to string values");S(e)},P=e=>typeof e=="string"&&e.startsWith("registry.cloudflare.com/")&&e.includes("@sha256:"),A=(e,t)=>{if(e.length===0||e.length>u)throw new TypeError(`defineContainer: image name "${e}" must be 1–${String(u)} characters`);if(typeof t=="string"){m(t);return}if(!P(t.registry))throw new TypeError(`defineContainer: \`images["${e}"].registry\` must be a digest-pinned (@sha256) reference under registry.cloudflare.com — push other registries' images there first`)},k=e=>{for(const r of g)if(e[r]!==void 0)throw new TypeError(`defineContainer: \`${r}\` is not supported with schedulingPolicy "durable_object"`);const t=Object.entries(e.images??{});if(t.length>c)throw new TypeError(`defineContainer: \`images\` holds at most ${String(c)} entries (got ${String(t.length)})`);for(const[r,o]of t)A(r,o);if(e.image!==void 0&&!Object.hasOwn(e.images??{},e.image)&&!w(e.image))throw new TypeError(`defineContainer: \`image\` "${e.image}" must name an entry of \`images\` or a Cloudflare-managed "${f}…" image`);const{instanceType:n}=e;if(typeof n=="string"&&!l.has(n))throw new TypeError(`defineContainer: \`instanceType\` "${n}" is not a runtime size — use one of ${[...l].join(", ")}, or a full { vcpu, memoryMib, diskMb } object`);if(typeof n=="object"&&[n.vcpu,n.memoryMib,n.diskMb].some(r=>typeof r!="number"||r<=0))throw new TypeError('defineContainer: a custom `instanceType` under schedulingPolicy "durable_object" needs positive vcpu, memoryMib and diskMb')},_=e=>{if(e.images!==void 0)throw new TypeError('defineContainer: `images` needs schedulingPolicy "durable_object"');m(e.image);const t=e.rollout?.stepPercentage;if(t!==void 0&&(!Number.isInteger(t)||t<1||t>100))throw new TypeError(`defineContainer: \`rollout.stepPercentage\` must be an integer in 1–100 (got ${String(t)})`);const n=e.rollout?.gracePeriodSeconds;if(n!==void 0&&(!Number.isInteger(n)||n<0))throw new TypeError(`defineContainer: \`rollout.gracePeriodSeconds\` must be a non-negative integer (got ${String(n)})`);if(e.maxInstances!==void 0&&(!Number.isInteger(e.maxInstances)||e.maxInstances<1))throw new TypeError(`defineContainer: \`maxInstances\` must be a positive integer (got ${String(e.maxInstances)})`);if(typeof e.instanceType=="string"&&!d.has(e.instanceType))throw new TypeError(`defineContainer: unknown \`instanceType\` "${e.instanceType}" — use one of ${[...d].join(", ")}, or a custom { vcpu, memoryMib, diskMb } object`)},V=e=>{const t=e.schedulingPolicy;if(e.schedulingPolicy==="durable_object")k(e);else if(t===void 0||t==="default")_(e);else throw new TypeError(`defineContainer: unknown \`schedulingPolicy\` ${JSON.stringify(t)} — use "default" or "durable_object"`);return e.defaultPort!==void 0&&i(e.defaultPort,"defaultPort"),p(e.sleepAfter,"sleepAfter"),p(e.hardTimeout,"hardTimeout"),$(e),N(e),{...e,isLunoraContainer:!0}},L=e=>typeof e=="object"&&e!==null&&e.isLunoraContainer===!0,H=(e,t,n)=>{const r={...e.env};for(const o of e.secrets??[]){const a=t[o];if(typeof a!="string"){const h=n===void 0?"container":`container "${n}"`;throw new b("INTERNAL",`${h}: declared secret "${o}" is not set on the Worker environment. Add it to .dev.vars for local dev and run \`wrangler secret put ${o}\` for production.`)}r[o]=a}return r};export{g as DURABLE_OBJECT_POLICY_FORBIDDEN_KEYS,x as containerBindingName,R as containerBuildTag,j as containerClassName,V as defineContainer,P as isCloudflareRegistryDigest,L as isContainerDefinition,w as isManagedImage,D as normalizeContainerImage,M as parseDurationSeconds,H as resolveContainerEnvVars};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as S}from"@lunora/errors";import{e as C,p as j,a as g,r as z}from"./exec-BYLW70sR.mjs";import{S as X}from"./sampling-D21rpu_s.mjs";import{containerBindingName as Y}from"./DURABLE_OBJECT_POLICY_FORBIDDEN_KEYS-Dk6cuK7G.mjs";import{a as q}from"./jurisdiction-DtE9s70w.mjs";const p=3,F=3e4,G=3,J=500,K="cf-container-target-port",O="__lunora",Q=t=>{const e=t.indexOf(";");return e===-1?t:t.slice(0,e)},V=t=>{const e=t.split("/").find(o=>o!=="")??"";return Q(e).toLowerCase()},b=(t,e)=>{const o=typeof t=="string"?t:t.url,r=URL.parse(o.startsWith("/")?`http://container${o}`:o);if(r!==null&&j(r.pathname,n=>V(n)===O))throw new S("BAD_REQUEST",`${e}: \`/${O}/*\` is reserved for Lunora's own container routes and cannot be reached with \`fetch\`. Use \`exec\` to run a command.`)},L=(t,e)=>{if(typeof t!="string"){const r=new Request(t,e);return r.headers.delete(g),[r,void 0]}if(e?.headers===void 0)return[t,e];const o=new Headers(e.headers);return o.delete(g),[t,{...e,headers:o}]},H=(t,e,o,r)=>{const n=typeof t=="string"&&t.startsWith("/")?new Request(`http://container${t}`,e):new Request(t,e);return o!==void 0&&n.headers.set(K,String(o)),r?.traceparent!==void 0&&n.headers.set("traceparent",r.traceparent),r?.sampleErrors!==void 0&&n.headers.set(X,r.sampleErrors?"1":"0"),n},$=async t=>{t<=0||await new Promise(e=>{setTimeout(e,t)})},Z=/no container instance|not listening|try again later|rate.?limit|provision/i,tt="no Container instance available",et="Failed to start container:",ot=1024,rt=async t=>(await z(t.clone().body,ot)).text,M=t=>t instanceof Error&&Z.test(t.message),B=async t=>{if(t.status===429)return!0;if(t.status!==500&&t.status!==503)return!1;try{const e=await rt(t);return e.includes(tt)||e.startsWith(et)}catch{return!1}},E=t=>`ctx.containers.${t.exportName}`,R=(t,e,o={},r,n)=>{const s=Math.max(1,o.attempts??G),a=o.backoffMs??J,c=o.maxBackoffMs??F,v=async(l,h)=>{const A=typeof l=="string"?s:1;let d;for(let m=0;m<A;m+=1){const u=m===A-1;m>0&&await $(Math.min(a*2**(m-1),c));try{const f=await t(H(l,h,r,n),h?.signal);if(u||!await B(f))return f}catch(f){if(d=f,u||!M(f))throw f}}throw d instanceof Error?d:new Error("ctx.containers: cold-start retry exhausted")};return{exec:C(v,e),fetch:async(l,h)=>(b(l,e),v(...L(l,h))),port:l=>R(t,e,o,l,n)}},nt=async(t,e)=>{if(e.aborted)throw t.catch(()=>{}),e.reason;const o=new AbortController,r=new Promise((n,s)=>{e.addEventListener("abort",()=>{s(e.reason)},{once:!0,signal:o.signal})});try{return await Promise.race([t,r])}finally{o.abort(),t.catch(()=>{})}},N=async(t,e,o)=>{if(!e.headers.has(g))return t.fetch(e);if(typeof t.lunoraExec!="function")throw new TypeError("ctx.containers: this container DO does not expose lunoraExec() — is @lunora/container/do up to date?");e.headers.delete(g);const r=t.lunoraExec(new Request(e.url,{body:await e.text(),headers:e.headers,method:e.method}));return o?nt(r,o):r},k=(t,e,o,r,n)=>R(async(s,a)=>N(t.get(t.idFromName(e)),s,a),o,r,void 0,n),i=async(t,e,o,r)=>{const n=t[e];if(typeof n!="function")throw new TypeError(`ctx.containers: the "${o}" container DO does not expose ${e}() — is @lunora/container/do up to date?`);return n(r)},st=(t,e)=>({allow:async o=>i(t(),"allowHost",e,o),deny:async o=>i(t(),"denyHost",e,o),removeAllowed:async o=>i(t(),"removeAllowedHost",e,o),removeDenied:async o=>i(t(),"removeDeniedHost",e,o),setAllowed:async o=>i(t(),"setAllowedHosts",e,[...o]),setDenied:async o=>i(t(),"setDeniedHosts",e,[...o])}),I=(t,e,o,r,n)=>{const s=()=>t.get(t.idFromName(o));return{...R(async(a,c)=>N(s(),a,c),E(e),r,void 0,n),destroy:async()=>i(s(),"destroy",e.binding),egress:st(s,e.binding),getState:async()=>i(s(),"getState",e.binding),renewActivityTimeout:async()=>i(s(),"renewActivityTimeout",e.binding),snapshot:async a=>i(s(),"lunoraSnapshot",e.binding,a),start:async a=>i(s(),"start",e.binding,a),stop:async a=>i(s(),"stop",e.binding,a)}},T="pool-",P=t=>`${T}${String(Math.floor(Math.random()*t))}`,U=(t,e)=>{if(t.startsWith(T))throw new S("BAD_REQUEST",`${E(e)}.get(${JSON.stringify(t)}): the "${T}" prefix is reserved for the instances .any()/.pool() pick, so this name would share a container (disk, lifecycle) with a pool member. Pick a name that does not start with "${T}".`)},at=t=>t.status>=500,D=(t,e,o={},r,n)=>{const s=o.size??e.maxInstances??p,a=Math.max(1,o.attempts??3),c=o.backoffMs??100,v=o.maxBackoffMs??F,l=`${E(e)}.pool()`,h=(d,m)=>async(u,f)=>{const _=typeof u=="string"?a:1;let x;for(let w=0;w<_;w+=1){w>0&&await $(Math.min(c*2**(w-1),v));const W=H(u,f,r,n);try{const y=await N(t.get(t.idFromName(P(s))),W,f?.signal);if(w===_-1||!await d(y))return y}catch(y){if(x=y,!m(y))throw y}}throw x instanceof Error?x:new Error(`ctx.containers.${e.exportName}.pool(): all ${String(_)} attempts failed`)},A=h(o.retryOn??at,()=>!0);return{exec:C(h(B,M),l),fetch:async(d,m)=>(b(d,l),A(...L(d,m))),port:d=>D(t,e,o,d,n)}},ct=(t,e,o)=>({any:(r,n)=>k(t,P(r??e.maxInstances??p),E(e),n,o),get:(r,n)=>(U(r,e),I(t,e,r,n,o)),pool:r=>D(t,e,r,void 0,o)}),it=t=>{const e=()=>{throw new S("INTERNAL",`ctx.containers.${t.exportName}: no "${t.binding}" Durable Object binding found. Run \`lunora dev\` (or \`lunora deploy\`) to reconcile wrangler.jsonc, and make sure the worker entry re-exports the generated container classes.`)};return{any:e,get:e,pool:e}},ut=(t,e,o,r,n)=>{const s={};for(const a of e){const c=t[a.binding];s[a.exportName]=c&&typeof c.idFromName=="function"&&typeof c.get=="function"?ct(q(c,o),a,{...n===void 0?{}:{sampleErrors:n},...r===void 0?{}:{traceparent:r}}):it(a)}return s},lt=t=>{const e=o=>({allowHost:()=>Promise.resolve(),denyHost:()=>Promise.resolve(),destroy:()=>Promise.resolve(),fetch:r=>Promise.resolve(t(r,{name:o})),lunoraExec:r=>Promise.resolve(t(r,{name:o})),lunoraSnapshot:r=>Promise.resolve({id:`test-snapshot-${o}`,size:0,...r?.name===void 0?{}:{name:r.name}}),getState:()=>Promise.resolve({lastChange:0}),removeAllowedHost:()=>Promise.resolve(),removeDeniedHost:()=>Promise.resolve(),renewActivityTimeout:()=>Promise.resolve(),setAllowedHosts:()=>Promise.resolve(),setDeniedHosts:()=>Promise.resolve(),start:()=>Promise.resolve(),stop:()=>Promise.resolve()});return{get:o=>e(String(o)),idFromName:o=>o}},wt=t=>{const e={};for(const[o,r]of Object.entries(t)){const n=lt(r),s={binding:Y(o),exportName:o};e[o]={any:(a,c)=>k(n,P(a??s.maxInstances??p),E(s),{attempts:1,...c}),get:(a,c)=>(U(a,s),I(n,s,a,{attempts:1,...c})),pool:a=>D(n,s,{...a,attempts:1})}}return e};export{ut as createContainerContext,wt as createContainerTestContext};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as d}from"@lunora/errors";import{a as w}from"./abort-deadline-jwbW9Ala.mjs";const E=async(o,t,r)=>{if(o===null)return{overflowed:!1,text:""};const e=o.getReader(),n=new TextDecoder,a=()=>{e.cancel().catch(()=>{})};r?.addEventListener("abort",a);let s="",c=0,i=!1;try{for(;r?.aborted!==!0;){const{done:f,value:u}=await e.read();if(f)break;if(c+u.byteLength>t){i=!0,s+=n.decode(u.subarray(0,t-c),{stream:!0});break}c+=u.byteLength,s+=n.decode(u,{stream:!0})}s+=n.decode()}finally{r?.removeEventListener("abort",a),e.cancel().catch(()=>{})}if(r?.aborted===!0)throw r.reason;return{overflowed:i,text:s}},l="/__lunora/exec",h="x-lunora-container-exec",x=5,m=o=>{try{return decodeURIComponent(o)}catch{return o.replaceAll(/%[\da-f]{2}/giu,t=>{try{return decodeURIComponent(t)}catch{return t}})}},_=(o,t)=>{let r=o;for(let e=0;e<=x;e+=1){if(t(r))return!0;const n=m(r);if(n===r)return!1;r=n}return!0},b=512,p=1e6,v="<redacted>",g=(o,t)=>{if(t===void 0)return o;let r=o;for(const e of Object.values(t))e.length>0&&(r=r.split(e).join(v));return r},T=o=>{if(o===null||typeof o!="object")return;const{code:t,stderr:r,stdout:e}=o;if(typeof t=="number")return{code:t,stderr:typeof r=="string"?r:"",stdout:typeof e=="string"?e:""}},A=(o,t)=>w(o,t,()=>new d("INTERNAL",`ctx.containers: exec timed out after ${String(t)}ms`)),R=(o,t)=>async(r,e={})=>{if(typeof r!="string"||r.length===0)throw new d("BAD_REQUEST",`${t}: exec requires a non-empty \`command\``);const n=e.maxOutputBytes??p;if(!Number.isSafeInteger(n)||n<=0)throw new d("BAD_REQUEST",`${t}: exec \`maxOutputBytes\` must be a positive whole number of bytes, got ${String(n)}`);const a=A(e.signal,e.timeoutMs);try{const s=await o(l,{body:JSON.stringify({args:e.args??[],command:r,...e.cwd===void 0?{}:{cwd:e.cwd},...e.env===void 0?{}:{env:e.env},maxOutputBytes:n,...e.timeoutMs===void 0?{}:{timeoutMs:e.timeoutMs}}),headers:{"content-type":"application/json",[h]:"1"},method:"POST",...a.signal===void 0?{}:{signal:a.signal}});if(!s.ok){const u=await E(s.body,b,a.signal).then(y=>g(y.text,e.env),()=>"<unreadable body>");throw new d("INTERNAL",`${t}: exec failed — the container answered ${String(s.status)} for POST ${l}. Does it serve that route? Body: ${u}`)}const c=await E(s.body,n,a.signal);if(c.overflowed)throw new d("INTERNAL",`${t}: exec response exceeded ${String(n)} bytes and was abandoned — it is buffered whole to be parsed, so an unbounded one would exhaust the isolate. Cap the command's output, or raise \`maxOutputBytes\`.`);let i;try{i=JSON.parse(c.text)}catch{throw new d("INTERNAL",`${t}: exec response was not JSON — expected {"code","stdout","stderr"} from POST ${l}`)}const f=T(i);if(f===void 0)throw new d("INTERNAL",`${t}: exec response is missing a numeric \`code\` — expected {"code","stdout","stderr"} from POST ${l}`);return f}finally{a.dispose()}};export{l as C,p as D,h as a,R as e,_ as p,E as r};
|
|
@@ -26,6 +26,22 @@ interface CustomContainerInstanceType {
|
|
|
26
26
|
* `ContainerInstanceType` is part of the experimental `@lunora/container` API and may change without a major version bump.
|
|
27
27
|
*/
|
|
28
28
|
type ContainerInstanceType = CustomContainerInstanceType | NamedContainerInstanceType;
|
|
29
|
+
/**
|
|
30
|
+
* An instance size a Durable Object can pick at start under the
|
|
31
|
+
* `durable_object` scheduling policy. The runtime does not accept `basic` (or
|
|
32
|
+
* the legacy `dev`/`standard` aliases), and a custom size must set all three
|
|
33
|
+
* fields — it is passed to `ctx.container.start({ instance })` as-is.
|
|
34
|
+
*/
|
|
35
|
+
type ContainerRuntimeInstanceType = Exclude<NamedContainerInstanceType, "basic"> | Required<CustomContainerInstanceType>;
|
|
36
|
+
/**
|
|
37
|
+
* One named image under the `durable_object` scheduling policy. A `string` is
|
|
38
|
+
* a local Dockerfile path or directory, built and uploaded by `wrangler deploy`
|
|
39
|
+
* (wrangler `dockerfile` + `build_context`); `{ registry }` must be a
|
|
40
|
+
* digest-pinned (`@sha256:…`) reference under `registry.cloudflare.com/`,
|
|
41
|
+
* the Cloudflare managed registry — Cloudflare does not accept Docker Hub, ECR
|
|
42
|
+
* or Artifact Registry references here.
|
|
43
|
+
*/
|
|
44
|
+
type ContainerNamedImageSource = RegistryImageSource | string;
|
|
29
45
|
/**
|
|
30
46
|
* Rolling-deploy tuning for a container.
|
|
31
47
|
*/
|
|
@@ -73,17 +89,17 @@ type ContainerImageSource = BuildImageSource | RegistryImageSource | string;
|
|
|
73
89
|
interface ContainerReadinessCheck {
|
|
74
90
|
/** HTTP path probed on the container, e.g. `"/ready"` (a leading slash is optional). */
|
|
75
91
|
path: string;
|
|
76
|
-
/** Port to probe. Defaults to {@link
|
|
92
|
+
/** Port to probe. Defaults to {@link ContainerConfigBase.defaultPort}. */
|
|
77
93
|
port?: number;
|
|
78
94
|
/** HTTP status that means "ready". Defaults to `200`. */
|
|
79
95
|
status?: number;
|
|
80
96
|
}
|
|
81
97
|
/**
|
|
82
|
-
*
|
|
98
|
+
* The fields every container accepts, whichever scheduling policy it uses.
|
|
83
99
|
*/
|
|
84
|
-
interface
|
|
100
|
+
interface ContainerConfigBase {
|
|
85
101
|
/**
|
|
86
|
-
* Hostnames the container may reach **even when {@link
|
|
102
|
+
* Hostnames the container may reach **even when {@link ContainerConfigBase.enableInternet}
|
|
87
103
|
* is `false`** — an egress allow-list (Cloudflare's `allowedHosts`). Glob
|
|
88
104
|
* patterns like `*.stripe.com` are supported. Pair with `enableInternet:
|
|
89
105
|
* false` to deny all egress except these hosts (the firewall pattern
|
|
@@ -96,29 +112,29 @@ interface ContainerConfig {
|
|
|
96
112
|
/**
|
|
97
113
|
* Build-time variables for a Dockerfile/Railpack image — wrangler's
|
|
98
114
|
* `image_vars` (equivalent to `docker build --build-arg`). For *runtime*
|
|
99
|
-
* values use {@link
|
|
115
|
+
* values use {@link ContainerConfigBase.env} / {@link ContainerConfigBase.secrets}.
|
|
100
116
|
* Ignored for a pre-built `{ registry }` image.
|
|
101
117
|
*/
|
|
102
118
|
buildArgs?: Readonly<Record<string, string>>;
|
|
103
119
|
/**
|
|
104
120
|
* The port the container listens on. Worker → container requests target
|
|
105
121
|
* this port. Locally the Dockerfile must also `EXPOSE` it. For a
|
|
106
|
-
* multi-port container also declare {@link
|
|
122
|
+
* multi-port container also declare {@link ContainerConfigBase.requiredPorts}
|
|
107
123
|
* and route per request with the handle's `.port(n)`.
|
|
108
124
|
*/
|
|
109
125
|
defaultPort?: number;
|
|
110
126
|
/**
|
|
111
127
|
* Hostnames the container may **never** reach — an egress deny-list
|
|
112
128
|
* (Cloudflare's `deniedHosts`). Overrides everything else, including
|
|
113
|
-
* `enableInternet: true` and {@link
|
|
129
|
+
* `enableInternet: true` and {@link ContainerConfigBase.allowedHosts}. Glob
|
|
114
130
|
* patterns like `*.evil.com` are supported.
|
|
115
131
|
*/
|
|
116
132
|
deniedHosts?: ReadonlyArray<string>;
|
|
117
133
|
/**
|
|
118
134
|
* Whether the container may open outbound internet connections. Defaults
|
|
119
135
|
* to `true` — the platform default. Note that container egress is billed
|
|
120
|
-
* per GB by Cloudflare. Combine with {@link
|
|
121
|
-
* {@link
|
|
136
|
+
* per GB by Cloudflare. Combine with {@link ContainerConfigBase.allowedHosts} /
|
|
137
|
+
* {@link ContainerConfigBase.deniedHosts} for a precise egress firewall.
|
|
122
138
|
*/
|
|
123
139
|
enableInternet?: boolean;
|
|
124
140
|
/**
|
|
@@ -129,14 +145,14 @@ interface ContainerConfig {
|
|
|
129
145
|
entrypoint?: ReadonlyArray<string>;
|
|
130
146
|
/**
|
|
131
147
|
* Static environment variables passed to the container on every start.
|
|
132
|
-
* For secret values use {@link
|
|
148
|
+
* For secret values use {@link ContainerConfigBase.secrets} instead so they
|
|
133
149
|
* flow through Worker Secrets rather than source code.
|
|
134
150
|
*/
|
|
135
151
|
env?: Readonly<Record<string, string>>;
|
|
136
152
|
/**
|
|
137
153
|
* Hard cap on how long an instance may run, measured from start regardless
|
|
138
154
|
* of activity — a runaway-cost backstop on top of the idle
|
|
139
|
-
* {@link
|
|
155
|
+
* {@link ContainerConfigBase.sleepAfter}. Same grammar as `sleepAfter`
|
|
140
156
|
* (`"30s"`, `"5m"`, `"1h"`, or a plain number of seconds). When it elapses,
|
|
141
157
|
* the `LunoraContainer.onHardTimeoutExpired` hook runs (default: `stop()`,
|
|
142
158
|
* which sends SIGTERM and does not escalate — a container that ignores the
|
|
@@ -144,13 +160,6 @@ interface ContainerConfig {
|
|
|
144
160
|
* `destroy()`). (Upstream cloudflare/containers#85.)
|
|
145
161
|
*/
|
|
146
162
|
hardTimeout?: number | string;
|
|
147
|
-
/** Image source — a local Dockerfile path/directory or a registry reference. */
|
|
148
|
-
image: ContainerImageSource;
|
|
149
|
-
/**
|
|
150
|
-
* Resource class for each instance: a named Cloudflare instance type or a
|
|
151
|
-
* custom `{ vcpu, memoryMib, diskMb }` object.
|
|
152
|
-
*/
|
|
153
|
-
instanceType?: ContainerInstanceType;
|
|
154
163
|
/**
|
|
155
164
|
* Intercept the container's outbound **HTTPS** traffic so the egress
|
|
156
165
|
* allow/deny lists apply to TLS connections too (Cloudflare's
|
|
@@ -166,11 +175,6 @@ interface ContainerConfig {
|
|
|
166
175
|
* `start({ labels })`.
|
|
167
176
|
*/
|
|
168
177
|
labels?: Readonly<Record<string, string>>;
|
|
169
|
-
/**
|
|
170
|
-
* Maximum number of concurrently *running* instances. Stopped (slept)
|
|
171
|
-
* containers don't count. Also the default pool size for `.any()`.
|
|
172
|
-
*/
|
|
173
|
-
maxInstances?: number;
|
|
174
178
|
/**
|
|
175
179
|
* Override for the wrangler `containers[].name` identifier. Defaults to
|
|
176
180
|
* wrangler's own default (worker name + class name + environment).
|
|
@@ -196,17 +200,10 @@ interface ContainerConfig {
|
|
|
196
200
|
* Ports the container must be listening on before it's considered ready
|
|
197
201
|
* (Cloudflare's `requiredPorts`) — for multi-port containers. Start-up
|
|
198
202
|
* waits for every listed port, and the handle's `.port(n)` routes a request
|
|
199
|
-
* to any of them; {@link
|
|
203
|
+
* to any of them; {@link ContainerConfigBase.defaultPort} is the target when a
|
|
200
204
|
* request doesn't pick one.
|
|
201
205
|
*/
|
|
202
206
|
requiredPorts?: ReadonlyArray<number>;
|
|
203
|
-
/**
|
|
204
|
-
* Rolling-deploy tuning. `stepPercentage` is the share of instances updated
|
|
205
|
-
* per rollout step (wrangler `rollout_step_percentage`); `gracePeriodSeconds`
|
|
206
|
-
* is how long an active instance is left running before it's eligible for
|
|
207
|
-
* update (wrangler `rollout_active_grace_period`).
|
|
208
|
-
*/
|
|
209
|
-
rollout?: ContainerRollout;
|
|
210
207
|
/**
|
|
211
208
|
* Names of Worker secrets (from `wrangler secret` / `.dev.vars`) forwarded
|
|
212
209
|
* into the container's environment at instance start. Each declared name
|
|
@@ -220,7 +217,7 @@ interface ContainerConfig {
|
|
|
220
217
|
* binding name*. Each binding is resolved with its async `.get()` the first
|
|
221
218
|
* time the instance starts, then injected as that env var — e.g.
|
|
222
219
|
* `{ STRIPE_KEY: "STRIPE_SECRET" }` runs `env.STRIPE_SECRET.get()` and sets
|
|
223
|
-
* `STRIPE_KEY` inside the container. Unlike {@link
|
|
220
|
+
* `STRIPE_KEY` inside the container. Unlike {@link ContainerConfigBase.secrets}
|
|
224
221
|
* (plain Worker text secrets), this pulls from a `secrets_store_secrets`
|
|
225
222
|
* binding. A name already used by `env`/`secrets` is rejected at authoring
|
|
226
223
|
* time; a missing binding or unreadable value fails the start. Applies
|
|
@@ -236,13 +233,95 @@ interface ContainerConfig {
|
|
|
236
233
|
*/
|
|
237
234
|
sleepAfter?: number | string;
|
|
238
235
|
}
|
|
236
|
+
/**
|
|
237
|
+
* A container under the `default` scheduling policy: one application-wide
|
|
238
|
+
* image and instance size, set in wrangler and rolled out by Cloudflare on
|
|
239
|
+
* deploy.
|
|
240
|
+
*/
|
|
241
|
+
interface DefaultScheduledContainerConfig extends ContainerConfigBase {
|
|
242
|
+
/** Image source — a local Dockerfile path/directory or a registry reference. */
|
|
243
|
+
image: ContainerImageSource;
|
|
244
|
+
/**
|
|
245
|
+
* Resource class for each instance: a named Cloudflare instance type or a
|
|
246
|
+
* custom `{ vcpu, memoryMib, diskMb }` object.
|
|
247
|
+
*/
|
|
248
|
+
instanceType?: ContainerInstanceType;
|
|
249
|
+
/**
|
|
250
|
+
* Maximum number of concurrently *running* instances. Stopped (slept)
|
|
251
|
+
* containers don't count. Also the default pool size for `.any()`.
|
|
252
|
+
*/
|
|
253
|
+
maxInstances?: number;
|
|
254
|
+
/**
|
|
255
|
+
* Rolling-deploy tuning. `stepPercentage` is the share of instances updated
|
|
256
|
+
* per rollout step (wrangler `rollout_step_percentage`); `gracePeriodSeconds`
|
|
257
|
+
* is how long an active instance is left running before it's eligible for
|
|
258
|
+
* update (wrangler `rollout_active_grace_period`).
|
|
259
|
+
*/
|
|
260
|
+
rollout?: ContainerRollout;
|
|
261
|
+
/**
|
|
262
|
+
* Where the image and instance size are configured. `"default"` (the
|
|
263
|
+
* default) sets them once in wrangler; `"durable_object"` is the
|
|
264
|
+
* per-instance policy.
|
|
265
|
+
* Immutable once deployed: switching policies means a new container
|
|
266
|
+
* application, which replaces every running instance.
|
|
267
|
+
*/
|
|
268
|
+
schedulingPolicy?: "default";
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* A container under the `durable_object` scheduling policy (Cloudflare public
|
|
272
|
+
* beta): each instance picks its image and size when it starts, via
|
|
273
|
+
* `ctx.containers.<name>.get(id).start({ image, instanceType })`, and can save
|
|
274
|
+
* and restore its filesystem with `snapshot()` / `start({ snapshot })`.
|
|
275
|
+
* Instances keep the image they started with across deploys — there is no
|
|
276
|
+
* application-wide rollout, and no `maxInstances` cap (running instances count
|
|
277
|
+
* against the account limit).
|
|
278
|
+
*/
|
|
279
|
+
interface DurableObjectScheduledContainerConfig extends ContainerConfigBase {
|
|
280
|
+
/**
|
|
281
|
+
* The image a start uses when it names none — including the implicit start a
|
|
282
|
+
* `fetch`/`exec` triggers. Either a key of `images`
|
|
283
|
+
* or a Cloudflare-managed image such as `"cloudflare/debian-trixie"`.
|
|
284
|
+
* Without it, an instance must be started explicitly with an `image` (or a
|
|
285
|
+
* `snapshot`) before it can serve requests.
|
|
286
|
+
*/
|
|
287
|
+
image?: string;
|
|
288
|
+
/**
|
|
289
|
+
* Named images an instance can start from, keyed by the name `start({ image })`
|
|
290
|
+
* takes (wrangler `containers[].images`). Up to 100, each name 1–128
|
|
291
|
+
* characters. Updating the map does not restart running instances.
|
|
292
|
+
*/
|
|
293
|
+
images?: Readonly<Record<string, ContainerNamedImageSource>>;
|
|
294
|
+
/**
|
|
295
|
+
* The instance size a start uses when it names none. Cloudflare's runtime
|
|
296
|
+
* default is `"lite"`.
|
|
297
|
+
*/
|
|
298
|
+
instanceType?: ContainerRuntimeInstanceType;
|
|
299
|
+
/** Selects the per-instance policy. Immutable once deployed. */
|
|
300
|
+
schedulingPolicy: "durable_object";
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* `ContainerConfig` is part of the experimental `@lunora/container` API and may change without a major version bump.
|
|
304
|
+
*/
|
|
305
|
+
type ContainerConfig = DefaultScheduledContainerConfig | DurableObjectScheduledContainerConfig;
|
|
239
306
|
/**
|
|
240
307
|
* The value `defineContainer` returns: the validated config plus a brand the
|
|
241
308
|
* codegen discovery and the generated Container DO class key on.
|
|
242
309
|
*/
|
|
243
|
-
|
|
310
|
+
type ContainerDefinition = ContainerConfig & {
|
|
244
311
|
/** Brand marking a value as a Lunora container definition. */
|
|
245
312
|
readonly isLunoraContainer: true;
|
|
313
|
+
};
|
|
314
|
+
/**
|
|
315
|
+
* A saved container filesystem, as `snapshot()` returns it. Plain data: store
|
|
316
|
+
* it anywhere and pass it back to `start({ snapshot })`, from the same or
|
|
317
|
+
* another instance of the same container. Tied to the image it was taken
|
|
318
|
+
* from; Cloudflare keeps it 30 days, refreshed on each restore.
|
|
319
|
+
*/
|
|
320
|
+
interface ContainerSnapshot {
|
|
321
|
+
id: string;
|
|
322
|
+
name?: string;
|
|
323
|
+
/** Size in bytes. */
|
|
324
|
+
size: number;
|
|
246
325
|
}
|
|
247
326
|
/**
|
|
248
327
|
* A normalized image source, as written into `wrangler.jsonc`.
|
|
@@ -268,4 +347,4 @@ type NormalizedContainerImage = {
|
|
|
268
347
|
* @see https://developers.cloudflare.com/durable-objects/reference/data-location/
|
|
269
348
|
*/
|
|
270
349
|
type DurableObjectJurisdiction = "eu" | "fedramp" | "us";
|
|
271
|
-
export { BuildImageSource as B,
|
|
350
|
+
export { BuildImageSource as B, ContainerSnapshot as C, DurableObjectJurisdiction as D, NormalizedContainerImage as N, RegistryImageSource as R, ContainerRuntimeInstanceType as a, DefaultScheduledContainerConfig as b, DurableObjectScheduledContainerConfig as c, ContainerDefinition as d, ContainerImageSource as e, ContainerConfig as f, ContainerConfigBase as g, ContainerInstanceType as h, ContainerNamedImageSource as i, ContainerReadinessCheck as j, ContainerRollout as k, CustomContainerInstanceType as l, NamedContainerInstanceType as m };
|