Repository navigation
Releases: bluelibs/runner
Release list
6.8.0
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, andconcurrencyacceptcoordination?: "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 â
concurrencygainskeyBuilder(canonicalTaskId, input)for dynamic shared pools,maxQueueto bound waiting calls, andwaitTimeoutMsto bound acquisition. Saturation and acquisition deadlines throwmiddlewareConcurrencyQueueFullErrorandmiddlewareConcurrencyWaitTimeoutError; cancellation frees waiting capacity promptly. - Distributed reservations â the injected resilience resource exposes
semaphore({ key, limit, maxQueue?, waitTimeoutMs? })with reusablewithPermit(callback, { signal? })handles that share the pool of unscoped keyed distributed concurrency middleware. - Local runtime shell (Node) â opt-in
resources.shellattaches a Unix socket to a running container.connectShell()opens an interactive REPL with top-levelawait, session variables, completion, history, and.tasks/.resources/.statusdiscovery;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 }), orresources.shell.with({ readOnly: true })for every connection. Resources checkshell.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/waitTimeoutMsdefault to unlimited. - Redis resilience needs the optional
ioredisdependency. 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 explicitsemaphorecannot be combined withcoordination: "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
keyorkeyBuilderto 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.mdguides now document module boundaries across the repository.
Validation
npm run qapassed: 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
Highlights
- Durable workflow lifecycle control â pause and resume executions, restart terminal or paused executions with linked lineage, and use
continueAsNewto 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, andd.getStateprovide replay-safe state, with execution information throughd.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, anddataSchema),errors, top-levelscope/subtreeOf/isSameDefinition,Match.fromSchema/Match.Schema, andmiddleware.task.*over their deprecated aliases.tags.systemhas no effect and can be removed. - Distribution byte-budget gates were removed; build, entrypoint, and consumer bundle checks remain.
Validation
npm run qapassed: 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
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
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", andmode: "test"in TypeScript, matching
the documented runtime behavior. Existing enum inputs and resolvedRunnerMode
types remain supported. - Add
mode: "pre-prod"for pre-production deployments. Runner also detects it
fromNODE_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
symbolPolicycontinues to default toallow-all, as in 6.4.1.
Well-known and globalSymbol.forpayloads remain accepted.well-known-only
anddisabledremain explicit options; unique symbols remain unsupported. - Node 22 remains the minimum supported runtime. Node 24 is the default for local
development through.nvmrcand 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
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? })andinvalidateRefs(refs)on the
cache resource, coordinated across all task cache targets.- A journal-scoped
refscollector 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 racyget()+has()probe, so a storedundefinedis 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
Sleepingexecution parked on awaitForExecution(..., { timeoutMs })timeout
timer is now correctly skipped by recovery (previously onlysleep()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'sid, and always preserves the original id. - Cache providers are validated at wiring time, and the shutdown abort
lifecycle ordering was hardened. Match.Integeraccepts 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
Storewas split into focused
collaborators (lifecycle coordination, registry writer/compiler, visibility
tracking), andMemoryStore(~1,100 lines) was broken into amemory-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 plusExecutionAttemptRunner,AttemptCancellationController,
andExecutionManager.persistence/ExecutionManager.terminalhelpers. - 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
ICacheProviderimplementations must addgetEntry(key)and
invalidateKeys(keys). Both are now required on the exported interface;
getEntryreturns a{ value }envelope on a hit orundefinedon a miss.
The built-in memory provider andRedisCacheare already updated â this only
affects hand-written providers. Match.IntegerJSON schema changed. It no longer emitsminimum/maximum
(32-bit bounds); it now emits{ "type": "integer" }. Regenerate any snapshot
of emitted schemas if you assert on those bounds.
6.4.0
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 error6. 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
parentExecutionIdto 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: {
...6.3.0
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.identitywith
IIdentity { tenantId?: string; userId?: string }. - Breaking:
tenantScopeâidentityScopeacross 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 ofconsume: [lane]. - Pass an external
AbortSignaltorun()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 acceptssubtreeOf(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
maxKeysto cap tracked key
cardinality and avoid unbounded memory growth. - Event Lane hook policies via
consume[].hooks.onlyfilter 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.executionor task-level abort
propagation. It only governs therun()lifecycle boundary. - Combine with
shutdownHooks: falsewhen an external orchestrator (Lambda
runtime, container mesh, job scheduler) owns the shutdown trigger.
...
6.2.0
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.Rangevalidates 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
forprocess.envorresources.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.YtoX-Y(breaking). RuntimeCallSource.pathhas been removed; usesource.id(breaking).RuntimeCallSourceKind.Middlewareis now split intoTaskMiddlewareand
ResourceMiddleware(breaking).
Breaking Changes
Node.js 22+ Is Now Required
Runner 6.2 now requires Node.js 22 or newer.
package.json#engines.nodeis 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/runnerresolves to the universal build by default in Bun and
Deno.@bluelibs/runner/noderemains 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
denoandnodeexport conditions for npm packages. - Bun also supports runtime-specific export conditions.
- Without explicit
bun/denoconditions, those runtimes could drift toward
the Node-conditioned root export even when the portable universal build was
the safer default.
Practical guidance:
- Use
@bluelibs/runnerfor portable application code, fetch-based remote
clients, and the cross-runtime core surface. - Use
@bluelibs/runner/nodeonly when you intentionally depend on Node-style
capabilities such as smart HTTP clients, exposure servers, durable workflows,
event lanes, or rpc lanes. - Importing
./nodein 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
Matchhelper surface, not only
Schema()/Field(). Helpers such asMatch.ObjectIncluding(...),
Match.ArrayOf(...),Match.fromSchema(...), andcheck(...)remain
available there. - Neither decorator mode requires
emitDecoratorMetadataor
reflect-metadata. - The default
@bluelibs/runnerpackage now initializesSymbol.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.metadataimplementations 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: trueto 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 begunFix: 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) |
| ... |
6.1.0
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
keyBuilderdoes 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 equivalentdefineResource(...)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
keyBuilderonrateLimit,debounce, andthrottle. - The guides now explicitly distinguish quota enforcement from burst shaping: use
rateLimitfor policies like50/s, anddebounceorthrottlefor 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: trueand 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
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(...)andtaskRunner.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, exposingsystem.ctx.executionContextplus correlation ids, branch-local execution frames, and cycle detection. - Health monitoring built-in:
runner.healthresource reports initialization status per-resource at runtime. - Runtime pause/resume with
r.pause()/r.resume()and auto-recovery viar.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.
- Dotted IDs (e.g.,
- Removed middleware catch-all fields/APIs:
middleware.everywhereis 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 resourceinit(). myTag.exists(task)checks presence;myTag.extract(task)returns the tag config (orundefined).
- Event source contract is now structured:
- Event emissions use
{ kind, id }(RuntimeCallSource) instead of raw source strings.
- Event emissions use
RunOptions.initModewas renamed tolifecycleMode.ResourceInitModewas renamed toResourceLifecycleMode.
ITagnow 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.loggerandglobals.middlewareflattened toresources.loggerandmiddleware.*.
- Removed deprecated compatibility aliases:
createContext(...)->defineAsyncContext({ id })errorHelper.create(data)->errorHelper.new(data)durable.startExecution(...),durable.execute(...), anddurable.executeStrict(...)were removed in favor ofstart(...)/startAndWait(...)globalTags.internal->globalTags.systemcircularDependenciesError/dependencyCycleErrorwere removed in favor ofcircularDependencyError
- Serializer Defaults:
- Recommended
symbolPolicy: "well-known-only"as default for public-facing APIs.
- Recommended
Features
-
Resource isolation and runtime export enforcement
.isolate({ deny | only | exports })adds compile-time/wiring/runtime boundary control.whitelist: [{ for: [...], targets: [...] }]selectively relaxesdeny/onlyrestrictions 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-levelexportsdeclarations 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
undoclosure if executing within a transaction. - Transactional constraints are fail-fast (
transactional + parallelandtransactional + eventLaneare invalid).
-
Strict fluent ordering
- Task/hook/resource/middleware builders enforce phase-locked chains after
run()/init(). .schema()is now the unified alias forinputSchema,configSchema,payloadSchema, anddataSchema.- Invalid ordering now fails earlier with stronger TypeScript guidance.
- Task/hook/resource/middleware builders enforce phase-locked chains after
-
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
disposeshutdown config withtotalBudgetMs,drainingBudgetMs, andcooldownWindowMs. - New shutdown defaults:
30stotal budget,20sdrain budget,300mspost-cooldown window. - Lifecycle contract is explicit:
globals.events.readyfor startup,globals.events.disposingthenglobals.events.drainedduring shutdown. - Bootstrap shutdown handling and lifecycle admission control were hardened.
- Process shutdown hooks now auto-clean up when unused.
-
Execution context + correlation
executionContextenables 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)healthResourceexposes anIHealthReporterthat 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.statereturns the currentRuntimeState("running"|"paused").
-
Lifecycle-aware timers (
runner.timers)timersResourceprovides aRuntimeTimerscontroller that wrapssetTimeout/setIntervalwith automatic lifecycle tracking.- All pending handles are cancelled during resource
cooldownanddispose, 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, andmaxRegExpPatternLength.
-
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)redisCacheProviderResourcewrapsioredisin aCacheProvidercompatible with the built-in cache middleware.- Accepts a connection string, an existing
ioredis-compatible client, or nothing (creates and owns a default...