@lunora/container 1.0.0-alpha.5 → 1.0.0-alpha.50

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.
@@ -1,5 +1,5 @@
1
+ import { C as ContainerDefinition, D as DurableObjectJurisdiction } from "../packem_shared/jurisdiction.d-rC2ejtvT.mjs";
1
2
  import { DurableObject, WorkerEntrypoint } from 'cloudflare:workers';
2
- import { a as ContainerDefinition } from "../packem_shared/types.d-BlNwNY44.mjs";
3
3
  /**
4
4
  * ContainerStartOptions as they come from worker types
5
5
  */
@@ -119,7 +119,7 @@ type OutboundHandler<E = Cloudflare.Env, P = unknown> = {
119
119
  type OutboundHandlerParams = Record<string, unknown>;
120
120
  type OutboundHandlerParamsOf<THandler> = THandler extends ((req: Request, env: unknown, ctx: OutboundHandlerContext<infer Params>) => Promise<Response> | Response) ? Params : never;
121
121
  declare function outboundParams<THandler extends OutboundHandler<unknown, unknown>>(_handler: THandler, params: OutboundHandlerParamsOf<THandler>): OutboundHandlerParamsOf<THandler>;
122
- type OutboundHandlers<ParamsByMethod extends OutboundHandlerParams, E = Cloudflare.Env> = { [Method in keyof ParamsByMethod]?: OutboundHandler<E, ParamsByMethod[Method]> };
122
+ type OutboundHandlers<ParamsByMethod extends OutboundHandlerParams, E = Cloudflare.Env> = { [Method in keyof ParamsByMethod]?: OutboundHandler<E, ParamsByMethod[Method]>; };
123
123
  type OutboundHandlerOverride<Params = unknown> = {
124
124
  method: string;
125
125
  } & ([Params] extends [undefined] ? {
@@ -483,44 +483,30 @@ declare class Container<Env = Cloudflare.Env> extends DurableObject<Env> {
483
483
  getSchedule<T = string>(id: string): Promise<Schedule<T> | undefined>;
484
484
  private isActivityExpired;
485
485
  }
486
- /**
487
- * Cloudflare Durable Object data-residency jurisdiction. Widening union —
488
- * Cloudflare adds values over time.
489
- * @see https://developers.cloudflare.com/durable-objects/reference/data-location/
490
- */
491
- type DurableObjectJurisdiction = "eu" | "fedramp" | "us";
492
- /**
493
- * Forward one lifecycle envelope to the root ShardDO's log buffer, best-effort.
494
- *
495
- * `env` is the Container DO's worker `env`: we read the `SHARD` namespace and
496
- * the `LUNORA_ADMIN_TOKEN` from it. Returns a promise that NEVER rejects — every
497
- * failure path (missing binding, missing token, fetch error) resolves to
498
- * `undefined` — so the caller can `void` it from a lifecycle hook safely.
499
- */
500
486
  type DurableObjectContext = ConstructorParameters<typeof Container>[0];
501
487
  /**
502
- * Base class for the generated Container DO classes. Applies a
503
- * `defineContainer` definition onto `@cloudflare/containers`' `Container`:
504
- * port, sleep timeout, internet access, and the container environment (static
505
- * `env` merged with the declared Worker secrets — a declared-but-unset secret
506
- * fails fast here rather than starting a container without its credential).
507
- *
508
- * Generated subclasses stay one line of behavior:
509
- *
510
- * ```ts
511
- * export class TranscoderContainer extends LunoraContainer {
512
- * constructor(ctx: DurableObjectState, env: Env) {
513
- * super(ctx, env, transcoder, "transcoder");
514
- * }
515
- * }
516
- * ```
517
- */
488
+ * Base class for the generated Container DO classes. Applies a
489
+ * `defineContainer` definition onto `@cloudflare/containers`' `Container`:
490
+ * port, sleep timeout, internet access, and the container environment (static
491
+ * `env` merged with the declared Worker secrets — a declared-but-unset secret
492
+ * fails fast here rather than starting a container without its credential).
493
+ *
494
+ * Generated subclasses stay one line of behavior:
495
+ *
496
+ * ```ts
497
+ * export class TranscoderContainer extends LunoraContainer {
498
+ * constructor(ctx: DurableObjectState, env: Env) {
499
+ * super(ctx, env, transcoder, "transcoder");
500
+ * }
501
+ * }
502
+ * ```
503
+ */
518
504
  declare class LunoraContainer<Env = unknown> extends Container<Env> {
519
505
  /**
520
- * Data-residency jurisdiction the app's DOs are pinned to (codegen passes the
521
- * schema's `.jurisdiction("…")`). Used to pin the best-effort lifecycle report
522
- * to the same region as the root shard. `undefined` ⇒ un-pinned.
523
- */
506
+ * Data-residency jurisdiction the app's DOs are pinned to (codegen passes the
507
+ * schema's `.jurisdiction("…")`). Used to pin the best-effort lifecycle report
508
+ * to the same region as the root shard. `undefined` ⇒ un-pinned.
509
+ */
524
510
  private readonly lunoraJurisdiction?;
525
511
  /** The `lunora/containers.ts` export name, for lifecycle log correlation. */
526
512
  private readonly lunoraName;
@@ -528,84 +514,186 @@ declare class LunoraContainer<Env = unknown> extends Container<Env> {
528
514
  private readonly lunoraDefaultPort?;
529
515
  /** Hard-cap lifetime in whole seconds (from the `hardTimeout` config), or `undefined`. */
530
516
  private readonly lunoraHardTimeoutSeconds?;
517
+ /** In-flight `readyOn` gate for the current start; cleared when it fails. See `awaitReadinessGate`. */
518
+ private lunoraReadiness?;
531
519
  /** Declarative readiness probes that gate request proxying (from the `readyOn` config). */
532
520
  private readonly lunoraReadyOn;
533
521
  /** Map of container env-var name → Worker Secrets Store binding name (from the `secretsStore` config). */
534
522
  private readonly lunoraSecretsStore?;
535
523
  /** Memoised Secrets Store resolution: run once, then merged into `envVars` before the first start. */
536
524
  private lunoraSecretsStoreResolved?;
525
+ /**
526
+ * Count of runs observed to have ENDED, bumped by the `onStop` hook. Read
527
+ * across a start: a bump means the run that start snapshotted is over,
528
+ * whatever the `running` flag said at snapshot time. See `beginStart`.
529
+ */
530
+ private lunoraStops;
537
531
  constructor(context: DurableObjectContext, env: Env, definition: ContainerDefinition, exportName?: string, jurisdiction?: DurableObjectJurisdiction);
538
532
  /**
539
- * Proxy entry for every `ctx.containers.&lt;name>` fetch. Resolves the
540
- * `secretsStore` bindings into `envVars` before delegating, so the values
541
- * are present when the base implicitly starts the container for this
542
- * request a no-op when `secretsStore` is unset.
543
- */
533
+ * Proxy entry for every `ctx.containers.<name>` fetch. The base starts the
534
+ * container for this request through {@link startAndWaitForPorts}, which is
535
+ * where the `secretsStore` resolution lives a request that finds the
536
+ * container already healthy needs no resolution at all.
537
+ */
544
538
  override containerFetch(...args: Parameters<Container<Env>["containerFetch"]>): Promise<Response>;
545
539
  /**
546
- * Explicit start (`ctx.containers.&lt;name>.get(id).start()`). Resolves the
547
- * `secretsStore` bindings into `envVars` first, mirroring
548
- * {@link containerFetch}. A per-instance `start({ envVars })` replaces the
549
- * env set wholesale (base behavior), so the injected values only apply to a
550
- * bare `start()` — same as the static `env`/`secrets`. When the caller
551
- * supplies its own `envVars` we skip resolution entirely: those values would
552
- * be discarded anyway, so a missing/unreadable binding shouldn't fail a start
553
- * that never uses them.
554
- */
540
+ * The start path `containerFetch` takes (and the one an app can call itself).
541
+ * Resolves the `secretsStore` bindings into `envVars` first — `doStartContainer`
542
+ * reads `this.envVars`, so a container started this way would otherwise boot
543
+ * without its Secrets Store values.
544
+ *
545
+ * The base's last act is `blockConcurrencyWhile(… onStart())`, so this
546
+ * override resumes on the far side of that gate which is where
547
+ * {@link afterContainerStart} has to run. See its docblock.
548
+ */
549
+ override startAndWaitForPorts(...args: Parameters<Container<Env>["startAndWaitForPorts"]>): Promise<void>;
550
+ /**
551
+ * Explicit start (`ctx.containers.<name>.get(id).start()`). Resolves the
552
+ * `secretsStore` bindings into `envVars` first, mirroring
553
+ * {@link containerFetch}. A per-instance `start({ envVars })` replaces the
554
+ * env set wholesale (base behavior), so the injected values only apply to a
555
+ * bare `start()` — same as the static `env`/`secrets`. When the caller
556
+ * supplies its own `envVars` we skip resolution entirely: those values would
557
+ * be discarded anyway, so a missing/unreadable binding shouldn't fail a start
558
+ * that never uses them.
559
+ */
555
560
  override start(...args: Parameters<Container<Env>["start"]>): Promise<void>;
556
561
  override onActivityExpired(): Promise<void>;
557
562
  override onError(error: unknown): unknown;
558
563
  override onStart(): Promise<void>;
559
564
  /**
560
- * Hook run when the container's `hardTimeout` elapses (dispatched by the base
561
- * scheduler via the run-generation-stamped schedule armed in
562
- * {@link onStart}). Default: stop the instance. Override to drain/checkpoint
563
- * first. A stale schedule from a previous run, or an already-stopped
564
- * instance, is ignored (upstream cloudflare/containers#85).
565
- */
565
+ * Hook run when the container's `hardTimeout` elapses (dispatched by the base
566
+ * scheduler via the run-generation-stamped schedule armed in
567
+ * {@link onStart}). Default: stop the instance. Override to drain/checkpoint
568
+ * first. A stale schedule from a previous run, or an already-stopped
569
+ * instance, is ignored (upstream cloudflare/containers#85).
570
+ *
571
+ * `stop()` sends SIGTERM and returns; nothing escalates to `destroy()`. The
572
+ * cap is therefore a signal, not a bound — a container that traps or ignores
573
+ * SIGTERM keeps running, and the schedule is ONE-SHOT (the base deletes the
574
+ * row once it fires), so nothing signals it a second time either: an ignored
575
+ * SIGTERM means the cap is spent, not retried. Override this hook and follow
576
+ * the stop with a `destroy()` after a grace period if your workload needs a
577
+ * real ceiling.
578
+ */
566
579
  onHardTimeoutExpired(payload?: {
567
580
  generation?: number;
568
581
  }): Promise<void>;
569
582
  override onStop(parameters: StopParams): Promise<void>;
570
583
  /**
571
- * Arm the hard-timeout kill via the base scheduler (so it integrates with
572
- * the container's own alarm machinery instead of fighting it). Bumps the run
573
- * generation and stamps the schedule with it, so {@link onHardTimeoutExpired}
574
- * can tell a fresh schedule from a stale one. No-op without a `hardTimeout`.
575
- */
584
+ * Arm the hard timeout and block on the `readyOn` probes the work that has
585
+ * to happen once per real start, **outside** the base's start gate.
586
+ *
587
+ * It cannot live in `onStart`, which is the obvious home for it: the base
588
+ * invokes that hook as `blockConcurrencyWhile(async () => { … onStart() })`
589
+ * (`@cloudflare/containers`, both `start()` and `startAndWaitForPorts()`),
590
+ * and workerd treats a *rejecting* `blockConcurrencyWhile` closure as
591
+ * unrecoverable — it aborts the Durable Object, discards its in-memory state
592
+ * and every hibernating socket on it, and flattens the error to a plain
593
+ * `Error`. A readiness timeout is an ordinary, diagnosable failure: it must
594
+ * surface as the `LunoraError` naming the check, the port and the budget,
595
+ * not cost the object its life and arrive as an opaque message. (The same
596
+ * reasoning, and the same settle-outside-the-gate remedy, is written up on
597
+ * `ShardHost.runSerialized` in `@lunora/platform-cloudflare`.) A 30-second
598
+ * wait also has no business inside a gate that blocks every other dispatch
599
+ * to the object.
600
+ *
601
+ * Both start entry points call this immediately after `super`, so it runs
602
+ * once per start. It does NOT gate proxying on its own: the base commits the
603
+ * healthy state inside the start gate, before this runs, so a concurrent
604
+ * request would sail past `containerFetch`'s status check. That is what
605
+ * `awaitReadinessGate` is for, and it is the seam the move cost us —
606
+ * the in-gate placement got this for free from `blockConcurrencyWhile`.
607
+ */
608
+ private afterContainerStart;
609
+ /**
610
+ * Decide — synchronously, before anything is started — whether this start
611
+ * begins a NEW container run, and drop the previous run's readiness gate when
612
+ * it does. Returns whether the container was already running.
613
+ *
614
+ * The gate has to be keyed on the run, and `onStop` alone cannot key it:
615
+ * `@cloudflare/containers` reaches `onStop` only through
616
+ * `syncPendingStoppedEvents`, which `start()` never calls (only
617
+ * `startAndWaitForPorts`, `stop()` and the alarm loop do), while the monitor
618
+ * callback that observes a container exit merely records the state. So a
619
+ * `start()` in the up-to-three-minute window before the next alarm found the
620
+ * finished run's settled gate and skipped BOTH `armHardTimeout` and the
621
+ * `readyOn` probes — run 2 ran uncapped and was proxied to before it reported
622
+ * ready. A `hardTimeout` firing its own SIGTERM lands in exactly that window.
623
+ *
624
+ * The mirror case is why the answer is not "always re-arm": a start that finds
625
+ * the container already up begins no run, and re-arming there moves the cap
626
+ * (see {@link afterContainerStart}).
627
+ *
628
+ * Read with no `await` between it and `super`, so two concurrent starts of a
629
+ * stopped container both observe `false` and the second joins the first's
630
+ * gate instead of arming a second schedule — and so the flag is as fresh as
631
+ * it can be. It is a snapshot either way, and the run can end after it: the
632
+ * base's own pre-start work (`getPortsToCheck`, `syncPendingStoppedEvents`)
633
+ * still runs on the far side of it, and a start that came in on a live
634
+ * container would then arm nothing for the run the base goes on to start.
635
+ * Hence the second half of the answer, in the callers: an `onStop` observed
636
+ * ACROSS the base call ({@link lunoraStops}) demotes the snapshot, because
637
+ * the run it described is over. What that does not cover is an exit inside
638
+ * `start()`'s own base call — that path never syncs pending stop events, so
639
+ * nothing reports the exit until the next alarm or `startAndWaitForPorts`,
640
+ * and the cap for such a run is armed only when one of those arrives.
641
+ */
642
+ private beginStart;
643
+ /**
644
+ * Block until the `readyOn` probes for the current start have passed.
645
+ *
646
+ * Necessary because the base marks the container healthy *inside* the start
647
+ * gate — `startAndWaitForPorts` runs `setHealthy()` immediately before
648
+ * `onStart()` — while our probes run after it returns. `containerFetch`
649
+ * skips the start path entirely once it observes
650
+ * `container.running && status === "healthy"`, so without this a request
651
+ * arriving mid-probe would proxy to a container that never reported ready,
652
+ * and a request arriving after a *failed* probe would do so permanently.
653
+ * Holding `setHealthy` and the probes together the way the base does would
654
+ * mean putting the probes back inside the gate, which is the defect this
655
+ * whole path exists to avoid.
656
+ */
657
+ private awaitReadinessGate;
658
+ /**
659
+ * Arm the hard-timeout kill via the base scheduler (so it integrates with
660
+ * the container's own alarm machinery instead of fighting it). Bumps the run
661
+ * generation and stamps the schedule with it, so {@link onHardTimeoutExpired}
662
+ * can tell a fresh schedule from a stale one. No-op without a `hardTimeout`.
663
+ */
576
664
  private armHardTimeout;
577
665
  /**
578
- * Resolve the `secretsStore` bindings (async `.get()`) once and merge the
579
- * values into `envVars`, so they're present when the base starts the
580
- * container. Memoised on the first call — every later start reuses the
581
- * resolved promise. A missing binding or a non-string value fails fast (the
582
- * start surfaces the error), the same fail-closed stance the static
583
- * `secrets` resolution takes for a missing Worker secret. No-op without
584
- * `secretsStore`.
585
- */
666
+ * Resolve the `secretsStore` bindings (async `.get()`) once and merge the
667
+ * values into `envVars`, so they're present when the base starts the
668
+ * container. Memoised on the first call — every later start reuses the
669
+ * resolved promise. A missing binding or a non-string value fails fast (the
670
+ * start surfaces the error), the same fail-closed stance the static
671
+ * `secrets` resolution takes for a missing Worker secret. No-op without
672
+ * `secretsStore`.
673
+ */
586
674
  private resolveSecretsStoreEnv;
587
675
  /**
588
- * Block until every `readyOn` probe responds with its expected status, or
589
- * throw once the readiness budget is spent. Probes run in parallel and hit
590
- * the container's TCP port directly (NOT `containerFetch`, which would
591
- * recurse back into the start path). No-op without `readyOn`.
592
- */
676
+ * Block until every `readyOn` probe responds with its expected status, or
677
+ * throw once the readiness budget is spent. Probes run in parallel and hit
678
+ * the container's TCP port directly (NOT `containerFetch`, which would
679
+ * recurse back into the start path). No-op without `readyOn`.
680
+ */
593
681
  private awaitContainerReadiness;
594
682
  /** Poll one readiness probe until it returns its expected status or the shared deadline passes. */
595
683
  private awaitReadinessCheck;
596
684
  /**
597
- * Best-effort push of `envelope` into the root ShardDO's log buffer so it
598
- * also appears in the Studio Logs panel (the terminal already has it via
599
- * `emitContainerLifecycle`). Fire-and-forget and fully swallowed: a missing
600
- * `SHARD` binding, a missing admin token, or a fetch failure NEVER throws
601
- * out of a lifecycle hook — the `console` path stays the source of truth.
602
- */
685
+ * Best-effort push of `envelope` into the root ShardDO's log buffer so it
686
+ * also appears in the Studio Logs panel (the terminal already has it via
687
+ * `emitContainerLifecycle`). Fire-and-forget and fully swallowed: a missing
688
+ * `SHARD` binding, a missing admin token, or a fetch failure NEVER throws
689
+ * out of a lifecycle hook — the `console` path stays the source of truth.
690
+ */
603
691
  private surfaceInStudioLogs;
604
692
  /**
605
- * Per-instance correlation id: the Durable Object id, which Cloudflare also
606
- * injects into the container as `CLOUDFLARE_DURABLE_OBJECT_ID`. Read
607
- * defensively — the id shape varies and isn't worth crashing a hook over.
608
- */
693
+ * Per-instance correlation id: the Durable Object id, which Cloudflare also
694
+ * injects into the container as `CLOUDFLARE_DURABLE_OBJECT_ID`. Read
695
+ * defensively — the id shape varies and isn't worth crashing a hook over.
696
+ */
609
697
  private instanceId;
610
698
  }
611
699
  export { ContainerProxy, LunoraContainer, type OutboundHandler, type OutboundHandlerContext, type OutboundHandlerParams, type OutboundHandlerParamsOf, type OutboundHandlers, outboundParams };
@@ -1,5 +1,5 @@
1
+ import { C as ContainerDefinition, D as DurableObjectJurisdiction } from "../packem_shared/jurisdiction.d-rC2ejtvT.js";
1
2
  import { DurableObject, WorkerEntrypoint } from 'cloudflare:workers';
2
- import { a as ContainerDefinition } from "../packem_shared/types.d-BlNwNY44.js";
3
3
  /**
4
4
  * ContainerStartOptions as they come from worker types
5
5
  */
@@ -119,7 +119,7 @@ type OutboundHandler<E = Cloudflare.Env, P = unknown> = {
119
119
  type OutboundHandlerParams = Record<string, unknown>;
120
120
  type OutboundHandlerParamsOf<THandler> = THandler extends ((req: Request, env: unknown, ctx: OutboundHandlerContext<infer Params>) => Promise<Response> | Response) ? Params : never;
121
121
  declare function outboundParams<THandler extends OutboundHandler<unknown, unknown>>(_handler: THandler, params: OutboundHandlerParamsOf<THandler>): OutboundHandlerParamsOf<THandler>;
122
- type OutboundHandlers<ParamsByMethod extends OutboundHandlerParams, E = Cloudflare.Env> = { [Method in keyof ParamsByMethod]?: OutboundHandler<E, ParamsByMethod[Method]> };
122
+ type OutboundHandlers<ParamsByMethod extends OutboundHandlerParams, E = Cloudflare.Env> = { [Method in keyof ParamsByMethod]?: OutboundHandler<E, ParamsByMethod[Method]>; };
123
123
  type OutboundHandlerOverride<Params = unknown> = {
124
124
  method: string;
125
125
  } & ([Params] extends [undefined] ? {
@@ -483,44 +483,30 @@ declare class Container<Env = Cloudflare.Env> extends DurableObject<Env> {
483
483
  getSchedule<T = string>(id: string): Promise<Schedule<T> | undefined>;
484
484
  private isActivityExpired;
485
485
  }
486
- /**
487
- * Cloudflare Durable Object data-residency jurisdiction. Widening union —
488
- * Cloudflare adds values over time.
489
- * @see https://developers.cloudflare.com/durable-objects/reference/data-location/
490
- */
491
- type DurableObjectJurisdiction = "eu" | "fedramp" | "us";
492
- /**
493
- * Forward one lifecycle envelope to the root ShardDO's log buffer, best-effort.
494
- *
495
- * `env` is the Container DO's worker `env`: we read the `SHARD` namespace and
496
- * the `LUNORA_ADMIN_TOKEN` from it. Returns a promise that NEVER rejects — every
497
- * failure path (missing binding, missing token, fetch error) resolves to
498
- * `undefined` — so the caller can `void` it from a lifecycle hook safely.
499
- */
500
486
  type DurableObjectContext = ConstructorParameters<typeof Container>[0];
501
487
  /**
502
- * Base class for the generated Container DO classes. Applies a
503
- * `defineContainer` definition onto `@cloudflare/containers`' `Container`:
504
- * port, sleep timeout, internet access, and the container environment (static
505
- * `env` merged with the declared Worker secrets — a declared-but-unset secret
506
- * fails fast here rather than starting a container without its credential).
507
- *
508
- * Generated subclasses stay one line of behavior:
509
- *
510
- * ```ts
511
- * export class TranscoderContainer extends LunoraContainer {
512
- * constructor(ctx: DurableObjectState, env: Env) {
513
- * super(ctx, env, transcoder, "transcoder");
514
- * }
515
- * }
516
- * ```
517
- */
488
+ * Base class for the generated Container DO classes. Applies a
489
+ * `defineContainer` definition onto `@cloudflare/containers`' `Container`:
490
+ * port, sleep timeout, internet access, and the container environment (static
491
+ * `env` merged with the declared Worker secrets — a declared-but-unset secret
492
+ * fails fast here rather than starting a container without its credential).
493
+ *
494
+ * Generated subclasses stay one line of behavior:
495
+ *
496
+ * ```ts
497
+ * export class TranscoderContainer extends LunoraContainer {
498
+ * constructor(ctx: DurableObjectState, env: Env) {
499
+ * super(ctx, env, transcoder, "transcoder");
500
+ * }
501
+ * }
502
+ * ```
503
+ */
518
504
  declare class LunoraContainer<Env = unknown> extends Container<Env> {
519
505
  /**
520
- * Data-residency jurisdiction the app's DOs are pinned to (codegen passes the
521
- * schema's `.jurisdiction("…")`). Used to pin the best-effort lifecycle report
522
- * to the same region as the root shard. `undefined` ⇒ un-pinned.
523
- */
506
+ * Data-residency jurisdiction the app's DOs are pinned to (codegen passes the
507
+ * schema's `.jurisdiction("…")`). Used to pin the best-effort lifecycle report
508
+ * to the same region as the root shard. `undefined` ⇒ un-pinned.
509
+ */
524
510
  private readonly lunoraJurisdiction?;
525
511
  /** The `lunora/containers.ts` export name, for lifecycle log correlation. */
526
512
  private readonly lunoraName;
@@ -528,84 +514,186 @@ declare class LunoraContainer<Env = unknown> extends Container<Env> {
528
514
  private readonly lunoraDefaultPort?;
529
515
  /** Hard-cap lifetime in whole seconds (from the `hardTimeout` config), or `undefined`. */
530
516
  private readonly lunoraHardTimeoutSeconds?;
517
+ /** In-flight `readyOn` gate for the current start; cleared when it fails. See `awaitReadinessGate`. */
518
+ private lunoraReadiness?;
531
519
  /** Declarative readiness probes that gate request proxying (from the `readyOn` config). */
532
520
  private readonly lunoraReadyOn;
533
521
  /** Map of container env-var name → Worker Secrets Store binding name (from the `secretsStore` config). */
534
522
  private readonly lunoraSecretsStore?;
535
523
  /** Memoised Secrets Store resolution: run once, then merged into `envVars` before the first start. */
536
524
  private lunoraSecretsStoreResolved?;
525
+ /**
526
+ * Count of runs observed to have ENDED, bumped by the `onStop` hook. Read
527
+ * across a start: a bump means the run that start snapshotted is over,
528
+ * whatever the `running` flag said at snapshot time. See `beginStart`.
529
+ */
530
+ private lunoraStops;
537
531
  constructor(context: DurableObjectContext, env: Env, definition: ContainerDefinition, exportName?: string, jurisdiction?: DurableObjectJurisdiction);
538
532
  /**
539
- * Proxy entry for every `ctx.containers.&lt;name>` fetch. Resolves the
540
- * `secretsStore` bindings into `envVars` before delegating, so the values
541
- * are present when the base implicitly starts the container for this
542
- * request a no-op when `secretsStore` is unset.
543
- */
533
+ * Proxy entry for every `ctx.containers.<name>` fetch. The base starts the
534
+ * container for this request through {@link startAndWaitForPorts}, which is
535
+ * where the `secretsStore` resolution lives a request that finds the
536
+ * container already healthy needs no resolution at all.
537
+ */
544
538
  override containerFetch(...args: Parameters<Container<Env>["containerFetch"]>): Promise<Response>;
545
539
  /**
546
- * Explicit start (`ctx.containers.&lt;name>.get(id).start()`). Resolves the
547
- * `secretsStore` bindings into `envVars` first, mirroring
548
- * {@link containerFetch}. A per-instance `start({ envVars })` replaces the
549
- * env set wholesale (base behavior), so the injected values only apply to a
550
- * bare `start()` — same as the static `env`/`secrets`. When the caller
551
- * supplies its own `envVars` we skip resolution entirely: those values would
552
- * be discarded anyway, so a missing/unreadable binding shouldn't fail a start
553
- * that never uses them.
554
- */
540
+ * The start path `containerFetch` takes (and the one an app can call itself).
541
+ * Resolves the `secretsStore` bindings into `envVars` first — `doStartContainer`
542
+ * reads `this.envVars`, so a container started this way would otherwise boot
543
+ * without its Secrets Store values.
544
+ *
545
+ * The base's last act is `blockConcurrencyWhile(… onStart())`, so this
546
+ * override resumes on the far side of that gate which is where
547
+ * {@link afterContainerStart} has to run. See its docblock.
548
+ */
549
+ override startAndWaitForPorts(...args: Parameters<Container<Env>["startAndWaitForPorts"]>): Promise<void>;
550
+ /**
551
+ * Explicit start (`ctx.containers.<name>.get(id).start()`). Resolves the
552
+ * `secretsStore` bindings into `envVars` first, mirroring
553
+ * {@link containerFetch}. A per-instance `start({ envVars })` replaces the
554
+ * env set wholesale (base behavior), so the injected values only apply to a
555
+ * bare `start()` — same as the static `env`/`secrets`. When the caller
556
+ * supplies its own `envVars` we skip resolution entirely: those values would
557
+ * be discarded anyway, so a missing/unreadable binding shouldn't fail a start
558
+ * that never uses them.
559
+ */
555
560
  override start(...args: Parameters<Container<Env>["start"]>): Promise<void>;
556
561
  override onActivityExpired(): Promise<void>;
557
562
  override onError(error: unknown): unknown;
558
563
  override onStart(): Promise<void>;
559
564
  /**
560
- * Hook run when the container's `hardTimeout` elapses (dispatched by the base
561
- * scheduler via the run-generation-stamped schedule armed in
562
- * {@link onStart}). Default: stop the instance. Override to drain/checkpoint
563
- * first. A stale schedule from a previous run, or an already-stopped
564
- * instance, is ignored (upstream cloudflare/containers#85).
565
- */
565
+ * Hook run when the container's `hardTimeout` elapses (dispatched by the base
566
+ * scheduler via the run-generation-stamped schedule armed in
567
+ * {@link onStart}). Default: stop the instance. Override to drain/checkpoint
568
+ * first. A stale schedule from a previous run, or an already-stopped
569
+ * instance, is ignored (upstream cloudflare/containers#85).
570
+ *
571
+ * `stop()` sends SIGTERM and returns; nothing escalates to `destroy()`. The
572
+ * cap is therefore a signal, not a bound — a container that traps or ignores
573
+ * SIGTERM keeps running, and the schedule is ONE-SHOT (the base deletes the
574
+ * row once it fires), so nothing signals it a second time either: an ignored
575
+ * SIGTERM means the cap is spent, not retried. Override this hook and follow
576
+ * the stop with a `destroy()` after a grace period if your workload needs a
577
+ * real ceiling.
578
+ */
566
579
  onHardTimeoutExpired(payload?: {
567
580
  generation?: number;
568
581
  }): Promise<void>;
569
582
  override onStop(parameters: StopParams): Promise<void>;
570
583
  /**
571
- * Arm the hard-timeout kill via the base scheduler (so it integrates with
572
- * the container's own alarm machinery instead of fighting it). Bumps the run
573
- * generation and stamps the schedule with it, so {@link onHardTimeoutExpired}
574
- * can tell a fresh schedule from a stale one. No-op without a `hardTimeout`.
575
- */
584
+ * Arm the hard timeout and block on the `readyOn` probes the work that has
585
+ * to happen once per real start, **outside** the base's start gate.
586
+ *
587
+ * It cannot live in `onStart`, which is the obvious home for it: the base
588
+ * invokes that hook as `blockConcurrencyWhile(async () => { … onStart() })`
589
+ * (`@cloudflare/containers`, both `start()` and `startAndWaitForPorts()`),
590
+ * and workerd treats a *rejecting* `blockConcurrencyWhile` closure as
591
+ * unrecoverable — it aborts the Durable Object, discards its in-memory state
592
+ * and every hibernating socket on it, and flattens the error to a plain
593
+ * `Error`. A readiness timeout is an ordinary, diagnosable failure: it must
594
+ * surface as the `LunoraError` naming the check, the port and the budget,
595
+ * not cost the object its life and arrive as an opaque message. (The same
596
+ * reasoning, and the same settle-outside-the-gate remedy, is written up on
597
+ * `ShardHost.runSerialized` in `@lunora/platform-cloudflare`.) A 30-second
598
+ * wait also has no business inside a gate that blocks every other dispatch
599
+ * to the object.
600
+ *
601
+ * Both start entry points call this immediately after `super`, so it runs
602
+ * once per start. It does NOT gate proxying on its own: the base commits the
603
+ * healthy state inside the start gate, before this runs, so a concurrent
604
+ * request would sail past `containerFetch`'s status check. That is what
605
+ * `awaitReadinessGate` is for, and it is the seam the move cost us —
606
+ * the in-gate placement got this for free from `blockConcurrencyWhile`.
607
+ */
608
+ private afterContainerStart;
609
+ /**
610
+ * Decide — synchronously, before anything is started — whether this start
611
+ * begins a NEW container run, and drop the previous run's readiness gate when
612
+ * it does. Returns whether the container was already running.
613
+ *
614
+ * The gate has to be keyed on the run, and `onStop` alone cannot key it:
615
+ * `@cloudflare/containers` reaches `onStop` only through
616
+ * `syncPendingStoppedEvents`, which `start()` never calls (only
617
+ * `startAndWaitForPorts`, `stop()` and the alarm loop do), while the monitor
618
+ * callback that observes a container exit merely records the state. So a
619
+ * `start()` in the up-to-three-minute window before the next alarm found the
620
+ * finished run's settled gate and skipped BOTH `armHardTimeout` and the
621
+ * `readyOn` probes — run 2 ran uncapped and was proxied to before it reported
622
+ * ready. A `hardTimeout` firing its own SIGTERM lands in exactly that window.
623
+ *
624
+ * The mirror case is why the answer is not "always re-arm": a start that finds
625
+ * the container already up begins no run, and re-arming there moves the cap
626
+ * (see {@link afterContainerStart}).
627
+ *
628
+ * Read with no `await` between it and `super`, so two concurrent starts of a
629
+ * stopped container both observe `false` and the second joins the first's
630
+ * gate instead of arming a second schedule — and so the flag is as fresh as
631
+ * it can be. It is a snapshot either way, and the run can end after it: the
632
+ * base's own pre-start work (`getPortsToCheck`, `syncPendingStoppedEvents`)
633
+ * still runs on the far side of it, and a start that came in on a live
634
+ * container would then arm nothing for the run the base goes on to start.
635
+ * Hence the second half of the answer, in the callers: an `onStop` observed
636
+ * ACROSS the base call ({@link lunoraStops}) demotes the snapshot, because
637
+ * the run it described is over. What that does not cover is an exit inside
638
+ * `start()`'s own base call — that path never syncs pending stop events, so
639
+ * nothing reports the exit until the next alarm or `startAndWaitForPorts`,
640
+ * and the cap for such a run is armed only when one of those arrives.
641
+ */
642
+ private beginStart;
643
+ /**
644
+ * Block until the `readyOn` probes for the current start have passed.
645
+ *
646
+ * Necessary because the base marks the container healthy *inside* the start
647
+ * gate — `startAndWaitForPorts` runs `setHealthy()` immediately before
648
+ * `onStart()` — while our probes run after it returns. `containerFetch`
649
+ * skips the start path entirely once it observes
650
+ * `container.running && status === "healthy"`, so without this a request
651
+ * arriving mid-probe would proxy to a container that never reported ready,
652
+ * and a request arriving after a *failed* probe would do so permanently.
653
+ * Holding `setHealthy` and the probes together the way the base does would
654
+ * mean putting the probes back inside the gate, which is the defect this
655
+ * whole path exists to avoid.
656
+ */
657
+ private awaitReadinessGate;
658
+ /**
659
+ * Arm the hard-timeout kill via the base scheduler (so it integrates with
660
+ * the container's own alarm machinery instead of fighting it). Bumps the run
661
+ * generation and stamps the schedule with it, so {@link onHardTimeoutExpired}
662
+ * can tell a fresh schedule from a stale one. No-op without a `hardTimeout`.
663
+ */
576
664
  private armHardTimeout;
577
665
  /**
578
- * Resolve the `secretsStore` bindings (async `.get()`) once and merge the
579
- * values into `envVars`, so they're present when the base starts the
580
- * container. Memoised on the first call — every later start reuses the
581
- * resolved promise. A missing binding or a non-string value fails fast (the
582
- * start surfaces the error), the same fail-closed stance the static
583
- * `secrets` resolution takes for a missing Worker secret. No-op without
584
- * `secretsStore`.
585
- */
666
+ * Resolve the `secretsStore` bindings (async `.get()`) once and merge the
667
+ * values into `envVars`, so they're present when the base starts the
668
+ * container. Memoised on the first call — every later start reuses the
669
+ * resolved promise. A missing binding or a non-string value fails fast (the
670
+ * start surfaces the error), the same fail-closed stance the static
671
+ * `secrets` resolution takes for a missing Worker secret. No-op without
672
+ * `secretsStore`.
673
+ */
586
674
  private resolveSecretsStoreEnv;
587
675
  /**
588
- * Block until every `readyOn` probe responds with its expected status, or
589
- * throw once the readiness budget is spent. Probes run in parallel and hit
590
- * the container's TCP port directly (NOT `containerFetch`, which would
591
- * recurse back into the start path). No-op without `readyOn`.
592
- */
676
+ * Block until every `readyOn` probe responds with its expected status, or
677
+ * throw once the readiness budget is spent. Probes run in parallel and hit
678
+ * the container's TCP port directly (NOT `containerFetch`, which would
679
+ * recurse back into the start path). No-op without `readyOn`.
680
+ */
593
681
  private awaitContainerReadiness;
594
682
  /** Poll one readiness probe until it returns its expected status or the shared deadline passes. */
595
683
  private awaitReadinessCheck;
596
684
  /**
597
- * Best-effort push of `envelope` into the root ShardDO's log buffer so it
598
- * also appears in the Studio Logs panel (the terminal already has it via
599
- * `emitContainerLifecycle`). Fire-and-forget and fully swallowed: a missing
600
- * `SHARD` binding, a missing admin token, or a fetch failure NEVER throws
601
- * out of a lifecycle hook — the `console` path stays the source of truth.
602
- */
685
+ * Best-effort push of `envelope` into the root ShardDO's log buffer so it
686
+ * also appears in the Studio Logs panel (the terminal already has it via
687
+ * `emitContainerLifecycle`). Fire-and-forget and fully swallowed: a missing
688
+ * `SHARD` binding, a missing admin token, or a fetch failure NEVER throws
689
+ * out of a lifecycle hook — the `console` path stays the source of truth.
690
+ */
603
691
  private surfaceInStudioLogs;
604
692
  /**
605
- * Per-instance correlation id: the Durable Object id, which Cloudflare also
606
- * injects into the container as `CLOUDFLARE_DURABLE_OBJECT_ID`. Read
607
- * defensively — the id shape varies and isn't worth crashing a hook over.
608
- */
693
+ * Per-instance correlation id: the Durable Object id, which Cloudflare also
694
+ * injects into the container as `CLOUDFLARE_DURABLE_OBJECT_ID`. Read
695
+ * defensively — the id shape varies and isn't worth crashing a hook over.
696
+ */
609
697
  private instanceId;
610
698
  }
611
699
  export { ContainerProxy, LunoraContainer, type OutboundHandler, type OutboundHandlerContext, type OutboundHandlerParams, type OutboundHandlerParamsOf, type OutboundHandlers, outboundParams };