/** * `createHibernatingIsolatePool` — a keyed pool of isolate+context pairs that * can hibernate when idle and wake transparently on the next call. * * Built on top of {@link createIsolate} and the data-checkpoint primitives * shipped in 0.8.19+ (`context.checkpoint()` / * `isolate.createContext({ checkpoint })`). This is NOT a JavaScriptCore heap * pause/resume image — `SNAPSHOT_RESEARCH.md` says the public JSC C API * doesn't expose that primitive. Hibernation here is "serialize the data half, * drop the isolate, restore on next call." The seed code half is supplied by * the caller's `fn` (the same way the existing pool's `run(fn)` builds its * context). * * Compared to {@link createIsolatePool}: * * - The shared resource here is a `Context`, not an `Isolate`. The pool * manages context lifecycle so it can checkpoint + restore in one * atomic operation. * - When an active entry's idle clock exceeds `hibernateAfterMs`, the * entry is hibernated: the context is checkpointed via * `context.checkpoint()`, the checkpoint is stored, and the isolate is * disposed. The entry slot stays in the pool so subsequent runs find * it and wake. * - When the next `run(key, fn)` lands on a hibernated entry, the pool * creates a fresh isolate, calls * `isolate.createContext({ checkpoint })` to seed it with the * hibernated data, then runs `fn(context)`. * * This is the SB-7 substrate for the eventual {@link SB-6} hosted Cloud: * far more "tenant logical contexts" than physical isolates, because the * warm ones get serialized down to bytes when no one's calling them. * * @example * ```ts * const pool = createHibernatingIsolatePool({ * isolate: { backend: 'worker' }, * maxSize: 1000, // up to 1000 (active OR hibernated) keys total * hibernateAfterMs: 30_000, // idle 30s → hibernate * }); * * const counter = await pool.run('tenant-42', async (context) => { * const fn = await context.compileCallable(\`(args) => { * this.count = (this.count || 0) + args.delta; * return this.count; * }\`); * return await fn.call([{ delta: 1 }]); * }); * * // Wait long enough for the sweeper to hibernate it ... * // The next run wakes the context with `count` restored from the checkpoint: * const next = await pool.run('tenant-42', async (context) => { * const fn = await context.compileCallable(\`(args) => this.count + args.delta\`); * return await fn.call([{ delta: 1 }]); * }); * ``` */ import { type AdaptiveHibernationAdjustmentReason, type AdaptiveHibernationMetrics, type AdaptiveHibernationPolicyConfiguration, type AdaptiveHibernationPolicyOptions } from "./adaptiveHibernation"; import type { Context, ContextCheckpoint, ContextCheckpointOptions, IsolateOptions } from "./types"; /** * Pluggable storage for hibernated context checkpoints. The default * implementation is in-memory (one process). Pass a custom store for * persistent / shared hibernation (Redis, S3, local file cache, etc.). */ export type HibernationStore = { get: (key: string) => Promise | ContextCheckpoint | undefined; put: (key: string, checkpoint: ContextCheckpoint) => Promise | void; delete: (key: string) => Promise | void; }; /** Default in-memory hibernation store (one process). */ export declare const createInMemoryHibernationStore: () => HibernationStore; export type HibernatingIsolatePoolOptions = { /** Per-isolate options passed to {@link createIsolate}. */ isolate?: IsolateOptions; /** * Max total entries (active + hibernated). When exceeded, LRU entries * are dropped — hibernated checkpoints first, then active contexts. * Default 100. */ maxSize?: number; /** * Auto-hibernate an active entry that's been idle for this many ms. * Set to `0` to disable auto-hibernation (only explicit * `pool.hibernate(key)` and the LRU evictor will hibernate then). * Default 60_000 (1 minute). */ hibernateAfterMs?: number; /** * Adapt the idle window from observed checkpoint residence and wake cost. * Bounds are mandatory policy: adaptation can never move outside them. * Omit to preserve the fixed `hibernateAfterMs` behavior. */ adaptiveHibernation?: Omit; /** * Background sweep interval. Default 5_000 ms. Sweeps run only when * the pool is non-empty and are unrefed so they don't keep the * process alive. */ sweepIntervalMs?: number; /** Pluggable storage. Default in-memory (one process). */ hibernationStore?: HibernationStore; /** Options forwarded to `context.checkpoint(options)` on hibernation. */ checkpointOptions?: ContextCheckpointOptions; /** * Optional hook called whenever an entry's state changes. Useful for * observability — e.g. log every hibernate/wake to a metrics sink. * Errors thrown by the hook are caught + ignored. */ onTransition?: (event: HibernationEvent) => void; /** * Optional OpenTelemetry tracer provider. When set, every `run()`, * `warm()`, and `hibernate()` call emits a span with * `abs.tenant` (the key) and event-specific attributes (wake * duration, hibernated byte length, etc.). When omitted, all * tracing is a zero-allocation noop. Added in 0.11.0. * * Structural type via `@absolutejs/telemetry`; no peer-dep on * `@opentelemetry/api`. */ tracerProvider?: import("@absolutejs/telemetry").TracerProvider; }; export type HibernationEvent = { type: "wake"; key: string; from: "hibernated"; byteLength: number; durationMs: number; } | { type: "hibernate"; key: string; byteLength: number; durationMs: number; } | { type: "evict"; key: string; from: "active" | "hibernated"; } | { type: "restore-fallback"; key: string; reason: "checkpoint-invalid" | "checkpoint-missing" | "store-error"; } | { type: "hibernate-failed"; key: string; reason: "checkpoint-error" | "store-error"; } | { type: "policy-adjusted"; action: "decrease" | "increase"; effectiveIdleMs: number; previousIdleMs: number; reason: AdaptiveHibernationAdjustmentReason; } | { type: "policy-reconfigured"; effectiveIdleMs: number; enabled: boolean; previousIdleMs: number; }; export type HibernatingPoolStats = { active: number; hibernated: number; total: number; }; /** * Operator-shaped metrics surfaced by {@link HibernatingIsolatePool.metrics} * (0.10.0+). Point-in-time fields + cumulative counters since pool start — * what a PaaS host scrapes on an interval to attribute hibernation cost and * detect a runaway tenant. */ export type HibernatingPoolMetrics = { /** Date.now() when this snapshot was taken. */ at: number; /** Active contexts right now. */ active: number; /** Hibernated keys in the store right now. */ hibernated: number; /** Total tracked entries (`active + hibernated`). */ total: number; /** Active runs that haven't returned yet (sum of `inFlight` across active entries). */ inFlight: number; /** Cumulative hibernations since pool start. */ hibernations: number; /** Cumulative wakes (from a hibernated checkpoint) since pool start. */ wakes: number; /** Cumulative LRU evictions since pool start. */ evictions: number; /** Cumulative bytes ever written to the hibernation store. Useful for storage cost attribution. */ bytesHibernated: number; /** Cumulative fresh isolate/context materializations, including safe restore fallbacks. */ spawns: number; /** Cumulative hibernation attempts that failed and evicted their active entry. */ hibernationFailures: number; /** Cumulative wakes that safely fell back to a fresh context. */ restoreFallbacks: number; /** Duration of the most recent fresh isolate/context materialization. */ lastSpawnMs: number; /** Duration of the most recent successful hibernation write. */ lastHibernateMs: number; /** * Wake duration of the most recent wake event, in ms. Useful as a * coarse SLO signal — a wake taking seconds suggests checkpoint size * blow-up or a slow store backend. */ lastWakeMs: number; /** True when the pool is draining (refusing new keys). */ draining: boolean; /** Adaptive idle-window posture, or null when fixed-window mode is used. */ adaptiveHibernation: AdaptiveHibernationMetrics | null; }; export type HibernatingIsolatePool = { /** * Resolve the context for `key` (waking from a hibernated checkpoint if * needed, or spawning fresh if the key has never been seen) and run * `fn(context)` with it. Concurrent runs on the same key share the same * context via a wake-once promise. */ run: (key: string, fn: (context: Context) => Promise) => Promise; /** * Force-hibernate the entry for `key` now. No-op if the key is unknown, * already hibernated, or currently in-flight (we wait for inFlight to * settle before hibernating). */ hibernate: (key: string) => Promise; /** * Atomically replace or disable the adaptive idle-window policy without * disposing active contexts or deleting checkpoints. Replacement clears * partial evidence from the previous regime and clamps the effective * window into the new bounds. */ configureAdaptiveHibernation: (configuration: AdaptiveHibernationPolicyConfiguration | null) => AdaptiveHibernationMetrics | null; /** Snapshot of the pool's current state. */ stats: () => HibernatingPoolStats; /** * Operator-shaped metrics — point-in-time + cumulative counters since * pool start. What a PaaS host scrapes for cost attribution + SLO * monitoring. Added in 0.10.0. */ metrics: () => HibernatingPoolMetrics; /** * Begin draining: refuse new keys (`run` / `warm` on an unknown key * rejects with an error). Active + hibernated entries keep serving * existing callers; wait for `stats().total === 0` then call * `dispose()` for a clean shutdown. Added in 0.10.0. */ drain: () => void; /** * Ensure an active context exists for `key`, waking it from hibernation * (or spawning fresh) without invoking user code. Use ahead of expected * work to remove the cold-start tail from a tenant's first request. * Returns when the context is live and ready. Added in 0.10.0. */ warm: (key: string) => Promise; /** Dispose every active isolate. Does NOT delete hibernated checkpoints * from the store — those persist (so a shared store survives process * restart). To purge, pass a store whose `delete` clears state and * call it externally. */ dispose: () => Promise; }; export declare const createHibernatingIsolatePool: (options?: HibernatingIsolatePoolOptions) => HibernatingIsolatePool;