Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions src/IMQRPCOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,28 @@ export interface IMQAfterCall<_T> {
(req: IMQRPCRequest, res?: IMQRPCResponse): Promise<void>;
}

/**
* Around hook wrapping the actual service method invocation. It receives the
* request/response and a `next` callback that runs the method and resolves to
* its return value; it MUST call `next()` (returning its resolved value) to
* produce the response data. Unlike `beforeCall`/`afterCall`, this lets a hook
* run the method inside its own scope — e.g. establishing an OpenTelemetry
* context so any spans the method (and its downstream calls) create nest under
* the request span. When unset, the method is invoked directly.
*
* @param {IMQRPCRequest} req - the incoming request
* @param {IMQRPCResponse} res - the response being prepared
* @param {() => Promise<any>} next - runs the method, resolves to its result
* @return {Promise<any>} - the value to use as the response data
*/
export interface IMQWrapCall<_T> {
(
req: IMQRPCRequest,
res: IMQRPCResponse,
next: () => Promise<any>,
): Promise<any>;
}

/**
* Options for the built-in metrics server.
*/
Expand All @@ -67,6 +89,7 @@ export interface IMQServiceOptions extends IMQOptions {
metricsServer?: IMQMetricsServerOptions;
beforeCall?: IMQBeforeCall<IMQService>;
afterCall?: IMQAfterCall<IMQService>;
wrapCall?: IMQWrapCall<IMQService>;
}

/**
Expand Down
27 changes: 22 additions & 5 deletions src/IMQService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,11 @@ import { SIGNALS } from './helpers/index.js';
import { cpus } from 'node:os';
import cluster, { type Worker } from 'node:cluster';
import { type ArgDescription } from './IMQRPCDescription.js';
import { type IMQBeforeCall, type IMQAfterCall } from './IMQRPCOptions.js';
import {
type IMQBeforeCall,
type IMQAfterCall,
type IMQWrapCall,
} from './IMQRPCOptions.js';
import { runWithRequest } from './IMQRequestContext.js';
import { createServer, type Server } from 'node:http';

Expand Down Expand Up @@ -270,11 +274,24 @@ export abstract class IMQService {
}

try {
response.data = this[method].apply(this, args);
// Run the method through the optional `wrapCall` around-hook so a
// hook can execute it within its own scope (e.g. an OpenTelemetry
// context). Without a hook the method is invoked directly, exactly
// as before.
const invoke = async (): Promise<any> => {
const result = this[method].apply(this, args);

return result && result.then ? await result : result;
};

if (response.data && response.data.then) {
response.data = await response.data;
}
const wrapCall = this.options.wrapCall as
| IMQWrapCall<IMQService>
| undefined;

response.data =
typeof wrapCall === 'function'
? await wrapCall.call(this, request, response, invoke)
: await invoke();
} catch (err: any) {
response.error = IMQError(
err.code || 'IMQ_RPC_CALL_ERROR',
Expand Down
Loading