Overview
The execution context provides a way to share state and communicate between middleware, tasks, and hooks within a single execution. The primary mechanism is the execution journal, which acts as a typed key-value store for middleware coordination.
import { journal } from "@bluelibs/runner";
// Create a journal key
const abortControllerKey = journal.createKey<AbortController>("timeout.abortController");
// In middleware: store value
const controller = new AbortController();
journal.set(abortControllerKey, controller);
// In task: retrieve value
const controller = journal.get(abortControllerKey);
if (controller) {
// Use the abort controller
}
Execution Journal
The execution journal is created per task execution and passed through the middleware chain. It allows middleware to:
- Store intermediate state
- Communicate with tasks
- Share data between middleware layers
Creating Journal Keys
Create typed keys for storing values in the journal:
journal.createKey<T>(id: string): JournalKey<T>
Unique identifier for the journal key.const retryCountKey = journal.createKey<number>("middleware.retry.count");
const timestampKey = journal.createKey<Date>("middleware.timestamp");
const abortKey = journal.createKey<AbortController>("middleware.abort");
A typed journal key for storing and retrieving values.
Storing Values
Store a value in the journal:
journal.set<T>(
key: JournalKey<T>,
value: T,
options?: { override?: boolean }
): void
The journal key to store the value under.
When true, allows overwriting existing value. When false, throws if key already exists.
Example:
const timestampKey = journal.createKey<Date>("timestamp");
journal.set(timestampKey, new Date());
// Overwrite existing value
journal.set(timestampKey, new Date(), { override: true });
Throws: RuntimeError if key already exists and override is false
Retrieving Values
Retrieve a value from the journal:
journal.get<T>(key: JournalKey<T>): T | undefined
The journal key to retrieve the value for.
The stored value, or undefined if not present.
Example:
const value = journal.get(timestampKey);
if (value) {
console.log("Timestamp:", value);
}
Checking Existence
Check if a key exists in the journal:
journal.has<T>(key: JournalKey<T>): boolean
Example:
if (journal.has(abortControllerKey)) {
const controller = journal.get(abortControllerKey)!;
controller.abort();
}
Creating Custom Journal
Create a new journal instance:
journal.create(): ExecutionJournal
Useful when you need to pass a specific journal to runTask():
const customJournal = journal.create();
customJournal.set(someKey, someValue);
await runtime.runTask(myTask, input, { journal: customJournal });
export interface ExecutionJournal {
set<T>(key: JournalKey<T>, value: T, options?: { override?: boolean }): void;
get<T>(key: JournalKey<T>): T | undefined;
has<T>(key: JournalKey<T>): boolean;
}
export interface JournalKey<T> {
id: string;
_type?: T; // Phantom type for TypeScript inference
}
export const journal: {
createKey<T>(id: string): JournalKey<T>;
create(): ExecutionJournal;
};
Usage Patterns
Middleware Communication
Middleware can store state that tasks can read:
import { journal } from "@bluelibs/runner";
// Define journal keys
const abortControllerKey = journal.createKey<AbortController>("timeout.abortController");
const startTimeKey = journal.createKey<number>("timing.start");
// Timeout middleware
const timeoutMiddleware = r.middleware.task("timeout")
.configSchema<{ ms: number }>({ parse: (v) => v })
.run(async ({ config, next, journal }) => {
const controller = new AbortController();
journal.set(abortControllerKey, controller);
const timeoutId = setTimeout(() => controller.abort(), config.ms);
try {
return await next();
} finally {
clearTimeout(timeoutId);
}
})
.build();
// Task that uses the abort controller
const fetchData = r.task("fetchData")
.middleware([timeoutMiddleware.with({ ms: 5000 })])
.run(async ({ journal }) => {
const controller = journal.get(abortControllerKey);
const response = await fetch('https://api.example.com/data', {
signal: controller?.signal,
});
return await response.json();
})
.build();
Timing and Metrics
const timingMiddleware = r.middleware.task("timing")
.run(async ({ task, next, journal, logger }) => {
const startTime = Date.now();
journal.set(startTimeKey, startTime);
try {
const result = await next();
const duration = Date.now() - startTime;
await logger.info(`${task.id} completed in ${duration}ms`);
return result;
} catch (error) {
const duration = Date.now() - startTime;
await logger.error(`${task.id} failed after ${duration}ms`);
throw error;
}
})
.build();
Retry State
const retryCountKey = journal.createKey<number>("retry.count");
const retryErrorsKey = journal.createKey<Error[]>("retry.errors");
const retryMiddleware = r.middleware.task("retry")
.configSchema<{ maxAttempts: number }>({ parse: (v) => v })
.run(async ({ config, next, journal }) => {
const errors: Error[] = [];
for (let attempt = 1; attempt <= config.maxAttempts; attempt++) {
journal.set(retryCountKey, attempt, { override: true });
try {
return await next();
} catch (error) {
errors.push(error as Error);
journal.set(retryErrorsKey, errors, { override: true });
if (attempt === config.maxAttempts) {
throw error;
}
}
}
throw new Error('Unreachable');
})
.build();
// Task can inspect retry state
const task = r.task("myTask")
.middleware([retryMiddleware.with({ maxAttempts: 3 })])
.run(async ({ journal, logger }) => {
const attempt = journal.get(retryCountKey) ?? 1;
await logger.info(`Attempt ${attempt}`);
// ... task logic
})
.build();
Cache Keys
const cacheKeyKey = journal.createKey<string>("cache.key");
const cacheHitKey = journal.createKey<boolean>("cache.hit");
const cacheMiddleware = r.middleware.task("cache")
.dependencies(() => ({ cache: cacheResource }))
.run(async ({ task, input, deps, next, journal }) => {
const cacheKey = `${task.id}:${JSON.stringify(input)}`;
journal.set(cacheKeyKey, cacheKey);
const cached = await deps.cache.get(cacheKey);
if (cached) {
journal.set(cacheHitKey, true);
return cached;
}
journal.set(cacheHitKey, false);
const result = await next();
await deps.cache.set(cacheKey, result);
return result;
})
.build();
// Task can check if result was cached
const task = r.task("myTask")
.middleware([cacheMiddleware])
.run(async ({ journal, logger }) => {
const wasHit = journal.get(cacheHitKey);
await logger.info(wasHit ? 'Cache hit' : 'Cache miss');
// ... task logic
})
.build();
Circuit Breaker State
const circuitStateKey = journal.createKey<'open' | 'closed' | 'half-open'>('circuit.state');
const circuitBreaker = r.middleware.task("circuitBreaker")
.run(async ({ next, journal, logger }) => {
const state = getCircuitState(); // From circuit breaker resource
journal.set(circuitStateKey, state);
if (state === 'open') {
throw new Error('Circuit breaker is open');
}
try {
const result = await next();
recordSuccess();
return result;
} catch (error) {
recordFailure();
throw error;
}
})
.build();
Global Middleware Journal Keys
Built-in middleware expose their journal keys:
import { globals } from "@bluelibs/runner";
// Retry middleware keys
const retryKeys = globals.middleware.task.retry.journalKeys;
const attempt = journal.get(retryKeys.attemptNumber);
// Cache middleware keys
const cacheKeys = globals.middleware.task.cache.journalKeys;
const wasHit = journal.get(cacheKeys.hit);
// Timeout middleware keys
const timeoutKeys = globals.middleware.task.timeout.journalKeys;
const controller = journal.get(timeoutKeys.abortController);
// Circuit breaker keys
const cbKeys = globals.middleware.task.circuitBreaker.journalKeys;
const state = journal.get(cbKeys.state);
// Rate limit keys
const rlKeys = globals.middleware.task.rateLimit.journalKeys;
const remaining = journal.get(rlKeys.remaining);
// Fallback keys
const fbKeys = globals.middleware.task.fallback.journalKeys;
const usedFallback = journal.get(fbKeys.usedFallback);
Best Practices
1. Use Descriptive Key IDs
// Good
const abortControllerKey = journal.createKey<AbortController>("middleware.timeout.abortController");
// Avoid
const key = journal.createKey<any>("key");
2. Type Your Keys
// Good
const countKey = journal.createKey<number>("retry.count");
const count = journal.get(countKey); // number | undefined
// Avoid
const countKey = journal.createKey("retry.count"); // any
3. Check Existence Before Use
// Good
const controller = journal.get(abortControllerKey);
if (controller) {
controller.abort();
}
// Or use has()
if (journal.has(abortControllerKey)) {
const controller = journal.get(abortControllerKey)!;
controller.abort();
}
4. Document Journal Keys
Expose journal keys from your middleware:
export const myMiddleware = r.middleware.task("myMiddleware")
.run(async ({ next, journal }) => {
journal.set(myMiddleware.journalKeys.state, 'active');
return await next();
})
.build();
// Expose keys
myMiddleware.journalKeys = {
state: journal.createKey<string>("myMiddleware.state"),
};
5. Use Override When Updating
// When you need to update a value
for (let i = 0; i < attempts; i++) {
journal.set(attemptKey, i, { override: true });
// ...
}