Carry the id through a job
@evanion/nestjs-correlation-id 2.1.0 and its patch releases up to 2.1.1, the version on npm. Values on this page were produced by running the source at 77650ee, a later commit than the 2.1.0 tag, and are not re-executed.
To run a queue message’s work under the id the message carried, open a
correlation context with CorrelationService.run(id, callback).
CorrelationIdMiddleware opens none outside a request, so there
getCorrelationId() returns undefined.
Open a correlation context in a queue consumer
RestockJob is a queue consumer in Baize’s orders service that opens its own
correlation context with CorrelationService.run() from
@evanion/nestjs-correlation-id. For each restock message it asks stock for
the game’s current level. The code that queued the message, such as a request
handler in orders, copied getCorrelationId() into the message’s
correlationId field, so the message carries that request’s id when there was
one.
CorrelationConfig is the type of the configuration forRoot() builds from its
argument and the defaults, with the fields header, generator and validate.
CORRELATION_CONFIG_TOKEN is the injection token forRoot() provides that
configuration under, and RestockJob reads validate from it to check the id a
message carried. RestockJob injects CorrelationService, the configuration,
and StockClient, the client Forward the id builds:
import { Inject, Injectable } from '@nestjs/common';
import {
CORRELATION_CONFIG_TOKEN,
CorrelationService,
type CorrelationConfig,
} from '@evanion/nestjs-correlation-id';
import { StockClient } from './stock.client.js';
export interface RestockMessage {
game: string;
correlationId?: string;
}
@Injectable()
export class RestockJob {
constructor(
private readonly correlation: CorrelationService,
@Inject(CORRELATION_CONFIG_TOKEN)
private readonly config: CorrelationConfig,
private readonly stock: StockClient,
) {}
handle(message: RestockMessage) {
const carried = message.correlationId;
const id =
carried && this.config.validate?.(carried)
? carried
: this.correlation.generate();
return this.correlation.run(id, () => this.stock.level(message.game));
}
}run(id, callback)runscallbackinside a correlation context and returns whatcallbackreturns, so the promise fromstock.level()comes straight back out ofhandle().- Everything the callback calls or awaits reads
id.StockModuleregistersStockClient’sHttpServicewithwithCorrelation(), so its call tostockcarries the id. generate()returns a new id from the configured generator,randomUUIDunlessforRoot()was given another. A cron job has no id to carry, so it runs its work insiderun(generate(), …).
Declare RestockJob in a module that imports StockModule, and call handle()
from your queue library’s consumer.
Why RestockJob validates the carried id
An id read off a queue message reaches the same log lines and outgoing headers
as one read off a request, so RestockJob runs it through the same check
@evanion/nestjs-correlation-id applies to a request. validate on the
injected configuration is the function CorrelationIdMiddleware applies to an
incoming header, DEFAULT_CORRELATION_ID_VALIDATOR unless forRoot() was given
another. A carried id that fails the check is dropped, and the job generates its
own.
See the id each restock message runs under
The code below builds RestockJob by hand on a CorrelationService from
@evanion/nestjs-correlation-id, with a stand-in for StockClient that records
the id each call to stock runs under. It hands the job three messages: one
carrying storefront-4f1c9a, one carrying no id, and one carrying
storefront 4f1c9a, whose space fails DEFAULT_CORRELATION_ID_VALIDATOR:
import {
CorrelationService,
DEFAULT_CORRELATION_ID_VALIDATOR,
} from '@evanion/nestjs-correlation-id';
import { RestockJob } from '../examples/restock.job.js';
const config = {
header: 'X-Correlation-Id',
generator: () => 'orders-7c41d2',
validate: DEFAULT_CORRELATION_ID_VALIDATOR,
};
const correlation = new CorrelationService(config);
// Stands in for StockClient and records the id each call runs under, the id
// StockModule's HttpService sends to stock. `as never` lets it stand in for the
// class, which also holds an HttpService.
const sent: (string | undefined)[] = [];
const stock = {
level: async (game: string) => {
sent.push(correlation.getCorrelationId());
return { game, available: 3 };
},
};
const job = new RestockJob(correlation, config, stock as never);
await job.handle({ game: 'urn:game:azul', correlationId: 'storefront-4f1c9a' });
await job.handle({ game: 'urn:game:azul' });
await job.handle({ game: 'urn:game:azul', correlationId: 'storefront 4f1c9a' });
sent; // -> ['storefront-4f1c9a', 'orders-7c41d2', 'orders-7c41d2']The first message ran under its own id. The other two ran under the id the generator returned.
Replace the id inside a correlation context
CorrelationService.setCorrelationId(id), from
@evanion/nestjs-correlation-id, replaces the id for the rest of the context it
is called in. It serves a consumer that opens its context before it has read the
message: a wrapper calls run(generate(), …) around every handler, so each
handler starts under a generated id. A handler that then reads an id off its
message passes that id to setCorrelationId. Outside any context
setCorrelationId throws.
The code below builds a queue worker, the process that runs such handlers, as a
Nest application context, which starts no HTTP server. It calls generate,
run and setCorrelationId:
import { } from '@nestjs/core';
import {
,
,
} from '@evanion/nestjs-correlation-id';
const = await .(
.({ : () => 'orders-7c41d2' }),
{ : false },
);
const = .();
const = .(.(), () =>
.(),
);
let : string | undefined;
.('orders-0f3a2b', () => {
.('storefront-4f1c9a');
= .();
});
let = '';
try {
.('storefront-4f1c9a');
} catch () {
= ( as Error).;
}
await .();
; // -> 'orders-7c41d2'
; // -> 'storefront-4f1c9a'
; // -> 'setCorrelationId() was called outside a correlation context. Apply CorrelationIdMiddleware, or wrap the work in CorrelationService.run().'The generated id came from the generator passed to forRoot(). Inside
run('orders-0f3a2b', …), setCorrelationId changed what getCorrelationId()
returns. Outside, the call threw, and the error message names the two ways to
open a context: CorrelationIdMiddleware and CorrelationService.run().
Inside a request, setCorrelationId leaves the response header alone. The
header keeps the id the middleware wrote when the request arrived.
Change the header, the generator and the check
A job now carries the id its message brought, and
@evanion/nestjs-correlation-id generates a fresh one when the message brought
none. Configuration changes the header, the generator and
the check @evanion/nestjs-correlation-id applies to an incoming id, through
the fields forRoot() takes.