diff --git a/changelog.md b/changelog.md index 906dc4a..d53bfa2 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,16 @@ # Changelog +## 2025-12-04 - 3.2.0 - feat(typedrouter) +Add request/response hooks and monitoring to TypedRouter; emit hooks from TypedRequest; improve VirtualStream encoding/decoding; re-export hook types + +- Introduce ITypedRequestLogEntry and ITypedRouterHooks interfaces to represent structured traffic log entries and hook callbacks. +- Add static globalHooks on TypedRouter with helper APIs TypedRouter.setGlobalHooks and TypedRouter.clearGlobalHooks for global traffic monitoring. +- Add instance-level hooks on TypedRouter (setHooks) and a unified callHook() that invokes both global and instance hooks safely (errors are caught and logged). +- TypedRequest now emits onOutgoingRequest before sending and onIncomingResponse after receiving, including timestamps, duration and payload/error details. +- TypedRouter now emits lifecycle hooks while routing: onIncomingRequest when a request arrives, onOutgoingResponse for responses (both success and handler-missing error cases), and onIncomingResponse when responses arrive to be fulfilled. +- VirtualStream.encodePayloadForNetwork and decodePayloadFromNetwork were enhanced to recurse into arrays and nested objects (preserving special built-ins) to correctly handle embedded virtual streams. +- Re-export ITypedRequestLogEntry and ITypedRouterHooks from the package index for external consumption. + ## 2025-12-03 - 3.1.11 - fix(virtualstream) Expose transport localData to handlers via TypedTools; improve VirtualStream payload encode/decode to preserve built-ins and handle nested arrays/objects diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index e811f8f..40d6423 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@api.global/typedrequest', - version: '3.1.11', + version: '3.2.0', description: 'A TypeScript library for making typed requests towards APIs, including facilities for handling requests, routing, and virtual stream handling.' } diff --git a/ts/classes.typedrequest.ts b/ts/classes.typedrequest.ts index 501015b..7c132c2 100644 --- a/ts/classes.typedrequest.ts +++ b/ts/classes.typedrequest.ts @@ -1,11 +1,25 @@ import * as plugins from './plugins.js'; import { VirtualStream } from './classes.virtualstream.js'; import { TypedResponseError } from './classes.typedresponseerror.js'; -import { TypedRouter } from './classes.typedrouter.js'; +import { TypedRouter, type ITypedRequestLogEntry } from './classes.typedrouter.js'; import { TypedTarget } from './classes.typedtarget.js'; const webrequestInstance = new plugins.webrequest.WebRequest(); +/** + * Helper to call global hooks from TypedRequest + */ +function callGlobalHook( + hookName: keyof typeof TypedRouter.globalHooks, + entry: ITypedRequestLogEntry +): void { + try { + TypedRouter.globalHooks[hookName]?.(entry); + } catch (err) { + console.error(`TypedRequest hook error (${hookName}):`, err); + } +} + export class TypedRequest { /** * in case we post against a url endpoint @@ -36,6 +50,8 @@ export class TypedRequest { + const requestStartTime = Date.now(); + let payloadSending: plugins.typedRequestInterfaces.ITypedRequest = { method: this.method, request: fireArg, @@ -53,6 +69,16 @@ export class TypedRequest; } }); + + // Hook: incoming response (for this outgoing request) + callGlobalHook('onIncomingResponse', { + correlationId: payloadSending.correlation.id, + method: this.method, + direction: 'incoming', + phase: 'response', + timestamp: Date.now(), + durationMs: Date.now() - requestStartTime, + payload: payloadReceiving?.response, + error: payloadReceiving?.error?.text, + }); + return payloadReceiving.response; } diff --git a/ts/classes.typedrouter.ts b/ts/classes.typedrouter.ts index 9e379e4..827d6d0 100644 --- a/ts/classes.typedrouter.ts +++ b/ts/classes.typedrouter.ts @@ -4,12 +4,80 @@ import { VirtualStream } from './classes.virtualstream.js'; import { TypedHandler } from './classes.typedhandler.js'; import { TypedRequest } from './classes.typedrequest.js'; +/** + * Log entry for TypedRequest traffic monitoring + */ +export interface ITypedRequestLogEntry { + correlationId: string; + method: string; + direction: 'outgoing' | 'incoming'; + phase: 'request' | 'response'; + timestamp: number; + durationMs?: number; + payload: any; + error?: string; +} + +/** + * Hooks for intercepting TypedRequest traffic + */ +export interface ITypedRouterHooks { + onOutgoingRequest?: (entry: ITypedRequestLogEntry) => void; + onIncomingResponse?: (entry: ITypedRequestLogEntry) => void; + onIncomingRequest?: (entry: ITypedRequestLogEntry) => void; + onOutgoingResponse?: (entry: ITypedRequestLogEntry) => void; +} + /** * A typed router decides on which typed handler to call based on the method * specified in the typed request * This is thought for reusing the same url endpoint for different methods */ export class TypedRouter { + // Static hooks for global traffic monitoring + public static globalHooks: ITypedRouterHooks = {}; + + /** + * Set global hooks for monitoring all TypedRequest traffic + */ + public static setGlobalHooks(hooks: ITypedRouterHooks): void { + TypedRouter.globalHooks = { ...TypedRouter.globalHooks, ...hooks }; + } + + /** + * Clear all global hooks + */ + public static clearGlobalHooks(): void { + TypedRouter.globalHooks = {}; + } + + // Instance-level hooks (for per-router monitoring) + public hooks: ITypedRouterHooks = {}; + + /** + * Set instance-level hooks for monitoring traffic through this router + */ + public setHooks(hooks: ITypedRouterHooks): void { + this.hooks = { ...this.hooks, ...hooks }; + } + + /** + * Helper to call both global and instance hooks + */ + private callHook( + hookName: keyof ITypedRouterHooks, + entry: ITypedRequestLogEntry + ): void { + try { + // Call global hooks + TypedRouter.globalHooks[hookName]?.(entry); + // Call instance hooks + this.hooks[hookName]?.(entry); + } catch (err) { + console.error(`TypedRouter hook error (${hookName}):`, err); + } + } + public routerMap = new plugins.lik.ObjectMap(); public handlerMap = new plugins.lik.ObjectMap< TypedHandler @@ -109,6 +177,18 @@ export class TypedRouter { // lets do normal routing if (typedRequestArg?.correlation?.phase === 'request' || localRequestArg) { + const requestStartTime = Date.now(); + + // Hook: incoming request + this.callHook('onIncomingRequest', { + correlationId: typedRequestArg.correlation?.id || 'unknown', + method: typedRequestArg.method, + direction: 'incoming', + phase: 'request', + timestamp: requestStartTime, + payload: typedRequestArg.request, + }); + const typedHandler = this.getTypedHandlerForMethod(typedRequestArg.method); if (!typedHandler) { @@ -124,6 +204,19 @@ export class TypedRouter { typedRequestArg = VirtualStream.encodePayloadForNetwork(typedRequestArg, { typedrouter: this, }); + + // Hook: outgoing response (error - no handler) + this.callHook('onOutgoingResponse', { + correlationId: typedRequestArg.correlation?.id || 'unknown', + method: typedRequestArg.method, + direction: 'outgoing', + phase: 'response', + timestamp: Date.now(), + durationMs: Date.now() - requestStartTime, + payload: typedRequestArg.response, + error: typedRequestArg.error?.text, + }); + return typedRequestArg; } @@ -133,8 +226,32 @@ export class TypedRouter { typedRequestArg = VirtualStream.encodePayloadForNetwork(typedRequestArg, { typedrouter: this, }); + + // Hook: outgoing response (success) + this.callHook('onOutgoingResponse', { + correlationId: typedRequestArg.correlation?.id || 'unknown', + method: typedRequestArg.method, + direction: 'outgoing', + phase: 'response', + timestamp: Date.now(), + durationMs: Date.now() - requestStartTime, + payload: typedRequestArg.response, + error: typedRequestArg.error?.text, + }); + return typedRequestArg; } else if (typedRequestArg?.correlation?.phase === 'response') { + // Hook: incoming response + this.callHook('onIncomingResponse', { + correlationId: typedRequestArg.correlation?.id || 'unknown', + method: typedRequestArg.method, + direction: 'incoming', + phase: 'response', + timestamp: Date.now(), + payload: typedRequestArg.response, + error: typedRequestArg.error?.text, + }); + this.fireEventInterestMap .findInterest(typedRequestArg.correlation.id) ?.fullfillInterest(typedRequestArg); diff --git a/ts/index.ts b/ts/index.ts index a2f876a..6ab312c 100644 --- a/ts/index.ts +++ b/ts/index.ts @@ -3,4 +3,7 @@ export * from './classes.typedhandler.js'; export * from './classes.typedrouter.js'; export * from './classes.typedresponseerror.js'; export * from './classes.typedtarget.js'; -export * from './classes.virtualstream.js'; \ No newline at end of file +export * from './classes.virtualstream.js'; + +// Re-export hook interfaces from typedrouter +export type { ITypedRequestLogEntry, ITypedRouterHooks } from './classes.typedrouter.js'; \ No newline at end of file