@lunora/container 1.0.0-alpha.4 → 1.0.0-alpha.41

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,256 +1 @@
1
- import { Container } from '@cloudflare/containers';
2
- export { ContainerProxy, outboundParams } from '@cloudflare/containers';
3
- import { resolveContainerEnvVars as resolveContainerEnvVariables, parseDurationSeconds } from '../packem_shared/containerBindingName-C7Lic2ET.mjs';
4
-
5
- const LUNORA_EVENT_SOURCE = "lunora";
6
- const buildContainerLifecycleEvent = (container, instance, event, message) => {
7
- return {
8
- container,
9
- event,
10
- instance,
11
- level: event === "error" ? "error" : "info",
12
- message,
13
- source: LUNORA_EVENT_SOURCE,
14
- ts: Date.now(),
15
- type: "container"
16
- };
17
- };
18
- const emitContainerLifecycle = (container, instance, event, message) => {
19
- const envelope = buildContainerLifecycleEvent(container, instance, event, message);
20
- const line = JSON.stringify(envelope);
21
- if (event === "error") {
22
- console.error(line);
23
- } else {
24
- console.log(line);
25
- }
26
- return envelope;
27
- };
28
-
29
- const RECORD_CONTAINER_EVENT_OP = "__lunora_admin__:recordContainerEvent";
30
- const ROOT_SHARD_NAME = "__root__";
31
- const applyJurisdiction = (namespace, jurisdiction) => {
32
- if (jurisdiction === void 0) {
33
- return namespace;
34
- }
35
- if (typeof namespace.jurisdiction !== "function") {
36
- throw new TypeError(
37
- `@lunora/container: Durable Object namespace does not support jurisdiction("${jurisdiction}") — update @cloudflare/workers-types or remove the jurisdiction option`
38
- );
39
- }
40
- return namespace.jurisdiction(jurisdiction);
41
- };
42
- const isShardNamespace = (value) => {
43
- if (value === null || typeof value !== "object") {
44
- return false;
45
- }
46
- const candidate = value;
47
- return typeof candidate.get === "function" && typeof candidate.idFromName === "function";
48
- };
49
- const resolveRootShard = (namespace, jurisdiction) => {
50
- const pinned = applyJurisdiction(namespace, jurisdiction);
51
- if (typeof pinned.getByName === "function") {
52
- return pinned.getByName(ROOT_SHARD_NAME);
53
- }
54
- return pinned.get(pinned.idFromName(ROOT_SHARD_NAME));
55
- };
56
- const reportContainerLifecycle = async (env, envelope, jurisdiction) => {
57
- try {
58
- const envRecord = env ?? {};
59
- const namespace = envRecord["SHARD"];
60
- if (!isShardNamespace(namespace)) {
61
- return;
62
- }
63
- const adminBearer = typeof envRecord["LUNORA_ADMIN_TOKEN"] === "string" ? envRecord["LUNORA_ADMIN_TOKEN"] : void 0;
64
- if (!adminBearer || adminBearer.length === 0) {
65
- return;
66
- }
67
- const request = new Request("https://shard.internal/rpc", {
68
- body: JSON.stringify({ args: { event: envelope }, functionPath: RECORD_CONTAINER_EVENT_OP }),
69
- headers: { authorization: `Bearer ${adminBearer}`, "content-type": "application/json" },
70
- method: "POST"
71
- });
72
- await resolveRootShard(namespace, jurisdiction).fetch(request);
73
- } catch {
74
- }
75
- };
76
-
77
- const READINESS_POLL_INTERVAL_MS = 500;
78
- const READINESS_TIMEOUT_MS = 3e4;
79
- const HARD_TIMEOUT_GENERATION_KEY = "__lunoraHardTimeoutGeneration";
80
- class LunoraContainer extends Container {
81
- /**
82
- * Data-residency jurisdiction the app's DOs are pinned to (codegen passes the
83
- * schema's `.jurisdiction("…")`). Used to pin the best-effort lifecycle report
84
- * to the same region as the root shard. `undefined` ⇒ un-pinned.
85
- */
86
- lunoraJurisdiction;
87
- /** The `lunora/containers.ts` export name, for lifecycle log correlation. */
88
- lunoraName;
89
- /** Default port the readiness probes target when a check omits its own `port`. */
90
- lunoraDefaultPort;
91
- /** Hard-cap lifetime in whole seconds (from the `hardTimeout` config), or `undefined`. */
92
- lunoraHardTimeoutSeconds;
93
- /** Declarative readiness probes that gate request proxying (from the `readyOn` config). */
94
- lunoraReadyOn;
95
- constructor(context, env, definition, exportName, jurisdiction) {
96
- super(context, env, {
97
- defaultPort: definition.defaultPort,
98
- entrypoint: definition.entrypoint ? [...definition.entrypoint] : void 0,
99
- envVars: resolveContainerEnvVariables(definition, env, exportName),
100
- sleepAfter: definition.sleepAfter
101
- });
102
- if (definition.enableInternet !== void 0) {
103
- this.enableInternet = definition.enableInternet;
104
- }
105
- if (definition.requiredPorts !== void 0) {
106
- this.requiredPorts = [...definition.requiredPorts];
107
- }
108
- if (definition.interceptHttps !== void 0) {
109
- this.interceptHttps = definition.interceptHttps;
110
- }
111
- if (definition.allowedHosts !== void 0) {
112
- this.allowedHosts = [...definition.allowedHosts];
113
- }
114
- if (definition.deniedHosts !== void 0) {
115
- this.deniedHosts = [...definition.deniedHosts];
116
- }
117
- if (definition.pingEndpoint !== void 0) {
118
- this.pingEndpoint = definition.pingEndpoint;
119
- }
120
- if (definition.labels !== void 0) {
121
- this.labels = { ...definition.labels };
122
- }
123
- this.lunoraName = exportName ?? "container";
124
- this.lunoraJurisdiction = jurisdiction;
125
- this.lunoraDefaultPort = definition.defaultPort;
126
- this.lunoraReadyOn = definition.readyOn ? [...definition.readyOn] : [];
127
- this.lunoraHardTimeoutSeconds = definition.hardTimeout === void 0 ? void 0 : parseDurationSeconds(definition.hardTimeout);
128
- }
129
- async onActivityExpired() {
130
- const envelope = emitContainerLifecycle(this.lunoraName, this.instanceId(), "sleep");
131
- this.surfaceInStudioLogs(envelope);
132
- await super.onActivityExpired();
133
- }
134
- onError(error) {
135
- const envelope = emitContainerLifecycle(this.lunoraName, this.instanceId(), "error", error instanceof Error ? error.message : String(error));
136
- this.surfaceInStudioLogs(envelope);
137
- return super.onError(error);
138
- }
139
- async onStart() {
140
- const envelope = emitContainerLifecycle(this.lunoraName, this.instanceId(), "start");
141
- this.surfaceInStudioLogs(envelope);
142
- await super.onStart();
143
- await this.armHardTimeout();
144
- await this.awaitContainerReadiness();
145
- }
146
- /**
147
- * Hook run when the container's `hardTimeout` elapses (dispatched by the base
148
- * scheduler via the run-generation-stamped schedule armed in
149
- * {@link onStart}). Default: stop the instance. Override to drain/checkpoint
150
- * first. A stale schedule from a previous run, or an already-stopped
151
- * instance, is ignored (upstream cloudflare/containers#85).
152
- */
153
- async onHardTimeoutExpired(payload) {
154
- const current = await this.ctx.storage.get(HARD_TIMEOUT_GENERATION_KEY);
155
- if (payload?.generation !== void 0 && payload.generation !== current) {
156
- return;
157
- }
158
- if (this.ctx.container?.running !== true) {
159
- return;
160
- }
161
- const envelope = emitContainerLifecycle(this.lunoraName, this.instanceId(), "stop", "hard timeout reached");
162
- this.surfaceInStudioLogs(envelope);
163
- await this.stop();
164
- }
165
- async onStop(parameters) {
166
- const envelope = emitContainerLifecycle(this.lunoraName, this.instanceId(), "stop", `${parameters.reason} (exit ${String(parameters.exitCode)})`);
167
- this.surfaceInStudioLogs(envelope);
168
- await super.onStop(parameters);
169
- }
170
- /**
171
- * Arm the hard-timeout kill via the base scheduler (so it integrates with
172
- * the container's own alarm machinery instead of fighting it). Bumps the run
173
- * generation and stamps the schedule with it, so {@link onHardTimeoutExpired}
174
- * can tell a fresh schedule from a stale one. No-op without a `hardTimeout`.
175
- */
176
- async armHardTimeout() {
177
- if (this.lunoraHardTimeoutSeconds === void 0) {
178
- return;
179
- }
180
- const generation = (await this.ctx.storage.get(HARD_TIMEOUT_GENERATION_KEY) ?? 0) + 1;
181
- await this.ctx.storage.put(HARD_TIMEOUT_GENERATION_KEY, generation);
182
- await this.schedule(this.lunoraHardTimeoutSeconds, "onHardTimeoutExpired", { generation });
183
- }
184
- /**
185
- * Block until every `readyOn` probe responds with its expected status, or
186
- * throw once the readiness budget is spent. Probes run in parallel and hit
187
- * the container's TCP port directly (NOT `containerFetch`, which would
188
- * recurse back into the start path). No-op without `readyOn`.
189
- */
190
- async awaitContainerReadiness() {
191
- if (this.lunoraReadyOn.length === 0) {
192
- return;
193
- }
194
- const { container } = this.ctx;
195
- if (container === void 0) {
196
- return;
197
- }
198
- const deadline = Date.now() + READINESS_TIMEOUT_MS;
199
- await Promise.all(this.lunoraReadyOn.map(async (check) => this.awaitReadinessCheck(container, check, deadline)));
200
- }
201
- /** Poll one readiness probe until it returns its expected status or the shared deadline passes. */
202
- async awaitReadinessCheck(container, check, deadline) {
203
- const port = check.port ?? this.lunoraDefaultPort;
204
- if (port === void 0) {
205
- throw new Error(
206
- `container "${this.lunoraName}": readyOn check "${check.path}" has no port — set the check's \`port\` or the container \`defaultPort\`.`
207
- );
208
- }
209
- const expectedStatus = check.status ?? 200;
210
- const path = check.path.startsWith("/") ? check.path : `/${check.path}`;
211
- const tcpPort = container.getTcpPort(port);
212
- for (; ; ) {
213
- try {
214
- const response = await tcpPort.fetch(`http://container${path}`);
215
- if (response.status === expectedStatus) {
216
- return;
217
- }
218
- } catch {
219
- }
220
- if (Date.now() >= deadline) {
221
- throw new Error(
222
- `container "${this.lunoraName}": readiness check "${check.path}" (port ${String(port)}) did not return ${String(expectedStatus)} within ${String(READINESS_TIMEOUT_MS)}ms`
223
- );
224
- }
225
- await new Promise((resolve) => {
226
- setTimeout(resolve, READINESS_POLL_INTERVAL_MS);
227
- });
228
- }
229
- }
230
- /**
231
- * Best-effort push of `envelope` into the root ShardDO's log buffer so it
232
- * also appears in the Studio Logs panel (the terminal already has it via
233
- * `emitContainerLifecycle`). Fire-and-forget and fully swallowed: a missing
234
- * `SHARD` binding, a missing admin token, or a fetch failure NEVER throws
235
- * out of a lifecycle hook — the `console` path stays the source of truth.
236
- */
237
- surfaceInStudioLogs(envelope) {
238
- reportContainerLifecycle(this.env, envelope, this.lunoraJurisdiction).catch(() => {
239
- });
240
- }
241
- /**
242
- * Per-instance correlation id: the Durable Object id, which Cloudflare also
243
- * injects into the container as `CLOUDFLARE_DURABLE_OBJECT_ID`. Read
244
- * defensively — the id shape varies and isn't worth crashing a hook over.
245
- */
246
- instanceId() {
247
- try {
248
- const { id } = this.ctx;
249
- return typeof id?.toString === "function" ? id.toString() : "unknown";
250
- } catch {
251
- return "unknown";
252
- }
253
- }
254
- }
255
-
256
- export { LunoraContainer };
1
+ import{LunoraError as u}from"@lunora/errors";import{a as f}from"../packem_shared/abort-deadline-jwbW9Ala.mjs";import{resolveContainerEnvVars as y,parseDurationSeconds as g}from"../packem_shared/containerBindingName-D01FopQt.mjs";import{a as w}from"../packem_shared/jurisdiction-DtE9s70w.mjs";import{Container as E}from"../packem_shared/ContainerProxy-fTW_pXWY.mjs";import{ContainerProxy as C,outboundParams as M}from"../packem_shared/ContainerProxy-fTW_pXWY.mjs";const N="lunora",R=(s,t,r,e)=>({container:s,event:r,instance:t,level:r==="error"?"error":"info",message:e,source:N,ts:Date.now(),type:"container"}),i=(s,t,r,e)=>{const o=R(s,t,r,e),n=JSON.stringify(o);return r==="error"?console.error(n):console.log(n),o},T="__lunora_admin__:recordContainerEvent",S="__root__",_=s=>{if(s===null||typeof s!="object")return!1;const t=s;return typeof t.get=="function"&&typeof t.idFromName=="function"},I=(s,t)=>{const r=w(s,t);return typeof r.getByName=="function"?r.getByName(S):r.get(r.idFromName(S))},O=async(s,t,r)=>{try{const e=s??{},o=e.SHARD;if(!_(o))return;const n=typeof e.LUNORA_ADMIN_TOKEN=="string"?e.LUNORA_ADMIN_TOKEN:void 0;if(!n||n.length===0)return;const a=new Request("https://shard.internal/rpc",{body:JSON.stringify({args:{event:t},functionPath:T}),headers:{authorization:`Bearer ${n}`,"content-type":"application/json"},method:"POST"});await I(o,r).fetch(a)}catch{}},m=500,v=3e4,d="__lunoraHardTimeoutGeneration";class b extends E{lunoraJurisdiction;lunoraName;lunoraDefaultPort;lunoraHardTimeoutSeconds;lunoraReadyOn;lunoraSecretsStore;lunoraSecretsStoreResolved;constructor(t,r,e,o,n){super(t,r,{defaultPort:e.defaultPort,entrypoint:e.entrypoint?[...e.entrypoint]:void 0,envVars:y(e,r,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=n,this.lunoraDefaultPort=e.defaultPort,this.lunoraReadyOn=e.readyOn?[...e.readyOn]:[],this.lunoraHardTimeoutSeconds=e.hardTimeout===void 0?void 0:g(e.hardTimeout),this.lunoraSecretsStore=e.secretsStore}async containerFetch(...t){return await this.resolveSecretsStoreEnv(),super.containerFetch(...t)}async start(...t){const[r]=t;return r?.envVars===void 0&&await this.resolveSecretsStoreEnv(),super.start(...t)}async onActivityExpired(){const t=i(this.lunoraName,this.instanceId(),"sleep");this.surfaceInStudioLogs(t),await super.onActivityExpired()}onError(t){const r=i(this.lunoraName,this.instanceId(),"error",t instanceof Error?t.message:String(t));return this.surfaceInStudioLogs(r),super.onError(t)}async onStart(){const t=i(this.lunoraName,this.instanceId(),"start");this.surfaceInStudioLogs(t),await super.onStart(),await this.armHardTimeout(),await this.awaitContainerReadiness()}async onHardTimeoutExpired(t){const r=await this.ctx.storage.get(d);if(t?.generation!==void 0&&t.generation!==r||this.ctx.container?.running!==!0)return;const e=i(this.lunoraName,this.instanceId(),"stop","hard timeout reached");this.surfaceInStudioLogs(e),await this.stop()}async onStop(t){const r=i(this.lunoraName,this.instanceId(),"stop",`${t.reason} (exit ${String(t.exitCode)})`);this.surfaceInStudioLogs(r),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 r=this.env,e={};for(const[o,n]of Object.entries(t)){const a=r[n];if(a===void 0||typeof a.get!="function")throw new u("INTERNAL",`container "${this.lunoraName}": secretsStore env "${o}" points at binding "${n}", which is not a Secrets Store binding on the Worker env. Add a \`secrets_store_secrets\` entry binding "${n}".`);const c=await a.get();if(typeof c!="string")throw new TypeError(`container "${this.lunoraName}": Secrets Store binding "${n}" (env "${o}") did not resolve to a string value.`);e[o]=c}this.envVars={...this.envVars,...e}})().catch(r=>{throw this.lunoraSecretsStoreResolved=void 0,r}),await this.lunoraSecretsStoreResolved)}async awaitContainerReadiness(){if(this.lunoraReadyOn.length===0)return;const{container:t}=this.ctx;if(t===void 0)return;const r=Date.now()+v;await Promise.all(this.lunoraReadyOn.map(async e=>this.awaitReadinessCheck(t,e,r)))}async awaitReadinessCheck(t,r,e){const o=r.port??this.lunoraDefaultPort;if(o===void 0)throw new u("INTERNAL",`container "${this.lunoraName}": readyOn check "${r.path}" has no port — set the check's \`port\` or the container \`defaultPort\`.`);const n=r.status??200,a=r.path.startsWith("/")?r.path:`/${r.path}`,c=t.getTcpPort(o);for(;;){const l=Math.max(m,e-Date.now()),h=f(void 0,l,()=>new DOMException(`readiness probe timed out after ${String(l)}ms`,"TimeoutError"));try{if((await c.fetch(`http://container${a}`,{signal:h.signal})).status===n)return}catch{}finally{h.dispose()}if(Date.now()>=e)throw new u("INTERNAL",`container "${this.lunoraName}": readiness check "${r.path}" (port ${String(o)}) did not return ${String(n)} within ${String(v)}ms`);await new Promise(p=>{setTimeout(p,m)})}}surfaceInStudioLogs(t){O(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{C as ContainerProxy,b as LunoraContainer,M as outboundParams};