@lunora/container 1.0.0-alpha.2 → 1.0.0-alpha.21

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/dist/do/index.mjs CHANGED
@@ -1,138 +1 @@
1
- import { Container } from '@cloudflare/containers';
2
- import { resolveContainerEnvVars as resolveContainerEnvVariables } from '../packem_shared/containerBindingName-BGdSdFNA.mjs';
3
-
4
- const LUNORA_EVENT_SOURCE = "lunora";
5
- const buildContainerLifecycleEvent = (container, instance, event, message) => {
6
- return {
7
- container,
8
- event,
9
- instance,
10
- level: event === "error" ? "error" : "info",
11
- message,
12
- source: LUNORA_EVENT_SOURCE,
13
- ts: Date.now(),
14
- type: "container"
15
- };
16
- };
17
- const emitContainerLifecycle = (container, instance, event, message) => {
18
- const envelope = buildContainerLifecycleEvent(container, instance, event, message);
19
- const line = JSON.stringify(envelope);
20
- if (event === "error") {
21
- console.error(line);
22
- } else {
23
- console.log(line);
24
- }
25
- return envelope;
26
- };
27
-
28
- const RECORD_CONTAINER_EVENT_OP = "__lunora_admin__:recordContainerEvent";
29
- const ROOT_SHARD_NAME = "__root__";
30
- const applyJurisdiction = (namespace, jurisdiction) => {
31
- if (jurisdiction === void 0) {
32
- return namespace;
33
- }
34
- if (typeof namespace.jurisdiction !== "function") {
35
- throw new TypeError(
36
- `@lunora/container: Durable Object namespace does not support jurisdiction("${jurisdiction}") — update @cloudflare/workers-types or remove the jurisdiction option`
37
- );
38
- }
39
- return namespace.jurisdiction(jurisdiction);
40
- };
41
- const isShardNamespace = (value) => {
42
- if (value === null || typeof value !== "object") {
43
- return false;
44
- }
45
- const candidate = value;
46
- return typeof candidate.get === "function" && typeof candidate.idFromName === "function";
47
- };
48
- const resolveRootShard = (namespace, jurisdiction) => {
49
- const pinned = applyJurisdiction(namespace, jurisdiction);
50
- if (typeof pinned.getByName === "function") {
51
- return pinned.getByName(ROOT_SHARD_NAME);
52
- }
53
- return pinned.get(pinned.idFromName(ROOT_SHARD_NAME));
54
- };
55
- const reportContainerLifecycle = async (env, envelope, jurisdiction) => {
56
- try {
57
- const envRecord = env ?? {};
58
- const namespace = envRecord["SHARD"];
59
- if (!isShardNamespace(namespace)) {
60
- return;
61
- }
62
- const adminBearer = typeof envRecord["LUNORA_ADMIN_TOKEN"] === "string" ? envRecord["LUNORA_ADMIN_TOKEN"] : void 0;
63
- if (!adminBearer || adminBearer.length === 0) {
64
- return;
65
- }
66
- const request = new Request("https://shard.internal/rpc", {
67
- body: JSON.stringify({ args: { event: envelope }, functionPath: RECORD_CONTAINER_EVENT_OP }),
68
- headers: { authorization: `Bearer ${adminBearer}`, "content-type": "application/json" },
69
- method: "POST"
70
- });
71
- await resolveRootShard(namespace, jurisdiction).fetch(request);
72
- } catch {
73
- }
74
- };
75
-
76
- class LunoraContainer extends Container {
77
- /**
78
- * Data-residency jurisdiction the app's DOs are pinned to (codegen passes the
79
- * schema's `.jurisdiction("…")`). Used to pin the best-effort lifecycle report
80
- * to the same region as the root shard. `undefined` ⇒ un-pinned.
81
- */
82
- lunoraJurisdiction;
83
- /** The `lunora/containers.ts` export name, for lifecycle log correlation. */
84
- lunoraName;
85
- constructor(context, env, definition, exportName, jurisdiction) {
86
- super(context, env, {
87
- defaultPort: definition.defaultPort,
88
- envVars: resolveContainerEnvVariables(definition, env, exportName),
89
- sleepAfter: definition.sleepAfter
90
- });
91
- if (definition.enableInternet !== void 0) {
92
- this.enableInternet = definition.enableInternet;
93
- }
94
- this.lunoraName = exportName ?? "container";
95
- this.lunoraJurisdiction = jurisdiction;
96
- }
97
- onError(error) {
98
- const envelope = emitContainerLifecycle(this.lunoraName, this.instanceId(), "error", error instanceof Error ? error.message : String(error));
99
- this.surfaceInStudioLogs(envelope);
100
- return super.onError(error);
101
- }
102
- async onStart() {
103
- const envelope = emitContainerLifecycle(this.lunoraName, this.instanceId(), "start");
104
- this.surfaceInStudioLogs(envelope);
105
- await super.onStart();
106
- }
107
- async onStop(parameters) {
108
- const envelope = emitContainerLifecycle(this.lunoraName, this.instanceId(), "stop", `${parameters.reason} (exit ${String(parameters.exitCode)})`);
109
- this.surfaceInStudioLogs(envelope);
110
- await super.onStop(parameters);
111
- }
112
- /**
113
- * Best-effort push of `envelope` into the root ShardDO's log buffer so it
114
- * also appears in the Studio Logs panel (the terminal already has it via
115
- * `emitContainerLifecycle`). Fire-and-forget and fully swallowed: a missing
116
- * `SHARD` binding, a missing admin token, or a fetch failure NEVER throws
117
- * out of a lifecycle hook — the `console` path stays the source of truth.
118
- */
119
- surfaceInStudioLogs(envelope) {
120
- reportContainerLifecycle(this.env, envelope, this.lunoraJurisdiction).catch(() => {
121
- });
122
- }
123
- /**
124
- * Per-instance correlation id: the Durable Object id, which Cloudflare also
125
- * injects into the container as `CLOUDFLARE_DURABLE_OBJECT_ID`. Read
126
- * defensively — the id shape varies and isn't worth crashing a hook over.
127
- */
128
- instanceId() {
129
- try {
130
- const { id } = this.ctx;
131
- return typeof id?.toString === "function" ? id.toString() : "unknown";
132
- } catch {
133
- return "unknown";
134
- }
135
- }
136
- }
137
-
138
- export { LunoraContainer as default };
1
+ import{Container as S}from"../packem_shared/ContainerProxy-BsQAwSNX.mjs";import{ContainerProxy as A,outboundParams as L}from"../packem_shared/ContainerProxy-BsQAwSNX.mjs";import{LunoraError as u}from"@lunora/errors";import{resolveContainerEnvVars as g,parseDurationSeconds as v}from"../packem_shared/containerBindingName-DP2NqQV-.mjs";import{e as y}from"../packem_shared/jurisdiction-BKRNOTip.mjs";const w="lunora",N=(i,t,n,e)=>({container:i,event:n,instance:t,level:n==="error"?"error":"info",message:e,source:w,ts:Date.now(),type:"container"}),a=(i,t,n,e)=>{const o=N(i,t,n,e),r=JSON.stringify(o);return n==="error"?console.error(r):console.log(r),o},$="__lunora_admin__:recordContainerEvent",h="__root__",E=i=>{if(i===null||typeof i!="object")return!1;const t=i;return typeof t.get=="function"&&typeof t.idFromName=="function"},I=(i,t)=>{const n=y(i,t);return typeof n.getByName=="function"?n.getByName(h):n.get(n.idFromName(h))},T=async(i,t,n)=>{try{const e=i??{},o=e.SHARD;if(!E(o))return;const r=typeof e.LUNORA_ADMIN_TOKEN=="string"?e.LUNORA_ADMIN_TOKEN:void 0;if(!r||r.length===0)return;const s=new Request("https://shard.internal/rpc",{body:JSON.stringify({args:{event:t},functionPath:$}),headers:{authorization:`Bearer ${r}`,"content-type":"application/json"},method:"POST"});await I(o,n).fetch(s)}catch{}},l=500,p=3e4,d="__lunoraHardTimeoutGeneration";class _ extends S{lunoraJurisdiction;lunoraName;lunoraDefaultPort;lunoraHardTimeoutSeconds;lunoraReadyOn;lunoraSecretsStore;lunoraSecretsStoreResolved;constructor(t,n,e,o,r){super(t,n,{defaultPort:e.defaultPort,entrypoint:e.entrypoint?[...e.entrypoint]:void 0,envVars:g(e,n,o),sleepAfter:e.sleepAfter}),e.enableInternet!==void 0&&(this.enableInternet=e.enableInternet),e.requiredPorts!==void 0&&(this.requiredPorts=[...e.requiredPorts]),e.interceptHttps!==void 0&&(this.interceptHttps=e.interceptHttps),e.allowedHosts!==void 0&&(this.allowedHosts=[...e.allowedHosts]),e.deniedHosts!==void 0&&(this.deniedHosts=[...e.deniedHosts]),e.pingEndpoint!==void 0&&(this.pingEndpoint=e.pingEndpoint),e.labels!==void 0&&(this.labels={...e.labels}),this.lunoraName=o??"container",this.lunoraJurisdiction=r,this.lunoraDefaultPort=e.defaultPort,this.lunoraReadyOn=e.readyOn?[...e.readyOn]:[],this.lunoraHardTimeoutSeconds=e.hardTimeout===void 0?void 0:v(e.hardTimeout),this.lunoraSecretsStore=e.secretsStore}async containerFetch(...t){return await this.resolveSecretsStoreEnv(),super.containerFetch(...t)}async start(...t){const[n]=t;return n?.envVars===void 0&&await this.resolveSecretsStoreEnv(),super.start(...t)}async onActivityExpired(){const t=a(this.lunoraName,this.instanceId(),"sleep");this.surfaceInStudioLogs(t),await super.onActivityExpired()}onError(t){const n=a(this.lunoraName,this.instanceId(),"error",t instanceof Error?t.message:String(t));return this.surfaceInStudioLogs(n),super.onError(t)}async onStart(){const t=a(this.lunoraName,this.instanceId(),"start");this.surfaceInStudioLogs(t),await super.onStart(),await this.armHardTimeout(),await this.awaitContainerReadiness()}async onHardTimeoutExpired(t){const n=await this.ctx.storage.get(d);if(t?.generation!==void 0&&t.generation!==n||this.ctx.container?.running!==!0)return;const e=a(this.lunoraName,this.instanceId(),"stop","hard timeout reached");this.surfaceInStudioLogs(e),await this.stop()}async onStop(t){const n=a(this.lunoraName,this.instanceId(),"stop",`${t.reason} (exit ${String(t.exitCode)})`);this.surfaceInStudioLogs(n),await super.onStop(t)}async armHardTimeout(){if(this.lunoraHardTimeoutSeconds===void 0)return;const t=(await this.ctx.storage.get(d)??0)+1;await this.ctx.storage.put(d,t),await this.schedule(this.lunoraHardTimeoutSeconds,"onHardTimeoutExpired",{generation:t})}async resolveSecretsStoreEnv(){const t=this.lunoraSecretsStore;t!==void 0&&(this.lunoraSecretsStoreResolved??=(async()=>{const n=this.env,e={};for(const[o,r]of Object.entries(t)){const s=n[r];if(s===void 0||typeof s.get!="function")throw new u("INTERNAL",`container "${this.lunoraName}": secretsStore env "${o}" points at binding "${r}", which is not a Secrets Store binding on the Worker env. Add a \`secrets_store_secrets\` entry binding "${r}".`);const c=await s.get();if(typeof c!="string")throw new TypeError(`container "${this.lunoraName}": Secrets Store binding "${r}" (env "${o}") did not resolve to a string value.`);e[o]=c}this.envVars={...this.envVars,...e}})().catch(n=>{throw this.lunoraSecretsStoreResolved=void 0,n}),await this.lunoraSecretsStoreResolved)}async awaitContainerReadiness(){if(this.lunoraReadyOn.length===0)return;const{container:t}=this.ctx;if(t===void 0)return;const n=Date.now()+p;await Promise.all(this.lunoraReadyOn.map(async e=>this.awaitReadinessCheck(t,e,n)))}async awaitReadinessCheck(t,n,e){const o=n.port??this.lunoraDefaultPort;if(o===void 0)throw new u("INTERNAL",`container "${this.lunoraName}": readyOn check "${n.path}" has no port — set the check's \`port\` or the container \`defaultPort\`.`);const r=n.status??200,s=n.path.startsWith("/")?n.path:`/${n.path}`,c=t.getTcpPort(o);for(;;){const f=Math.max(l,e-Date.now());try{if((await c.fetch(`http://container${s}`,{signal:AbortSignal.timeout(f)})).status===r)return}catch{}if(Date.now()>=e)throw new u("INTERNAL",`container "${this.lunoraName}": readiness check "${n.path}" (port ${String(o)}) did not return ${String(r)} within ${String(p)}ms`);await new Promise(m=>{setTimeout(m,l)})}}surfaceInStudioLogs(t){T(this.env,t,this.lunoraJurisdiction).catch(()=>{})}instanceId(){try{const{id:t}=this.ctx;return typeof t?.toString=="function"?t.toString():"unknown"}catch{return"unknown"}}}export{A as ContainerProxy,_ as LunoraContainer,L as outboundParams};
package/dist/index.d.mts CHANGED
@@ -1,15 +1,9 @@
1
- import { a as ContainerConfig, C as ContainerDefinition, b as ContainerImageSource, N as NormalizedContainerImage } from "./packem_shared/types.d-D2l2SYol.mjs";
2
- export type { B as BuildImageSource, c as ContainerInstanceType, d as ContainerRollout, e as CustomContainerInstanceType, f as NamedContainerInstanceType, R as RegistryImageSource } from "./packem_shared/types.d-D2l2SYol.mjs";
3
- /**
4
- * The `ctx.containers` action surface: typed handles over the `CONTAINER_*`
5
- * Durable Object namespace bindings the config layer reconciles.
6
- *
7
- * Deliberately structural (no `@cloudflare/containers` import): a Durable
8
- * Object namespace stub is all that is needed to route a request to a
9
- * container-enabled DO, so this module stays Node-safe and the test double
10
- * below can satisfy the exact same shape without a workerd runtime.
11
- */
12
- /** Options for explicitly starting an instance (mirrors `@cloudflare/containers`). */
1
+ import { D as DurableObjectJurisdiction, C as ContainerConfig, a as ContainerDefinition, b as ContainerImageSource, N as NormalizedContainerImage } from "./packem_shared/jurisdiction.d-8oUUvrew.mjs";
2
+ export type { B as BuildImageSource, c as ContainerInstanceType, d as ContainerReadinessCheck, e as ContainerRollout, f as CustomContainerInstanceType, g as NamedContainerInstanceType, R as RegistryImageSource } from "./packem_shared/jurisdiction.d-8oUUvrew.mjs";
3
+ /**
4
+ * Options for explicitly starting an instance (mirrors `@cloudflare/containers`).
5
+ * @experimental
6
+ */
13
7
  interface ContainerStartOptions {
14
8
  /** Override outbound internet access for this start. */
15
9
  enableInternet?: boolean;
@@ -20,108 +14,215 @@ interface ContainerStartOptions {
20
14
  /** Metadata labels attached for metrics/observability. */
21
15
  labels?: Record<string, string>;
22
16
  }
23
- /** A container instance's runtime state, as returned by `getState()`. Structural — the platform adds fields over time. */
17
+ /**
18
+ * A container instance's runtime state, as returned by `getState()`. Structural — the platform adds fields over time.
19
+ * @experimental
20
+ */
24
21
  interface ContainerInstanceState {
25
22
  [key: string]: unknown;
23
+ /** Process exit code, present once the instance has `stopped_with_code`. */
24
+ exitCode?: number;
25
+ /** Epoch-ms of the last state transition. */
26
26
  lastChange?: number;
27
+ /** Lifecycle status. Widening union — Cloudflare adds values over time. */
28
+ status?: "healthy" | "running" | "stopped" | "stopped_with_code" | "stopping";
27
29
  }
28
- /** What a handle needs from a Durable Object stub — `fetch` plus the optional lifecycle RPCs the container DO exposes. */
30
+ /** What a handle needs from a Durable Object stub — `fetch` plus the optional lifecycle/egress RPCs the container DO exposes. */
29
31
  interface ContainerStubLike {
32
+ allowHost?: (hostname: string) => Promise<void>;
33
+ denyHost?: (hostname: string) => Promise<void>;
30
34
  destroy?: () => Promise<void>;
31
35
  fetch: (input: Request) => Promise<Response>;
32
36
  getState?: () => Promise<ContainerInstanceState>;
37
+ removeAllowedHost?: (hostname: string) => Promise<void>;
38
+ removeDeniedHost?: (hostname: string) => Promise<void>;
39
+ renewActivityTimeout?: () => Promise<void>;
40
+ setAllowedHosts?: (hosts: string[]) => Promise<void>;
41
+ setDeniedHosts?: (hosts: string[]) => Promise<void>;
33
42
  start?: (options?: ContainerStartOptions) => Promise<void>;
34
43
  stop?: (signal?: number | string) => Promise<void>;
35
44
  }
36
45
  /**
37
- * Cloudflare Durable Object data-residency jurisdiction. Widening union —
38
- * Cloudflare adds values over time.
39
- * @see https://developers.cloudflare.com/durable-objects/reference/data-location/
40
- */
41
- type DurableObjectJurisdiction = "eu" | "fedramp" | "us";
42
- /** What the client needs from a Durable Object namespace binding. */
46
+ * What the client needs from a Durable Object namespace binding.
47
+ * @experimental
48
+ */
43
49
  interface ContainerNamespaceLike {
44
50
  get: (id: unknown) => ContainerStubLike;
45
51
  idFromName: (name: string) => unknown;
46
52
  /**
47
- * Derive a jurisdiction-restricted subnamespace. Optional because older
48
- * workers-types releases (and test doubles) may not expose it.
49
- */
53
+ * Derive a jurisdiction-restricted subnamespace. Optional because older
54
+ * workers-types releases (and test doubles) may not expose it.
55
+ */
50
56
  jurisdiction?: (jurisdiction: DurableObjectJurisdiction) => ContainerNamespaceLike;
51
57
  }
52
- /** A handle on one container instance (one Durable Object). */
58
+ /**
59
+ * A handle on one container instance (one Durable Object).
60
+ * @experimental
61
+ */
53
62
  interface ContainerHandle {
54
63
  /**
55
- * Send an HTTP (or WebSocket-upgrade) request to the container. A path
56
- * string (`"/transcode"`) is resolved against a synthetic origin; a full
57
- * `Request`/URL passes through unchanged.
58
- */
64
+ * Send an HTTP (or WebSocket-upgrade) request to the container. A path
65
+ * string (`"/transcode"`) is resolved against a synthetic origin; a full
66
+ * `Request`/URL passes through unchanged.
67
+ */
59
68
  fetch: (input: Request | string, init?: RequestInit) => Promise<Response>;
69
+ /**
70
+ * Return a handle that routes every request to `targetPort` on the
71
+ * container instead of the definition's `defaultPort` — for multi-port
72
+ * containers (declare the ports in `requiredPorts`). Sets the
73
+ * `cf-container-target-port` header the way `@cloudflare/containers`'
74
+ * `switchPort` does, so it composes with `.get()`, `.any()`, and `.pool()`:
75
+ * `ctx.containers.app.get("u1").port(9090).fetch("/admin")`.
76
+ */
77
+ port: (targetPort: number) => ContainerHandle;
60
78
  }
61
79
  /**
62
- * A handle on a *named* instance (from `.get(name)`) — `fetch` plus explicit
63
- * lifecycle control. The per-entity pattern (a sandbox per user, a room per
64
- * game, a job runner per id) often needs to tear down or inspect the instance
65
- * rather than wait for `sleepAfter`, so these wrap the container DO's
66
- * `start`/`stop`/`destroy`/`getState`.
67
- */
80
+ * A handle on a *named* instance (from `.get(name)`) — `fetch` plus explicit
81
+ * lifecycle control. The per-entity pattern (a sandbox per user, a room per
82
+ * game, a job runner per id) often needs to tear down or inspect the instance
83
+ * rather than wait for `sleepAfter`, so these wrap the container DO's
84
+ * `start`/`stop`/`destroy`/`getState`.
85
+ * @experimental
86
+ */
68
87
  interface ContainerInstanceHandle extends ContainerHandle {
69
88
  /** Stop and discard the instance (its ephemeral disk is lost). */
70
89
  destroy: () => Promise<void>;
90
+ /**
91
+ * Adjust this instance's egress allow/deny lists at runtime — the dynamic
92
+ * counterpart to the static `allowedHosts`/`deniedHosts` config. Useful for
93
+ * per-tenant egress policy. Requires the worker to export `ContainerProxy`
94
+ * (codegen re-exports it from the generated container file whenever any
95
+ * container is defined, so the runtime controls always work).
96
+ */
97
+ egress: ContainerEgressControls;
71
98
  /** Read the instance's current runtime state. */
72
99
  getState: () => Promise<ContainerInstanceState>;
100
+ /**
101
+ * Reset the instance's `sleepAfter` idle timer. The platform renews it on
102
+ * each proxied request, and because `@lunora/container` proxies WebSocket
103
+ * frames through the Durable Object, message traffic on an open socket
104
+ * renews it too (the WebSocket-keepalive gap of cloudflare/containers#147 is
105
+ * closed in the bundled base). This manual control is the escape hatch for
106
+ * keeping a container awake during activity that is neither an HTTP request
107
+ * nor a WS message — e.g. a long out-of-band job running inside it.
108
+ */
109
+ renewActivityTimeout: () => Promise<void>;
73
110
  /** Explicitly start the instance, optionally with per-instance env/entrypoint. */
74
111
  start: (options?: ContainerStartOptions) => Promise<void>;
75
112
  /** Stop the instance (optionally with a signal); it can start again on the next request. */
76
113
  stop: (signal?: number | string) => Promise<void>;
77
114
  }
78
- /** The per-definition accessor exposed as `ctx.containers.&lt;exportName>`. */
115
+ /**
116
+ * Runtime egress-firewall controls for a named instance (`handle.egress.*`).
117
+ * Each maps to the corresponding `@cloudflare/containers` `Container` RPC, so
118
+ * an app can tighten or relax a single instance's allowed/denied hosts after
119
+ * start without redeploying.
120
+ * @experimental
121
+ */
122
+ interface ContainerEgressControls {
123
+ /** Add one hostname (or glob) to the allow-list. */
124
+ allow: (hostname: string) => Promise<void>;
125
+ /** Add one hostname (or glob) to the deny-list. */
126
+ deny: (hostname: string) => Promise<void>;
127
+ /** Remove one hostname from the allow-list. */
128
+ removeAllowed: (hostname: string) => Promise<void>;
129
+ /** Remove one hostname from the deny-list. */
130
+ removeDenied: (hostname: string) => Promise<void>;
131
+ /** Replace the entire allow-list. */
132
+ setAllowed: (hosts: ReadonlyArray<string>) => Promise<void>;
133
+ /** Replace the entire deny-list. */
134
+ setDenied: (hosts: ReadonlyArray<string>) => Promise<void>;
135
+ }
136
+ /**
137
+ * The per-definition accessor exposed as `ctx.containers.&lt;exportName>`.
138
+ * @experimental
139
+ */
79
140
  interface ContainerAccessor {
80
141
  /**
81
- * A random instance from a fixed pool of `count` (defaults to the
82
- * definition's `maxInstances`, else 3 — mirroring `getRandom` from
83
- * `@cloudflare/containers`). For stateless, interchangeable workloads.
84
- */
85
- any: (count?: number) => ContainerHandle;
86
- /** The instance for `name` one container per entity (user, room, job…), with lifecycle control. */
87
- get: (name: string) => ContainerInstanceHandle;
142
+ * A random instance from a fixed pool of `count` (defaults to the
143
+ * definition's `maxInstances`, else 3 — mirroring `getRandom` from
144
+ * `@cloudflare/containers`). For stateless, interchangeable workloads.
145
+ *
146
+ * Like `.get()`, a path/URL-string fetch transparently retries the
147
+ * cold-start "instance is provisioning" transients (cloudflare/containers#45,
148
+ * #139); pass {@link InstanceRetryOptions} to tune or disable it.
149
+ */
150
+ any: (count?: number, options?: InstanceRetryOptions) => ContainerHandle;
88
151
  /**
89
- * A resilient handle over the pool: each `fetch` picks a random instance and,
90
- * on a thrown error or a retryable response (5xx by default), retries on a
91
- * freshly-picked instance with exponential backoff. Until Cloudflare ships
92
- * native autoscaling + health-aware routing this is the recommended way to
93
- * call a stateless container pool it rides over a single cold/unhealthy
94
- * instance instead of failing the whole request.
95
- *
96
- * Because a retry re-issues the request, pass a **replayable** body a path
97
- * string plus an `init.body` string/`ArrayBuffer` (re-created each attempt).
98
- * A pre-built `Request` carrying a stream body can only be sent once, so it
99
- * is not retry-safe here; use `.get()`/`.any()` for those.
100
- */
152
+ * The instance for `name` one container per entity (user, room, job…),
153
+ * with lifecycle control.
154
+ *
155
+ * A path/URL-string fetch transparently retries the platform's cold-start
156
+ * transients "there is no Container instance available" / "container is
157
+ * not listening" while an instance is still provisioning
158
+ * (cloudflare/containers#45, #139) — on the *same* instance with backoff,
159
+ * since the request never reached the app. Pass {@link InstanceRetryOptions}
160
+ * to tune attempts/backoff or disable it (`{ attempts: 1 }`). A pre-built
161
+ * `Request` (possibly a one-shot stream body) is sent once, never retried.
162
+ */
163
+ get: (name: string, options?: InstanceRetryOptions) => ContainerInstanceHandle;
164
+ /**
165
+ * A resilient handle over the pool: each `fetch` picks a random instance and,
166
+ * on a thrown error or a retryable response (5xx by default), retries on a
167
+ * freshly-picked instance with exponential backoff. Until Cloudflare ships
168
+ * native autoscaling + health-aware routing this is the recommended way to
169
+ * call a stateless container pool — it rides over a single cold/unhealthy
170
+ * instance instead of failing the whole request.
171
+ *
172
+ * Because a retry re-issues the request, pass a **replayable** body — a path
173
+ * string plus an `init.body` string/`ArrayBuffer` (re-created each attempt).
174
+ * A pre-built `Request` carrying a stream body can only be sent once, so it
175
+ * is not retry-safe here; use `.get()`/`.any()` for those.
176
+ */
101
177
  pool: (options?: PoolOptions) => ContainerHandle;
102
178
  }
103
- /** Tuning for a pooled, retrying container handle. See {@link ContainerAccessor.pool}. */
179
+ /**
180
+ * Tuning for a pooled, retrying container handle. See {@link ContainerAccessor.pool}.
181
+ * @experimental
182
+ */
104
183
  interface PoolOptions {
105
184
  /** Total attempts before giving up (each on a freshly-picked instance). Default 3. */
106
185
  attempts?: number;
107
186
  /** Base backoff in ms between attempts; doubles each retry (0 disables the wait). Default 100. */
108
187
  backoffMs?: number;
109
188
  /**
110
- * Upper bound on a single backoff sleep, in ms. The doubling delay is clamped
111
- * to this ceiling so a large `attempts` count can't produce an unboundedly
112
- * long wait. Default {@link DEFAULT_MAX_BACKOFF_MS} (30s).
113
- */
189
+ * Upper bound on a single backoff sleep, in ms. The doubling delay is clamped
190
+ * to this ceiling so a large `attempts` count can't produce an unboundedly
191
+ * long wait. Default {@link DEFAULT_MAX_BACKOFF_MS} (30s).
192
+ */
114
193
  maxBackoffMs?: number;
115
194
  /**
116
- * Whether a *returned* response should be retried on another instance.
117
- * Defaults to retrying any `5xx`. A thrown error (network/start failure) is
118
- * always retried regardless of this predicate.
119
- */
195
+ * Whether a *returned* response should be retried on another instance.
196
+ * Defaults to retrying any `5xx`. A thrown error (network/start failure) is
197
+ * always retried regardless of this predicate.
198
+ */
120
199
  retryOn?: (response: Response) => boolean;
121
200
  /** Pool size to spread picks across. Defaults to the definition's `maxInstances`, else 3. */
122
201
  size?: number;
123
202
  }
124
- /** Wiring info for one definition, emitted by codegen into the generated DO. */
203
+ /**
204
+ * Tuning for the cold-start retry on a `.get()`/`.any()` handle. The retry fires
205
+ * only on the platform's provisioning transients (no-instance / not-listening /
206
+ * rate-limited — see {@link isColdStartTransient}), which is why it's safe by
207
+ * default: those responses mean the request never reached the container.
208
+ * @experimental
209
+ */
210
+ interface InstanceRetryOptions {
211
+ /**
212
+ * Total attempts on a cold-start transient before the last outcome is
213
+ * surfaced as-is. `1` disables the retry. Default
214
+ * {@link DEFAULT_COLD_START_ATTEMPTS}.
215
+ */
216
+ attempts?: number;
217
+ /** Base backoff in ms between attempts; doubles each retry (0 disables the wait). Default {@link DEFAULT_COLD_START_BACKOFF_MS}. */
218
+ backoffMs?: number;
219
+ /** Upper bound on a single backoff sleep, in ms. Default {@link DEFAULT_MAX_BACKOFF_MS} (30s). */
220
+ maxBackoffMs?: number;
221
+ }
222
+ /**
223
+ * Wiring info for one definition, emitted by codegen into the generated DO.
224
+ * @experimental
225
+ */
125
226
  interface ContainerBindingSpec {
126
227
  /** Durable Object binding name, e.g. `CONTAINER_TRANSCODER`. */
127
228
  binding: string;
@@ -131,70 +232,90 @@ interface ContainerBindingSpec {
131
232
  maxInstances?: number;
132
233
  }
133
234
  /**
134
- * Build the `ctx.containers` record from the Worker `env`. Called by the
135
- * generated ShardDO with the specs codegen derived from
136
- * `lunora/containers.ts`. A missing binding doesn't throw here — only when the
137
- * handle is actually used — so one unprovisioned container never breaks
138
- * unrelated functions.
139
- */
140
- declare const createContainerContext: (env: Record<string, unknown>, specs: ReadonlyArray<ContainerBindingSpec>, jurisdiction?: DurableObjectJurisdiction) => Record<string, ContainerAccessor>;
141
- /** A test handler: receives the request plus the targeted instance name. */
235
+ * Build the `ctx.containers` record from the Worker `env`. Called by the
236
+ * generated ShardDO with the specs codegen derived from `lunora/containers.ts`.
237
+ * A missing binding doesn't throw here — only when the handle is actually used —
238
+ * so one unprovisioned container never breaks unrelated functions.
239
+ *
240
+ * `traceparent` (the inbound RPC's W3C trace context, forwarded by the runtime
241
+ * and read off the request by the DO) is stamped onto every outbound container
242
+ * `fetch`, so the container's own spans stitch under the Worker's trace.
243
+ * @experimental
244
+ */
245
+ declare const createContainerContext: (env: Record<string, unknown>, specs: ReadonlyArray<ContainerBindingSpec>, jurisdiction?: DurableObjectJurisdiction, traceparent?: string) => Record<string, ContainerAccessor>;
246
+ /**
247
+ * A test handler: receives the request plus the targeted instance name.
248
+ * @experimental
249
+ */
142
250
  type ContainerTestHandler = (request: Request, instance: {
143
251
  name: string;
144
252
  }) => Promise<Response> | Response;
145
253
  /**
146
- * Docker-free test double for `ctx.containers`: each export name maps to a
147
- * fetch handler that plays the container. Mirrors the real shape exactly, so
148
- * action handlers under test can't tell the difference.
149
- *
150
- * ```ts
151
- * const containers = createContainerTestContext({
152
- * transcoder: (request) => new Response("ok"),
153
- * });
154
- * ```
155
- */
254
+ * Docker-free test double for `ctx.containers`: each export name maps to a
255
+ * fetch handler that plays the container. Mirrors the real shape exactly, so
256
+ * action handlers under test can't tell the difference.
257
+ *
258
+ * ```ts
259
+ * const containers = createContainerTestContext({
260
+ * transcoder: (request) => new Response("ok"),
261
+ * });
262
+ * ```
263
+ * @experimental
264
+ */
156
265
  declare const createContainerTestContext: (handlers: Record<string, ContainerTestHandler>) => Record<string, ContainerAccessor>;
157
266
  /**
158
- * Normalize a `ContainerImageSource` into the shape wrangler wants: a
159
- * Dockerfile path + build context for local builds, or a fully-qualified
160
- * reference for pre-built images.
161
- *
162
- * A local-path string whose basename starts with `Dockerfile` (so
163
- * `Dockerfile.dev` also counts) is used as-is with its directory as the build
164
- * context; any other path is treated as the build-context directory and the
165
- * Dockerfile is expected at `&lt;dir>/Dockerfile`.
166
- */
267
+ * Normalize a `ContainerImageSource` into the shape wrangler wants: a
268
+ * Dockerfile path + build context for local builds, or a fully-qualified
269
+ * reference for pre-built images.
270
+ *
271
+ * A local-path string whose basename starts with `Dockerfile` (so
272
+ * `Dockerfile.dev` also counts) is used as-is with its directory as the build
273
+ * context; any other path is treated as the build-context directory and the
274
+ * Dockerfile is expected at `&lt;dir>/Dockerfile`.
275
+ * @experimental
276
+ */
167
277
  declare const normalizeContainerImage: (image: ContainerImageSource) => NormalizedContainerImage;
168
278
  /**
169
- * The generated Container DO class name for a `lunora/containers.ts` export:
170
- * `transcoder` → `TranscoderContainer`. wrangler's `containers[].class_name`
171
- * and the Durable Object binding's `class_name` both reference it, so codegen
172
- * and the config layer MUST derive it identically — always via this helper.
173
- */
279
+ * The generated Container DO class name for a `lunora/containers.ts` export:
280
+ * `transcoder` → `TranscoderContainer`. wrangler's `containers[].class_name`
281
+ * and the Durable Object binding's `class_name` both reference it, so codegen
282
+ * and the config layer MUST derive it identically — always via this helper.
283
+ * @experimental
284
+ */
174
285
  declare const containerClassName: (exportName: string) => string;
175
286
  /**
176
- * The Durable Object binding name for a container export: `transcoder` →
177
- * `CONTAINER_TRANSCODER`, `imageResizer` → `CONTAINER_IMAGE_RESIZER`. The
178
- * `CONTAINER_` prefix namespaces these away from `SHARD`/`SESSION`/`SCHEDULER`
179
- * so a container export can never collide with the built-in bindings.
180
- */
287
+ * The Durable Object binding name for a container export: `transcoder` →
288
+ * `CONTAINER_TRANSCODER`, `imageResizer` → `CONTAINER_IMAGE_RESIZER`. The
289
+ * `CONTAINER_` prefix namespaces these away from `SHARD`/`SESSION`/`SCHEDULER`
290
+ * so a container export can never collide with the built-in bindings.
291
+ * @experimental
292
+ */
181
293
  declare const containerBindingName: (exportName: string) => string;
182
294
  /**
183
- * The local image tag a Railpack `{ build }` container is built and pushed
184
- * under: `transcoder` → `lunora-transcoder:build`. The config reconciler writes
185
- * it as the wrangler `containers[].image`, and `lunora deploy` builds that tag
186
- * with Railpack and `wrangler containers push`es it before deploying — so all
187
- * three derive the tag from this one helper and can never disagree.
188
- */
295
+ * The local image tag a Railpack `{ build }` container is built and pushed
296
+ * under: `transcoder` → `lunora-transcoder:build`. The config reconciler writes
297
+ * it as the wrangler `containers[].image`, and `lunora deploy` builds that tag
298
+ * with Railpack and `wrangler containers push`es it before deploying — so all
299
+ * three derive the tag from this one helper and can never disagree.
300
+ * @experimental
301
+ */
189
302
  declare const containerBuildTag: (exportName: string) => string;
303
+ /**
304
+ * `defineContainer` is part of the experimental `@lunora/container` API and may change without a major version bump.
305
+ * @experimental
306
+ */
190
307
  declare const defineContainer: (config: ContainerConfig) => ContainerDefinition;
191
- /** True when a value is a `defineContainer` result (the runtime brand check). */
308
+ /**
309
+ * True when a value is a `defineContainer` result (the runtime brand check).
310
+ * @experimental
311
+ */
192
312
  declare const isContainerDefinition: (value: unknown) => value is ContainerDefinition;
193
313
  /**
194
- * The container's full environment at instance start: the static `env` block
195
- * plus every declared secret resolved from the Worker `env`. A declared secret
196
- * missing from the Worker env fails fast — starting the container without a
197
- * credential it was promised yields far worse errors downstream.
198
- */
314
+ * The container's full environment at instance start: the static `env` block
315
+ * plus every declared secret resolved from the Worker `env`. A declared secret
316
+ * missing from the Worker env fails fast — starting the container without a
317
+ * credential it was promised yields far worse errors downstream.
318
+ * @experimental
319
+ */
199
320
  declare const resolveContainerEnvVariables: (definition: ContainerDefinition, workerEnv: Record<string, unknown>, exportName?: string) => Record<string, string>;
200
- export { type ContainerAccessor, type ContainerBindingSpec, type ContainerConfig, type ContainerDefinition, type ContainerHandle, type ContainerImageSource, type ContainerInstanceHandle, type ContainerInstanceState, type ContainerNamespaceLike, type ContainerStartOptions, type ContainerTestHandler, type DurableObjectJurisdiction, type NormalizedContainerImage, type PoolOptions, containerBindingName, containerBuildTag, containerClassName, createContainerContext, createContainerTestContext, defineContainer, isContainerDefinition, normalizeContainerImage, resolveContainerEnvVariables as resolveContainerEnvVars };
321
+ export { type ContainerAccessor, type ContainerBindingSpec, type ContainerConfig, type ContainerDefinition, type ContainerEgressControls, type ContainerHandle, type ContainerImageSource, type ContainerInstanceHandle, type ContainerInstanceState, type ContainerNamespaceLike, type ContainerStartOptions, type ContainerTestHandler, type DurableObjectJurisdiction, type InstanceRetryOptions, type NormalizedContainerImage, type PoolOptions, containerBindingName, containerBuildTag, containerClassName, createContainerContext, createContainerTestContext, defineContainer, isContainerDefinition, normalizeContainerImage, resolveContainerEnvVariables as resolveContainerEnvVars };