Skip to content

Releases: bluelibs/runner

6.8.0

Choose a tag to compare

@theodorDiaconu theodorDiaconu released this 30 Sep 15:41

Highlights

  • Shared Redis resilience (Node) — register resources.resilience.with({ namespace, redis }) to share rate-limit, circuit-breaker, and concurrency state across replicas. Admission and transitions are atomic and use Redis time; permits are renewable and ownership-checked, and circuit recovery uses leased half-open probes. Without the registration nothing changes: state stays in memory and no Redis connection is opened.
  • Per-middleware coordination — rateLimit, circuitBreaker, and concurrency accept coordination?: "local" | "distributed". Omitted adopts registered resilience or memory, "local" always uses memory, and "distributed" requires resilience at startup. A local concurrency cap can protect each worker while a distributed rate limit protects a shared provider allowance.
  • Bounded concurrency admission — concurrency gains keyBuilder(canonicalTaskId, input) for dynamic shared pools, maxQueue to bound waiting calls, and waitTimeoutMs to bound acquisition. Saturation and acquisition deadlines throw middlewareConcurrencyQueueFullError and middlewareConcurrencyWaitTimeoutError; cancellation frees waiting capacity promptly.
  • Distributed reservations — the injected resilience resource exposes semaphore({ key, limit, maxQueue?, waitTimeoutMs? }) with reusable withPermit(callback, { signal? }) handles that share the pool of unscoped keyed distributed concurrency middleware.
  • Local runtime shell (Node) — opt-in resources.shell attaches a Unix socket to a running container. connectShell() opens an interactive REPL with top-level await, session variables, completion, history, and .tasks / .resources / .status discovery; runShell() evaluates one command without a TTY and returns { success, output }. Run the connector through SSH for remote use; no HTTP or TCP listener is opened. See the runtime shell guide and the runnable example.
  • Read-only shell sessions — connectShell({ readOnly: true }), or resources.shell.with({ readOnly: true }) for every connection. Resources check shell.isReadOnly() at their write boundary; the mode follows the session's async execution scope.
  • Plain-object validation across realms — genuine object literals created in another JavaScript realm now pass plain-object checks, while class instances and spoofed prototypes are still rejected.

Compatibility and notes

  • Everything is additive and opt-in. Existing middleware configuration keeps its in-memory behavior, and maxQueue / waitTimeoutMs default to unlimited.
  • Redis resilience needs the optional ioredis dependency. Redis failures propagate: there is no automatic fallback to local limits, no offline queueing, and no command replay. Conflicting parameters for an existing shared policy are rejected, and an explicit semaphore cannot be combined with coordination: "distributed".
  • Shared policies are keyed by namespace, canonical task id, canonical middleware id, and the occurrence of that middleware. Keep graph ids and repeated-middleware order consistent between replicas; use key or keyBuilder to share concurrency across tasks.
  • Distributed permits and semaphores are reservations only: waiting queues are per runtime, there is no global FIFO, and lease loss aborts cooperatively without fencing external side effects. See the middleware guide.
  • The runtime shell requires Node 22+ on Linux or macOS and an owner-only socket directory. It executes JavaScript with the application's privileges and is not a sandbox; read-only mode is cooperative policy that resources must enforce.
  • The durable workflows guide now states the step guarantee precisely: persisted step results are reused on replay, and a step body can run again if the process crashes before its result is persisted. Use idempotency keys for external side effects.
  • Contributor AGENTS.md guides now document module boundaries across the repository.

Validation

  • npm run qa passed: 5,604 framework tests with 100% coverage, five benchmark-comparison tests, lint, type checks, multi-platform builds, entrypoint checks, and consumer bundle checks.
  • The Redis resilience suite passed against a real Redis instance: 76 tests across 11 suites.
  • The npm tarball was dry-run packed and inspected: 758 files, including the Node entrypoints, resilience and shell type declarations, and the published skill with the runtime shell guide.

Full changelog: 6.7.0...6.8.0

6.7.0

Choose a tag to compare

@theodorDiaconu theodorDiaconu released this 28 Sep 15:21

Highlights

  • Durable workflow lifecycle control — pause and resume executions, restart terminal or paused executions with linked lineage, and use continueAsNew to move long-running workflows into a fresh execution. Waiting, signals, cancellation, and lifecycle operations follow continuation chains. Restart starts fresh from input; continue-as-new can carry state.
  • Typed workflow state — d.setState, d.replaceState, and d.getState provide replay-safe state, with execution information through d.info() and state reads through the service and operator APIs. State carries across continuations by default and can be reset explicitly.
  • Durable Workflows Studio — lifecycle actions, workflow-state inspection, and restart/continuation lineage are available in the dashboard. Browse the updated screenshot gallery.
  • Lifecycle reliability — hardens pause/resume races, restart ownership and cleanup, continuation hand-offs, queued signal transfer, waiter wakeups, and saga rollback after cancellation. Redis transitions preserve opaque signal payloads and waiter results, and deduplicate scan results.
  • Clearer public APIs — editor-visible deprecations point duplicate aliases toward the canonical APIs; existing aliases remain available. Guides now use those canonical forms consistently.

Compatibility and migration

  • Durable lifecycle and state capabilities are additive. Custom stores can continue serving existing operations; invoking a new capability without the required store support fails with a capability error. See the durable workflows guide for contracts and limitations.
  • Prefer the explicit builder schema methods (inputSchema, configSchema, payloadSchema, and dataSchema), errors, top-level scope / subtreeOf / isSameDefinition, Match.fromSchema / Match.Schema, and middleware.task.* over their deprecated aliases. tags.system has no effect and can be removed.
  • Distribution byte-budget gates were removed; build, entrypoint, and consumer bundle checks remain.

Validation

  • npm run qa passed: 5,395 framework tests with 100% coverage, five benchmark-comparison tests, lint, type checks, multi-platform builds, entrypoint checks, and consumer bundle checks.
  • Durable Workflows Studio passed its builds, 53 server tests, and 29 web tests.
  • The npm tarball was packed and inspected, including runtime entrypoints and the published skill.
  • A fresh runner-dev scaffold installed both local 6.7.0 tarballs and passed type checks, build, and tests, including a Node 24 LTS run.

Full changelog: 6.6.0...6.7.0

6.6.0

Choose a tag to compare

@theodorDiaconu theodorDiaconu released this 16 Sep 09:09

Highlights

  • Durable Workflows Studio — a complete operations dashboard with workflow health and activity views, cursor-paged execution history, timelines, parent/child trees, signals, audit trails, schedules, and guarded repair actions. Browse the screenshot gallery.
  • Durable workflow scale and safety — payload-free execution-state reads, stable indexed cursor pagination, Redis index rebuild tooling, validate-before-persist starts and schedules, precise current-state tracking, and store-backed workflow concurrency and rate limits.
  • RPC lane transport retries — per-binding retry policies, exponential backoff with jitter, cancellation-aware delays, preserved error identity, and safe handling for non-replayable upload sources.
  • Toolchain and contract improvements — TypeScript 7 support plus stricter required-context configuration typing and refreshed guidance.

Validation

  • 5,065 framework tests passed with 100% coverage.
  • Durable Workflows Studio passed 50 server tests and 26 web tests.
  • Type checks, linting, all builds, entrypoint checks, artifact and consumer bundle budgets, and the npm package dry-run passed.

Full changelog: 6.5.0...6.6.0

6.5.0

Choose a tag to compare

@theodorDiaconu theodorDiaconu released this 13 Sep 08:13

Runner 6.5.0

Runner 6.5.0 adds runtime graph inspection, improves task and event throughput,
and fixes isolation, lifecycle, validation, and remote-lane correctness issues.

Runtime And Performance

  • Inspect the compiled runtime graph with runtime.inspect().snapshot() and
    runtime.inspect().explain(definition). Detached, immutable views include
    canonical ids, dependencies, effective middleware, overrides, and lifecycle waves.
  • Reduce task and event hot-path allocations through lazy execution frames and
    middleware and health-policy caching after runtime lock. Health checks remain
    live, and caches remain local to each runtime.
  • Accept mode: "dev", mode: "prod", and mode: "test" in TypeScript, matching
    the documented runtime behavior. Existing enum inputs and resolved RunnerMode
    types remain supported.
  • Add mode: "pre-prod" for pre-production deployments. Runner also detects it
    from NODE_ENV=pre-prod; it does not enable test-only override precedence.

The reviewed integration's GitHub microbenchmarks measured 38.4% more plain-task
throughput, 38.8% more middleware-task throughput, and 9.7% more event throughput
than the previous main revision. The cache speedup ratio was 12.8% lower, within
its configured tolerance.

Correctness And Hardening

  • Forward journals, caller sources, and cancellation signals through fluent task
    implementations.
  • Snapshot builder metadata and dependency maps so building definitions neither
    freezes caller-owned plain containers nor retains mutable wiring aliases.
  • Retain async-context metadata and event error declarations.
  • Protect cached health policies from mutation through caught error data.
  • Keep canonical ids distinct from local ids throughout ownership and stateful
    runtime operations.
  • Clean up lazy-resource initialization failures and preserve shutdown ordering.
  • Harden Match against pathological nesting and correct class-validation typing.
  • Fix durable attempt deadlines, terminal-state handling, and worker registration.
  • Sign the bytes sent by remote-lane transports, enforce authentication context
    boundaries, and handle cyclic values safely during Node-file detection.

Compatibility Notes

  • Serializer symbolPolicy continues to default to allow-all, as in 6.4.1.
    Well-known and global Symbol.for payloads remain accepted. well-known-only
    and disabled remain explicit options; unique symbols remain unsupported.
  • Node 22 remains the minimum supported runtime. Node 24 is the default for local
    development through .nvmrc and for CI.
  • Runtime call-source records returned by framework factories are now frozen and
    readonly. Code that previously edited a source must construct a new record.
  • check() with a class pattern validates the original value without hydrating
    it. Its corrected return type no longer promises prototype methods that do not
    exist on plain input. Use the hydrating parse API when an instance is required.
  • Optional Redis support now uses ioredis 6. Review custom integrations that
    directly depend on ioredis-specific behavior. Other dependency updates include
    cron-parser 5.10 and lru-cache 11.5.2.

The immutable-source and validation-type corrections above are intentional
contract changes; this release does not claim compatibility with every behavior
previously accepted by the implementation or its declarations.

Release Verification

QA enforces 100% statements, branches, functions, and lines, along with lint,
typechecking, platform builds, entrypoint checks, and artifact and consumer bundle
budgets. Compile-time and runtime regressions cover the documented mode literals;
serializer tests cover the historical default and explicit restrictive policies.

Benchmark CI now compares fresh measurements using identical workloads on both
revisions and enforces every configured threshold. It runs on pull requests and
pushes to main. Separate Redis/RabbitMQ fault tests exercise lease ownership,
redelivery, and reconnect behavior.

6.4.1

Choose a tag to compare

@theodorDiaconu theodorDiaconu released this 03 Jul 14:37
372e8b9

Runner 6.4.1 Release Notes

Hardening, decoupling, and correctness fixes


Overview

6.4.1 is a hardening and cleanup release. There are no headline API additions —
the focus is a task-scoped cache with explicit invalidation, a more predictable
durable shutdown/abort lifecycle, and a large internal decomposition of the
Store and the durable ExecutionManager into small, single-purpose modules.

Most applications can upgrade with no code changes. Two compatibility notes at
the bottom apply only if you implement a custom ICacheProvider or depend on the
exact JSON schema emitted by Match.Integer.


New Features

Task-scoped cache with explicit invalidation

The cache middleware now supports targeted invalidation by key and by semantic
ref, scoped per task and optionally per identity:

  • invalidateKeys(keys, { identityScope? }) and invalidateRefs(refs) on the
    cache resource, coordinated across all task cache targets.
  • A journal-scoped refs collector is exposed during an active cache miss so
    task code can attach semantic refs discovered while computing the result;
    refs are tracked per retry attempt so a retried computation re-derives its own
    ref set.
  • Reads now go through a single atomic, presence-aware getEntry() primitive
    instead of a racy get() + has() probe, so a stored undefined is a hit,
    not a miss.

Middleware journal keys

Task middleware can declare typed journal keys for execution-local
coordination, exposed as middleware.journalKeys. The cache middleware uses this
to publish its hit flag and ref collector; the retry middleware's attempt
counter is consumed to scope collected refs per attempt. ExecutionJournal also
gains an optional delete(key).

Collaborative shutdown abort for durable workflows

Runner's shutdown lifecycle gains an explicit "Aborting" phase between graceful
drain and disposal. When drain does not complete in budget, Runner emits
events.aborting, then cooperatively interrupts in-flight durable attempts via
the new durableRuntimeTag fan-out so active steps can stop promptly and resume
on the next runtime — without being marked failed or cancelled. Durable runtimes
now expose interruptActiveAttempts(reason?) for this.


Fixes

  • Durable recovery no longer replays a legitimately-waiting parent. A
    Sleeping execution parked on a waitForExecution(..., { timeoutMs }) timeout
    timer is now correctly skipped by recovery (previously only sleep() and
    waitForSignal() timers were recognized).
  • No spurious wait timeout on a completed child. If a worker crashes in the
    window between persisting a child's terminal state and resolving its waiters,
    the parent's wait-timeout timer now resolves against the actual terminal result
    instead of recording a false timeout. The read is race-safe under the
    execution-wait lock.
  • Cancelling-state recovery bug fixed for durable executions.
  • Stable schedule identity. updateSchedule() now fails fast if an update
    tries to change a schedule's id, and always preserves the original id.
  • Cache providers are validated at wiring time, and the shutdown abort
    lifecycle ordering was hardened.
  • Match.Integer accepts any JavaScript integer. The previous, surprising
    32-bit clamp is removed, so millisecond timestamps and large ids validate
    correctly. See Compatibility Notes.
  • Standardized internal-server-error messaging and validation-detail handling in
    the HTTP exposure examples.

Internal

  • Store decentralized. The monolithic Store was split into focused
    collaborators (lifecycle coordination, registry writer/compiler, visibility
    tracking), and MemoryStore (~1,100 lines) was broken into a memory-store/
    module set (execution state/views/waiters, scheduling, signals, timers,
    snapshot, runtime).
  • Durable ExecutionManager decomposed from a single ~1,200-line class into a
    thin coordinator plus ExecutionAttemptRunner, AttemptCancellationController,
    and ExecutionManager.persistence / ExecutionManager.terminal helpers.
  • Redis cache invalidation batched (parallel smembers, hmget, batched
    entry removal) to cut per-entry round-trips.
  • Registry build is now strictly fail-fast (no partial-ownership rollback).
  • Removed the deprecated ExecutionManager.ts.bak; reorganized durable test
    folders; example (express) cleanups.

Dependencies

  • amqplib 1.0.5 → 2.0.1
  • lru-cache 11.3.6 → 11.5.1
  • ioredis 5.10.1 → 5.11.1
  • cron-parser 5.5.0 → 5.6.1

Compatibility Notes

  • Custom ICacheProvider implementations must add getEntry(key) and
    invalidateKeys(keys).
    Both are now required on the exported interface;
    getEntry returns a { value } envelope on a hit or undefined on a miss.
    The built-in memory provider and RedisCache are already updated — this only
    affects hand-written providers.
  • Match.Integer JSON schema changed. It no longer emits minimum/maximum
    (32-bit bounds); it now emits { "type": "integer" }. Regenerate any snapshot
    of emitted schemas if you assert on those bounds.

6.4.0

Choose a tag to compare

@theodorDiaconu theodorDiaconu released this 25 Mar 11:42
c82e51b

Runner 6.4 Release Notes

Branch feat/6.4 → main | 32 commits | 272 files changed


Overview

6.4 is the Durable Workflows era release. The headline change is a substantial rethinking of how durable workflow executions are persisted, identified, orchestrated, and recovered. Several types and APIs that shipped as optional or exploratory in earlier versions have been hardened, made required, or replaced with cleaner alternatives.

There are breaking changes in this release — mostly confined to the durable layer. If you are not using durable workflows you can upgrade with minimal friction (see the small set of non-durable changes at the bottom).

If you are already running durable workflows in production, read the End-to-End Upgrade Guide near the bottom before upgrading. The safe rollout order matters here.

We added ability to persist to file for MemoryWorkflows to be able to inspect recovery locally or via tests and we've also added ability to use custom serializer


Breaking Changes

This is an exception, we introduced breaking changes to durable workflows, an experimental feature which did not really have any real users.

1. Execution.taskId → Execution.workflowKey

The taskId field on the Execution type has been renamed to workflowKey. This is a data-level rename — values already persisted in a store will need a migration if you read them directly.

All internal consumers (queue messages, store queries, error constructors, filters) have been updated to the new name.

Why: taskId was the raw runtime canonical id, which was leaky and fragile. workflowKey now maps to the value declared via durableWorkflow.with({ key }), making it stable across renames and independent of the runtime registry id.

Important rollout note: if you are upgrading an existing durable deployment, do not introduce a new durableWorkflow.with({ key }) in the same deploy unless you also intend to migrate existing persisted workflowKey values. Otherwise old executions will stay grouped under the old canonical id while new executions move to the new stable key.


2. IDurableStore contract is now fully required

Several store methods that were previously optional are now required on IDurableStore:

Was optional Now required
listExecutions() ✅ required
listStepResults() ✅ required

The old two-step idempotency helpers have been replaced with a single atomic operation:

// REMOVED (optional pair):
getExecutionIdByIdempotencyKey?(...)
setExecutionIdByIdempotencyKey?(...)

// ADDED (required, atomic):
createExecutionWithIdempotencyKey(params: {
  execution: Execution;
  workflowKey: string;
  idempotencyKey: string;
}): Promise<{ created: true; executionId: string } | { created: false; executionId: string }>

Additionally the full signal journaling API and execution waiter API are now part of the core store contract (previously scattered between optional extensions). Custom store implementations targeting this layer will need updates.


3. describe() removed — replaced by getRepository()

IDurableResource.describe(task, input) which ran a dry-run recording pass (via DurableFlowShape) has been removed along with the entire flowShape.ts module.

It is replaced by getRepository<TInput, TResult>(task), which returns a live, fully typed read API against store data:

// REMOVED:
await durable.describe(myWorkflowTask, sampleInput);

// ADDED:
const repo = durable.getRepository(myWorkflowTask);
const records = await repo.find({ status: "running" });
const tree = await repo.findTree({ parentExecutionId: null });
const one = await repo.findOneOrFail({ id: executionId });

Why: describe() was a static recording that could only approximate a workflow's shape. The repository gives real execution data and supports rich filtering, sorting, and tree traversal.


4. DurableServiceConfig.worker → roles

The worker: boolean top-level config field on durable resources has been replaced with a structured roles object:

// REMOVED:
{
  worker: true;
}

// ADDED:
{
  roles: {
    queueConsumer: true;
  }
}

Queue creation also changed — the queue is no longer auto-created when worker: true was set. You must now explicitly declare both intent to use a queue and intent to consume it:

// redisDurableResource / memoryDurableResource
{
  queue: {
    enabled: true,   // creates the queue transport
    consume: true,   // this instance will consume messages
  }
}

For the built-in Runner workflow resources (resources.memoryWorkflow / resources.redisWorkflow), prefer the queue.consume switch above. The lower-level roles.queueConsumer knob still exists for custom durable runtime wiring.


5. ScheduleConfig is now a discriminated union

cron and interval are now mutually exclusive at the type level. Previously both were independently optional; the new RecurringScheduleConfig enforces exactly one:

// TypeScript will now catch:
{ id: "x", task: myTask, input: {}, cron: "* * * * *", interval: 60_000 } // ❌ compile error

6. ExecuteOptions.waitPollIntervalMs relocated

waitPollIntervalMs has been moved out of ExecuteOptions and into a dedicated WaitOptions interface used internally. It is still present in StartAndWaitOptions (which extends ExecuteOptions).

timeout was not renamed. ExecuteOptions.timeout still exists and still bounds workflow runtime.

StartAndWaitOptions now properly separates two distinct timeout semantics:

interface StartAndWaitOptions extends ExecuteOptions {
  waitTimeout?: number; // how long the *caller* waits before giving up
  waitPollIntervalMs?: number; // polling cadence if pub/sub not available
  // ExecuteOptions.timeout → bounds the workflow's own runtime
}

7. ListExecutionsOptions.taskId → workflowKey

The store-level filter for listing executions now uses workflowKey:

store.listExecutions({ workflowKey: "order-processing" });

8. RedisStore legacy support removed

The legacy migration compatibility paths inside RedisStore have been stripped. If your store data relied on legacy key formats you will need to run a migration before upgrading.


New Features

Durable Step Cancellation Signals

Durable step callbacks now receive a live { signal: AbortSignal } context so running work can cooperate with execution cancellation mid-step:

await ctx.step("ship-order", async ({ signal }) => {
  signal.throwIfAborted();
  return await shippingApi.createLabel(orderId, { signal });
});
  • Works with abort-aware APIs such as fetch(..., { signal })
  • Lets long-running loops bail out cleanly between chunks of work
  • Replay stays deterministic: cached steps still return without re-running the callback

Cancellation is still cooperative. For actively running executions, cancelExecution() now first records the request and flips the step signal, then marks the execution terminal cancelled once the attempt exits.


Cron Schedule Timezones

Recurring cron schedules now support an explicit timezone field:

await durable.ensureSchedule(reportTask, {}, {
  id: "daily-report",
  cron: "0 9 * * *",
  timezone: "America/New_York",
});
  • Uses IANA timezone identifiers such as "UTC" or "America/New_York"
  • Keeps business-facing wall-clock schedules stable across DST transitions
  • Applies to schedule creation and schedule updates

If timezone is omitted, cron schedules continue to use the process local timezone.


Workflow Orchestration: context.workflow()

Durable contexts gain a workflow() method for replay-safe child workflow starting:

step("kick-off-child", async () => {
  const childId = await ctx.workflow(
    processOrderTask,
    { orderId },
    {
      timeout: 60_000,
    },
  );
  // childId is memoized — replays return the same id without re-starting
});
  • Automatically derives a deterministic idempotency key from parentExecutionId + stepId
  • Forwards parentExecutionId to the child for tree-building
  • Only accepts tasks tagged with durableWorkflow

Workflow Orchestration: context.waitForExecution()

Durable contexts can now suspend waiting for another workflow execution to complete:

const result = await ctx.waitForExecution(processOrderTask, childExecutionId, {
  timeoutMs: 30_000,
  stepId: "wait-child-order",
})

if (result.kind === "timeout") { ... }
if (result.kind === "completed") { ... result.value ... }

IDurableExecutionRepository — Live Execution Read API

Accessed via durable.getRepository(task), the repository exposes a typed, collection-style query interface over a task's execution history:

const repo = durable.getRepository<OrderInput, OrderResult>(processOrderTask);

// list with filters + pagination
const records = await repo.find(
  { status: "failed", createdAt: { $gte: new Date("2026-01-01") } },
  { sort: { createdAt: -1 }, limit: 10 },
);

// fetch complete execution + steps + audit in one call
const { execution, steps, audit } = await repo.findOneOrFail({ id });

// recursive child-workflow tree
const forest = await repo.findTree({ parentExecutionId: null });

Each record includes execution, steps, and audit entries.


RecoveryManager — Automatic Orphan Recovery

A new RecoveryManager identifies orphaned executions (those that failed mid-flight without reaching a terminal state) and re-queues them for retry:

// DurableServiceConfig
{
  recovery: {
    ...
Read more

6.3.0

Choose a tag to compare

@theodorDiaconu theodorDiaconu released this 19 Mar 11:20

Runner 6.3 Release Notes

Highlights

Runner 6.3 introduces external abort signal integration for runtime
lifecycle control, forced disposal escalation that escapes stuck graceful
shutdowns, selector-based hook targets that subscribe to entire resource
subtrees or predicate-matched events at bootstrap time, and semantic cache
invalidation via refs
that enables cross-task cache purging by domain
identity.

The tenant → identity rename is the most significant breaking change in
this release: asyncContexts.tenant becomes asyncContexts.identity,
ITenant becomes IIdentity with newly optional tenantId and an added
userId field, tenantScope middleware config becomes identityScope across
all built-in middleware (cache, rate limit, debounce/throttle, concurrency),
and a rich IdentityScopeMode replaces the simple "auto" | "required" | "off" union with user-aware partitioning modes ("auto:userId",
"required:userId", "full").

A new pluggable identity context via run(app, { identity }) lets
applications supply a custom async context that Runner reads for all
identity-aware framework behaviour — middleware partitioning, scope keys, and
the new identityContextResource all bind to the configured context instead of
hard-coded asyncContexts.identity.

Keyed middleware capacity management adds maxKeys to rate limit and
temporal (debounce/throttle) middleware configs with automatic stale-state
pruning and a new middlewareKeyCapacityExceededError when the limit is
breached. Default key builders now include serialized input so different
payloads no longer silently share the same middleware bucket.

Event Lane topology hook policies introduce hooks.only allowlists on
consume entries, replacing the deprecated tags.eventLaneHook tag-based
approach. Tag-based event lane routing (tags.eventLane) is also deprecated in
favour of r.eventLane(...).applyTo(...).

This release also ships a major internal decomposition of run() into focused
modules — the new RunShutdownController, RunDisposalSignal,
ForceDisposalController, and normalizeRunOptions utilities keep shutdown
orchestration out of the main entrypoint — extracts StoreRegistry from
Store for cleaner separation of concerns, adds cooperative shutdown guards
throughout ready and cooldown waves, tightens isOneOf() to only match
definition-identity emissions, names the two internal framework namespace
resources (system and runner), and moves test-only active-runtime cleanup
into a dedicated module.

High-level takeaways:

  • Breaking: asyncContexts.tenant → asyncContexts.identity with
    IIdentity { tenantId?: string; userId?: string }.
  • Breaking: tenantScope → identityScope across all built-in middleware,
    with new modes including "auto:userId", "required:userId", and "full".
  • Breaking: Event Lane topology profiles now use
    consume: [{ lane, hooks? }] instead of consume: [lane].
  • Pass an external AbortSignal to run() to cancel bootstrap or trigger
    graceful disposal without polluting ambient execution context signals.
  • Call runtime.dispose({ force: true }) to skip remaining graceful phases and
    jump directly to resource disposal.
  • Hook .on(...) now accepts subtreeOf(resource), predicate functions, and
    mixed arrays alongside exact event references.
  • Cache key builders can return { cacheKey, refs } and call
    cacheResource.invalidateRefs(...) to purge entries by semantic identity.
  • Use run(app, { identity: myAsyncContext }) to override the runtime identity
    source for all middleware partitioning.
  • Rate limit and temporal middleware accept maxKeys to cap tracked key
    cardinality and avoid unbounded memory growth.
  • Event Lane hook policies via consume[].hooks.only filter which hooks run
    during relay re-emits.
  • The isOneOf() event guard now requires definition identity — arbitrary
    { id }-shaped objects no longer match.
  • Bootstrap phases are now interruptible: shutdown requested mid-startup
    cooperatively stops remaining ready waves.
  • Internal framework resources (system, runner) now carry metadata for
    tooling and documentation.

Breaking Changes

Tenant → Identity Rename

The entire tenant subsystem has been renamed to "identity" to reflect that the
context now carries both tenant and user dimensions.

Before (6.2) After (6.3)
asyncContexts.tenant asyncContexts.identity
ITenant IIdentity
TenantContextValue IdentityContextValue
tenantScope (middleware config) identityScope
TenantScopeMode IdentityScopeMode
TenantScopedMiddlewareConfig IdentityScopedMiddlewareConfig
TenantScopeConfig IdentityScopeConfig
tenantContextRequiredError identityContextRequiredError
tenantInvalidContextError identityInvalidContextError
tenant.asyncContext.ts identity.asyncContext.ts
tenantScope.shared.ts identityScope.shared.ts

IIdentity payload changes:

// Before (6.2)
interface ITenant {
  tenantId: string; // required
}

// After (6.3)
interface IIdentity {
  tenantId?: string; // now optional
  userId?: string; // new, optional
}

Both fields are optional at the ambient context level so apps can establish
identity gradually across request/auth boundaries. Middleware that opts into
identity partitioning validates the fields it actually needs at use time.

Fix: rename all tenantScope middleware config keys to identityScope,
replace ITenant / TenantContextValue with IIdentity /
IdentityContextValue, and update asyncContexts.tenant references to
asyncContexts.identity.


Identity Scope Modes Expanded

The simple "auto" | "required" | "off" union is replaced by a richer
IdentityScopeMode:

Mode Behaviour
"auto" Partition by tenantId when present (same as old "auto")
"auto:userId" Like "auto" but appends userId when present
"required" Require tenantId (same as old "required")
"required:userId" Require tenantId + append userId when present
"full" Require both tenantId AND userId, prefix as <tenantId>:<userId>:â€Ķ
"off" Disable identity partitioning (same as old "off")

Fix: existing tenantScope: "auto" migrates to identityScope: "auto" and
keeps the same semantics. Only update the config key name unless you want finer
user-aware scoping.


Event Lane Topology Profile Consume Shape

Profile consume arrays now contain entry objects instead of bare lane
references:

// Before (6.2)
profiles: {
  worker: {
    consume: [ordersLane, paymentsLane];
  }
}

// After (6.3)
profiles: {
  worker: {
    consume: [
      { lane: ordersLane },
      { lane: paymentsLane, hooks: { only: [auditHook] } },
    ];
  }
}

Fix: wrap each lane reference in { lane: <ref> }. Optionally add
hooks: { only: [...] } for hook policy filtering.


Default Middleware Key Builder Now Includes Serialized Input

Previously: the default key builder for rate limit and temporal middleware
returned the bare taskId, so all calls to the same task shared a single
bucket regardless of input.

Now: the default key builder returns
canonicalTaskKey + ":" + serialized(input), so different payloads get
isolated middleware state. A MiddlewareKeyBuilderHelpers object is passed as
the third argument to custom key builders.

Fix: if your middleware intentionally shares state across all inputs for a
task, provide an explicit keyBuilder: (taskId) => taskId.


New Features

External Abort Signal for Runtime Lifecycle

run() now accepts an optional signal parameter that bridges an external
AbortSignal into the runtime shutdown lifecycle. This signal is intentionally
separate from ambient execution context signals — it controls only the runtime
lifecycle boundary.

const controller = new AbortController();
const runtime = await run(app, {
  signal: controller.signal,
  shutdownHooks: false, // typical when an outer host owns shutdown
});

// Later: trigger graceful disposal from outside
controller.abort();

Behaviour by lifecycle phase:

Phase Effect of signal abort
Before readiness Cancels bootstrap; rolls back any initialized resources
After readiness Starts graceful disposal (same as runtime.dispose())
Bootstrap already failed No-op — startup already owns rollback

Key details:

  • The signal listener is cleaned up as soon as the runtime finishes tearing
    down, preventing memory leaks from long-lived signals.
  • The signal does not feed into asyncContexts.execution or task-level abort
    propagation. It only governs the run() lifecycle boundary.
  • Combine with shutdownHooks: false when an external orchestrator (Lambda
    runtime, container mesh, job scheduler) owns the shutdown trigger.

...

Read more

6.2.0

Choose a tag to compare

@theodorDiaconu theodorDiaconu released this 16 Mar 11:08
04d436b

Runner 6.2 Release Notes

Highlights

Runner 6.2 introduces cooperative abort-signal propagation across tasks,
events, and middleware, adds first-class multi-tenant support across
built-in middleware with a dedicated tenant async context, exposes the
resolved runtime mode directly through runtime.mode and the new
resources.mode built-in, and makes resource composition callbacks mode-aware
via (config, mode) => ....

This release also adds Match.Range for numeric bounds validation,
introduces a lightweight execution context mode (frames: "off") for
signal-only propagation without frame tracking overhead, closes a long-standing
lazy-resource wakeup race during shutdown, hardens subtree-vs-local
middleware conflict detection
, switches decorator-backed DTO ergonomics to
standard ES decorators by default while preserving a legacy compatibility
entrypoint, hydrates decorated class schemas by default on parse, and ships
internal refactors that simplify error IDs, runtime call sources, and the
framework root structure.

High-level takeaways:

  • Abort signals flow through tasks, events, and the execution context tree,
    enabling cooperative cancellation across the runtime.
  • Multi-tenant concerns are now a first-class built-in capability.
  • Match.Range validates numeric bounds with inclusive/exclusive and integer
    options.
  • Execution context can run in lightweight mode for signal and correlation
    propagation without frame tracking overhead.
  • Resource composition can branch on the resolved Runner mode without reaching
    for process.env or resources.runtime.
  • Standard ES decorators are now the default public path.
  • Decorated class schemas now materialize actual class instances on parse.
  • Silent subtree/local middleware deduplication is gone; conflicts now throw.
  • Lazy resource initialisation is blocked once shutdown has started.
  • Error IDs are shortened from runner.errors.X.Y to X-Y (breaking).
  • RuntimeCallSource.path has been removed; use source.id (breaking).
  • RuntimeCallSourceKind.Middleware is now split into TaskMiddleware and
    ResourceMiddleware (breaking).

Breaking Changes

Node.js 22+ Is Now Required

Runner 6.2 now requires Node.js 22 or newer.

  • package.json#engines.node is now >=22.
  • This affects local development, CI, and production deployments for the Node
    entrypoint.
  • Browser, edge, and universal fetch-oriented surfaces remain available, but
    the published package contract now assumes a modern Node runtime for install
    and test tooling.

If you are upgrading from Runner 6.1 or earlier:

  • Upgrade local Node versions, CI runners, and container base images to Node
    22+ before installing 6.2.
  • Re-run dependency install after the Node upgrade so native and lockfile
    artifacts are resolved against the supported runtime.

This requirement is intentional for 6.2 because the runtime now leans on modern
Node facilities such as process.getBuiltinModule(...) in compatibility paths.


Bun and Deno Resolution Is Now Explicit

Runner 6.2 now makes package resolution clearer for Bun and Deno:

  • @bluelibs/runner resolves to the universal build by default in Bun and
    Deno.
  • @bluelibs/runner/node remains the explicit Node-compat opt-in surface
    for Bun and Deno users who intentionally want the Node-flavoured APIs.

Why this changed:

  • Deno satisfies both deno and node export conditions for npm packages.
  • Bun also supports runtime-specific export conditions.
  • Without explicit bun / deno conditions, those runtimes could drift toward
    the Node-conditioned root export even when the portable universal build was
    the safer default.

Practical guidance:

  • Use @bluelibs/runner for portable application code, fetch-based remote
    clients, and the cross-runtime core surface.
  • Use @bluelibs/runner/node only when you intentionally depend on Node-style
    capabilities such as smart HTTP clients, exposure servers, durable workflows,
    event lanes, or rpc lanes.
  • Importing ./node in Bun or Deno now clearly means "I want the Node-compat
    path", not "the root package happened to resolve there implicitly".

This does not claim that every ./node feature is equally battle-tested in
Bun and Deno. It does make the entrypoint contract explicit and predictable.


ES Decorators are the Default Public Import Path

Decorator-backed schemas and serializer DTOs now target standard ES decorators
from the main package entrypoint.

import { Match, Serializer } from "@bluelibs/runner";

If your project still uses legacy TypeScript decorators
(experimentalDecorators), import the compatibility surface instead:

import { Match, Serializer } from "@bluelibs/runner/decorators/legacy";

Key details:

  • The legacy subpath still exposes the full Match helper surface, not only
    Schema() / Field(). Helpers such as Match.ObjectIncluding(...),
    Match.ArrayOf(...), Match.fromSchema(...), and check(...) remain
    available there.
  • Neither decorator mode requires emitDecoratorMetadata or
    reflect-metadata.
  • The default @bluelibs/runner package now initializes Symbol.metadata
    when the runtime does not provide it yet, so the ES-decorator path works
    out of the box from the main import.
  • Native/runtime-provided Symbol.metadata implementations are preserved;
    Runner only fills the gap when the symbol is absent.
  • Legacy decorator support remains available for migration-sensitive projects,
    but the default public path now aligns with modern TypeScript decorator
    semantics.

Testing coverage now explicitly validates both modes:

  • ES decorator syntax is exercised through the default test pipeline.
  • Legacy decorator syntax is compiled in-test with
    experimentalDecorators: true to verify the compatibility entrypoint against
    real legacy decorator emit, not only manual decorator function calls.

Subtree Middleware Conflicts Now Throw Instead of Silently Deduplicating

Previously: when a subtree middleware and a task-local (or resource-local)
middleware resolved to the same logical id, Runner silently dropped the subtree
middleware and kept the local one.

Now: Runner throws a fast-fail error (subtreeMiddlewareConflict)
instead of silently deduplicating. This prevents accidental override of
cross-cutting policies.

// Before: subtree + local middleware with same id -> local silently "won"
// After: this throws subtreeMiddlewareConflictError at compose time

subtreePolicy({
  middleware: [rateLimitTaskMiddleware.with({ windowMs: 1000, max: 10 })],
  register: [
    myTask.middleware([
      rateLimitTaskMiddleware.with({ windowMs: 500, max: 5 }),
    ]),
  ],
});

Fix: rename the local or subtree entry, or remove whichever one is
redundant.

Also fixed in the same area: middleware definitions that carry a
user-defined use field (for example fluent builder output with custom
properties) were incorrectly treated as conditional subtree entries. They are
now recognised as direct middleware attachments.


Lazy Resources During Shutdown Now Throw

Previously: calling runtime.getLazyResourceValue(resource) while the
runtime was already tearing down could race against dispose(). The resource's
ready() hook might run after dispose() had already completed.

Now: any attempt to lazy-initialise a resource once shutdown has begun
(CoolingDown / Disposing / Drained / Disposed) throws the typed
lazyResourceShutdownAccess error immediately.

// Before: calling this during dispose() could silently race
const value = await runtime.getLazyResourceValue(heavyResource);

// After: throws lazyResourceShutdownAccessError if shutdown has begun

Fix: call getLazyResourceValue() before runtime.dispose() starts, or
handle the new error.


Error IDs Shortened

All framework error IDs have been shortened from the verbose
runner.errors.X.Y format to a flat X-Y or camelCase format.

Previously:

runner.errors.http.baseUrlRequired
runner.errors.middleware.timeout
runner.errors.serializer.invalidPayload
runner.errors.lazyResourceSyncAccess

Now:

http-baseUrlRequired
middleware-timeout
serializer-invalidPayload
lazyResourceSyncAccess

Fix: update any code that matches on error.id strings to use the new
shortened format. The RunnerErrorId enum values have been updated accordingly.


RuntimeCallSource.path Removed

Previously: RuntimeCallSource included an optional path field alongside
kind and id.

Now: the path field has been removed entirely. Only kind and id
remain.

// Before
type RuntimeCallSource = {
  kind: RuntimeCallSourceKind;
  id: string;
  path?: string; // ← REMOVED
};

// After
type RuntimeCallSource = {
  kind: RuntimeCallSourceKind;
  id: string;
};

Fix: replace source.path with source.id. The runtimeSource.*() factory
helpers no longer accept a path parameter.


RuntimeCallSourceKind.Middleware Split into Task and Resource

Previously: task middleware and resource middleware shared a single
"middleware" source kind.

Now: they are distinguished:

Old New
RuntimeCallSourceKind.Middleware RuntimeCallSourceKind.TaskMiddleware
RuntimeCallSourceKind.ResourceMiddleware
runtimeSource.middleware(id) runtimeSource.taskMiddleware(id)
...
Read more

6.1.0

Choose a tag to compare

@theodorDiaconu theodorDiaconu released this 12 Mar 11:36
364f417

Runner 6.1 Release Notes

Compared with main, Runner 6.1 focuses on three areas: per-key middleware state, a simpler resource ownership model, and more composable subtree policies.

Highlights

Per-key rate limiting, debounce, and throttle

rateLimit, debounce, and throttle now accept an optional keyBuilder(taskId, input) so their state can be partitioned by something more specific than the task definition.

  • Default behavior still partitions by taskId.
  • You can now express policies such as per-user, per-tenant, or per-IP limits.
  • Temporal middleware fails fast if keyBuilder does not return a string.
  • Large keyed state maps are pruned lazily to keep long-running processes from accumulating stale entries.
r.task("ratelimit-ip")
  .middleware([
    middleware.task.rateLimit.with({
      windowMs: 1_000,
      max: 50,
      keyBuilder: () => RequestContext.use().ip,
    }),
  ])
  .run(async () => "ok")
  .build();

This release also adds clearer coverage around keyed middleware behavior, including independent state per key and error propagation from keyBuilder.

Simpler resource ownership and canonical IDs

Runner now treats user resources consistently: every user-defined resource contributes its own ownership segment to canonical runtime IDs.

  • Root resources can directly register tasks, resources, and middleware, then be passed straight to run(...).
  • Nested resources now produce predictable structural IDs such as app.billing.tasks.charge.
  • Only the internal framework root remains transparent.

This makes runtime IDs easier to reason about and removes the special-case behavior around transparent user resources.

Subtree policies can now be composed with arrays

.subtree() now accepts either a single policy or an array of policies.

r.resource("app")
  .subtree([
    { tasks: { middleware: [loggingPolicy] } },
    { validate: [schemaPolicy] },
  ])
  .build();

This also works for config-driven subtree resolvers, which can now return a policy list instead of a single policy object.

Breaking Changes

gateway: true has been removed from user resources

User-facing gateway resources are no longer part of the public resource model.

  • r.resource(id, { gateway: true }) and equivalent defineResource(...) usage should be replaced with normal resources.
  • If you depended on gateway transparency, update any runtime IDs or references that previously skipped the resource segment.
  • Non-leaf resources still cannot be forked.

Before:

const billing = r.resource("billing", { gateway: true }).register([chargeTask]);

After:

const billing = r.resource("billing").register([chargeTask]);

The important migration detail is that task and child-resource IDs now include that resource segment in their canonical path.

Internal framework root IDs are now reserved

runtime-framework-root is reserved for internal Runner use and can no longer be used as a user resource ID.

Advanced Node integrations: RPC lanes resource ID changed

If you rely on the internal RPC lanes resource ID in advanced Node integrations, update it from platform-node-resources-rpcLanes to runner.node.rpcLanes.

Documentation Updates

  • Middleware documentation now shows keyBuilder on rateLimit, debounce, and throttle.
  • The guides now explicitly distinguish quota enforcement from burst shaping: use rateLimit for policies like 50/s, and debounce or throttle for burst control.
  • Examples now show per-key partitioning patterns, including async-context-backed keys such as request IPs.

Notes for Upgraders

  • Review any resource definitions that used gateway: true and replace them with normal resources.
  • Update string-based runtime task/resource references if they relied on transparent resource segments.
  • If you have reusable subtree policy builders, you can now return an array directly instead of manually merging policies first.
  • If you expose per-user or per-tenant workloads, consider moving from task-wide rate limits to keyed rate limits.

6.0.0

Choose a tag to compare

@theodorDiaconu theodorDiaconu released this 11 Mar 09:50

BlueLibs Runner 6.0 - Clean Release Notes

This release closes some fundamental gaps, full namespace ids are now disallowed. While it was fun, it became a source of confusion, and extra work as the codebase evolves and concerns/domains adapt.

We also took the opportunity to tie some loose ends, regarding overrides (and the fact they can only override their subtree), better fluent builder contracts, health checks, subtree policies, isolation policies, propper lifecycle (with ready(), cooldown() to start/stop accepting work), and more.

Highlights

  • Resource boundaries are now first-class with .isolate({ deny | only | exports | whitelist }).
  • Middleware governance moved to explicit scoping with resource.subtree(...) and taskRunner.intercept(...).
  • Override API is simplified to one path: r.override(baseDefinition, overrideFn).
  • Event system gained transactional execution with rollback-capable hooks.
  • Builder contracts are stricter and phase-aware, reducing invalid fluent chains at compile time.
  • Definitions are deeply immutable after build() and derived operations.
  • Tag usage is stricter and more discoverable (dependency injection + target scoping).
  • User-land IDs are restricted to "local" (dot-less) naming; dotted IDs are reserved for framework internals.
  • Shutdown behavior is stricter and more predictable (cooldown, lifecycle events, drain budgets, lifecycle admission control).
  • Node runtime gets lane-based distributed primitives:
    • Event Lanes v1 for queue-backed event delivery.
    • RPC Lanes v1 for remote task/event request-response routing.
  • Execution context is now opt-in via executionContext, exposing system.ctx.executionContext plus correlation ids, branch-local execution frames, and cycle detection.
  • Health monitoring built-in: runner.health resource reports initialization status per-resource at runtime.
  • Runtime pause/resume with r.pause() / r.resume() and auto-recovery via r.recoverWhen().
  • Lifecycle-owned timers via runner.timers — all timer handles are automatically cancelled during cooldown/dispose.
  • Node: Redis-backed cache provider (redisCacheProviderResource) for distributed task caching.

Breaking Changes

  • Removed legacy override builder family:
    • r.override.task(...)
    • r.override.resource(...)
    • r.override.hook(...)
    • r.override.taskMiddleware(...)
    • r.override.resourceMiddleware(...)
    • Use r.override(baseDefinition, overrideFn) instead.
  • ID Naming Policy:
    • Dotted IDs (e.g., runner.*, system.*) are now strictly reserved for framework internals.
    • User-defined tasks, resources, and hooks must use "local" (dot-less) IDs.
  • Removed middleware catch-all fields/APIs:
    • middleware.everywhere is removed from task/resource middleware definitions.
    • For subtree-scoped middleware use resource.subtree({ tasks: { middleware: [{ use: mw, when: (task) => myTag.exists(task) }] } }).
    • For global interception use taskRunner.intercept(fn, { when: (task) => isSameDefinition(task, myTask) }) inside a resource init().
    • myTag.exists(task) checks presence; myTag.extract(task) returns the tag config (or undefined).
  • Event source contract is now structured:
    • Event emissions use { kind, id } (RuntimeCallSource) instead of raw source strings.
  • RunOptions.initMode was renamed to lifecycleMode.
    • ResourceInitMode was renamed to ResourceLifecycleMode.
  • ITag now has a 4th generic parameter for allowed targets:
    • ITag<TConfig, TEnforceIn, TEnforceOut, TAllowedTargets>.
  • Fluent builder outputs are now deep-frozen:
    • Mutating built definitions is no longer supported.
  • Common accessors:
    • globals.resources.logger and globals.middleware flattened to resources.logger and middleware.*.
  • Removed deprecated compatibility aliases:
    • createContext(...) -> defineAsyncContext({ id })
    • errorHelper.create(data) -> errorHelper.new(data)
    • durable.startExecution(...), durable.execute(...), and durable.executeStrict(...) were removed in favor of start(...) / startAndWait(...)
    • globalTags.internal -> globalTags.system
    • circularDependenciesError / dependencyCycleError were removed in favor of circularDependencyError
  • Serializer Defaults:
    • Recommended symbolPolicy: "well-known-only" as default for public-facing APIs.

Features

  • Resource isolation and runtime export enforcement

    • .isolate({ deny | only | exports }) adds compile-time/wiring/runtime boundary control.
    • whitelist: [{ for: [...], targets: [...] }] selectively relaxes deny/only restrictions for named consumers without widening the boundary for everyone else.
    • Public runtime APIs (runTask, emitEvent, getResourceValue, getLazyResourceValue) enforce root export policy.
    • Legacy .exports([...]) on builders and resource-level exports declarations were removed; use .isolate({ exports: ... }) instead.
  • Subtree governance + interception

    • resource.subtree(...) supports subtree-scoped middleware and policy validators.
    • taskRunner.intercept(...) provides explicit global task interception with deterministic order.
  • Transactional events + hook compensation

    • .transactional() enables ordered listener execution with rollback.
    • Hooks must return an async undo closure if executing within a transaction.
    • Transactional constraints are fail-fast (transactional + parallel and transactional + eventLane are invalid).
  • Strict fluent ordering

    • Task/hook/resource/middleware builders enforce phase-locked chains after run() / init().
    • .schema() is now the unified alias for inputSchema, configSchema, payloadSchema, and dataSchema.
    • Invalid ordering now fails earlier with stronger TypeScript guidance.
  • Tag evolution

    • Tags can be injected as dependencies with lazy, cached, visibility-filtered access.
    • Tag target scoping via .for([...]) restricts where tags are valid.
    • New built-in tags: tags.system, tags.debug, tags.excludeFromGlobalHooks, tags.httpRoute.
  • Lifecycle and shutdown hardening

    • cooldown() resource lifecycle hook for pre-drain ingress stop.
    • Parallel dispose for resources initialized in parallel (wave-based order).
    • Nested dispose shutdown config with totalBudgetMs, drainingBudgetMs, and cooldownWindowMs.
    • New shutdown defaults: 30s total budget, 20s drain budget, 300ms post-cooldown window.
    • Lifecycle contract is explicit: globals.events.ready for startup, globals.events.disposing then globals.events.drained during shutdown.
    • Bootstrap shutdown handling and lifecycle admission control were hardened.
    • Process shutdown hooks now auto-clean up when unused.
  • Execution context + correlation

    • executionContext enables AsyncLocalStorage-backed execution snapshots for tasks, hooks, and interceptors.
    • system.ctx.executionContext.use() / .tryUse() expose { correlationId, startedAt, depth, currentFrame, frames }.
    • system.ctx.executionContext.provide({ correlationId? }, fn) seeds correlation at the boundary.
    • system.ctx.executionContext.record({ correlationId? }, fn) returns { result, recording } with the full execution tree for that scope.
    • Nested task/event/hook flows share a correlation id; parallel child tasks inherit ancestry and append branch-local frames.
    • Cycle detection is now configured under executionContext.cycleDetection.
  • Health monitoring (runner.health)

    • healthResource exposes an IHealthReporter that reports per-resource initialization status without coupling resources to the store internals.
    • Useful for readiness probes: check whether a specific resource has completed init() before accepting work.
    • Validates that the runtime is fully bootstrapped and not in shutdown before returning health data.
  • Runtime pause/resume

    • r.pause(reason?) blocks new task submissions and event emissions without killing in-flight work.
    • r.resume() lifts the pause manually.
    • r.recoverWhen(options) registers an auto-resume check: the runtime polls the provided condition and self-heals when it passes.
    • r.state returns the current RuntimeState ("running" | "paused").
  • Lifecycle-aware timers (runner.timers)

    • timersResource provides a RuntimeTimers controller that wraps setTimeout/setInterval with automatic lifecycle tracking.
    • All pending handles are cancelled during resource cooldown and dispose, preventing timer leaks across hot-reloads or test runs.
    • Inject as a dependency: { timers: timersResource }.
  • Safety and validation hardening

    • Tag self-dependency is rejected at bootstrap.
    • Dependency extraction resolves independent dependencies in parallel.
    • Serializer hardening: symbolPolicy, allowedTypes, maxDepth, and maxRegExpPatternLength.
  • Node: Event Lanes v1 (@bluelibs/runner/node)

    • Async, fire-and-forget routing for events across Runners.
    • Queue-backed event routing with lane references, profile topology, and transport modes.
    • Serializer-first message boundary, binding-level retry controls, and RabbitMQ queue adapter enhancements.
  • Node: RPC Lanes v1 (@bluelibs/runner/node)

    • Repleaces legacy "HTTP Tunnels".
    • Lane/profile-based remote routing for tagged tasks/events.
    • Supports network, transparent, and local-simulated modes.
    • Exposure allow-list integration is derived from served lanes.
    • Propagates Async Context across runner boundaries.
  • Node: Redis Cache provider (@bluelibs/runner/node)

    • redisCacheProviderResource wraps ioredis in a CacheProvider compatible with the built-in cache middleware.
    • Accepts a connection string, an existing ioredis-compatible client, or nothing (creates and owns a default...
Read more