diff --git a/cspell.code-workspace b/cspell.code-workspace index a53bbba3ecf..01e3a1e0496 100644 --- a/cspell.code-workspace +++ b/cspell.code-workspace @@ -40,6 +40,9 @@ { "path": "packages/cspell-pipe" }, + { + "path": "packages/cspell-service-bus" + }, { "path": "packages/cspell-tools" }, diff --git a/packages/cspell-service-bus/src/SystemServiceBus.test.ts b/packages/cspell-service-bus/src/SystemServiceBus.test.ts new file mode 100644 index 00000000000..35f833020d7 --- /dev/null +++ b/packages/cspell-service-bus/src/SystemServiceBus.test.ts @@ -0,0 +1,104 @@ +import { assert } from './assert'; +import { + createResponse, + createResponseFail, + isServiceResponseFailure, + isServiceResponseSuccess, + ServiceRequest, + ServiceRequestFactory, +} from './request'; +import { + createSystemServiceBus, + RequestCreateSubsystemFactory, + RequestRegisterHandlerFactory, +} from './SystemServiceBus'; + +const TypeRequestFsReadFile = 'fs:readFile' as const; +class RequestFsReadFile extends ServiceRequest { + static type = TypeRequestFsReadFile; + private constructor(readonly uri: string) { + super(TypeRequestFsReadFile); + } + static is(req: ServiceRequest): req is RequestFsReadFile { + return req instanceof RequestFsReadFile; + } + static create(uri: string) { + return new RequestFsReadFile(uri); + } +} + +const TypeRequestZlibInflate = 'zlib:inflate' as const; +class RequestZlibInflate extends ServiceRequest { + static type = TypeRequestZlibInflate; + private constructor(readonly data: string) { + super(TypeRequestZlibInflate); + } + static is(req: ServiceRequest): req is RequestZlibInflate { + return req instanceof RequestZlibInflate; + } + static create(data: string) { + return new RequestZlibInflate(data); + } +} + +const knownRequestTypes = { + [RequestRegisterHandlerFactory.type]: RequestRegisterHandlerFactory, + [RequestCreateSubsystemFactory.type]: RequestCreateSubsystemFactory, + [RequestFsReadFile.type]: RequestFsReadFile, + [RequestZlibInflate.type]: RequestZlibInflate, +}; + +describe('SystemServiceBus', () => { + test('createSystemServiceBus', () => { + const bus = createSystemServiceBus(); + bus.createSubsystem('File System', 'fs:'); + bus.createSubsystem('ZLib', 'zlib:'); + bus.createSubsystem('Path', 'path:'); + expect(bus.subsystems).toMatchSnapshot(); + }); + + test('ServiceRequestFactory Compliance', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const knownRequests: [string, ServiceRequestFactory][] = Object.entries(knownRequestTypes); + expect(knownRequests.map(([name, def]) => [name, def.type])).toMatchSnapshot(); + }); +}); + +describe('SystemServiceBus Behavior', () => { + const serviceBus = createSystemServiceBus(); + serviceBus.createSubsystem('File System', 'fs:'); + serviceBus.createSubsystem('ZLib', 'zlib:'); + serviceBus.createSubsystem('Path', 'path:'); + serviceBus.registerRequestHandler(RequestFsReadFile, (req) => createResponse(`read file: ${req.uri}`)); + serviceBus.registerRequestHandler(RequestFsReadFile, (req, next) => + /https?:/.test(req.uri) ? createResponse(`fetch http: ${req.uri}`) : next(req) + ); + serviceBus.registerRequestHandler( + RequestFsReadFile, + (req, next, dispatcher) => { + if (!req.uri.endsWith('.gz')) { + return next(req); + } + const fileRes = next(req); + if (!isServiceResponseSuccess(fileRes)) return fileRes; + const decompressRes = dispatcher.dispatch(RequestZlibInflate.create(fileRes.value)); + if (isServiceResponseFailure(decompressRes)) { + return createResponseFail(RequestFsReadFile, decompressRes.error); + } + assert(decompressRes.value); + return createResponse(decompressRes.value); + }, + RequestFsReadFile.type + '/zip' + ); + serviceBus.registerRequestHandler(RequestZlibInflate, (req) => createResponse(`Inflate: ${req.data}`)); + + test.each` + request | expected + ${RequestFsReadFile.create('file://my_file.txt')} | ${{ value: 'read file: file://my_file.txt' }} + ${RequestFsReadFile.create('https://www.example.com/my_file.txt')} | ${{ value: 'fetch http: https://www.example.com/my_file.txt' }} + ${RequestFsReadFile.create('https://www.example.com/my_dict.trie.gz')} | ${{ value: 'Inflate: fetch http: https://www.example.com/my_dict.trie.gz' }} + ${{ type: 'zlib:compress' }} | ${{ error: Error('Unhandled Request: zlib:compress') }} + `('dispatch requests', ({ request, expected }) => { + expect(serviceBus.dispatch(request)).toEqual(expected); + }); +}); diff --git a/packages/cspell-service-bus/src/SystemServiceBus.ts b/packages/cspell-service-bus/src/SystemServiceBus.ts new file mode 100644 index 00000000000..0dc249e73ee --- /dev/null +++ b/packages/cspell-service-bus/src/SystemServiceBus.ts @@ -0,0 +1,172 @@ +import { assert } from './assert'; +import { + createRequestHandler, + createServiceBus, + Dispatcher, + Handler, + HandleRequestFn, + HandleRequestKnown, + HandlerNext, + ServiceBus, +} from './bus'; +import { createResponse, RequestResponseType, ServiceRequest, ServiceRequestFactory } from './request'; + +export interface SystemServiceBus extends Dispatcher { + registerHandler(requestPrefix: string, handler: Handler): void; + registerRequestHandler( + requestDef: ServiceRequestFactory, + fn: HandleRequestFn, + name?: string | undefined, + description?: string | undefined + ): void; + createSubsystem(name: string, requestPattern: string | RegExp): SubsystemServiceBus; + readonly subsystems: SubsystemServiceBus[]; +} + +class SystemServiceBusImpl implements SystemServiceBus { + private serviceBus: ServiceBus; + private _subsystems: SubsystemServiceBus[]; + constructor() { + this.serviceBus = createServiceBus(); + this._subsystems = []; + this.bindDefaultHandlers(); + this.createSubsystem('Default Subsystem', '' /* match everything */); + } + + private bindDefaultHandlers() { + this.serviceBus.addHandler( + createRequestHandler(RequestCreateSubsystemFactory, (req) => { + const { name, requestPattern } = req; + const sub = createSubsystemServiceBus(name, requestPattern); + this._subsystems.push(sub); + this.serviceBus.addHandler(sub.handler); + return createResponse(sub); + }) + ); + } + + dispatch>(request: R): RequestResponseType { + return this.serviceBus.dispatch(request); + } + + createSubsystem(name: string, requestPattern: string | RegExp): SubsystemServiceBus { + const res = this.dispatch(RequestCreateSubsystemFactory.create(name, requestPattern)); + assert(res?.value); + return res.value; + } + + registerHandler(requestPrefix: string, handler: Handler): void { + const request = RequestRegisterHandlerFactory.create(requestPrefix, handler); + this.serviceBus.dispatch(request); + } + + registerRequestHandler( + requestDef: ServiceRequestFactory, + fn: HandleRequestFn, + name?: string | undefined, + description?: string | undefined + ): void { + this.registerHandler(requestDef.type, createRequestHandler(requestDef, fn, name, description)); + } + + get subsystems() { + return [...this._subsystems]; + } +} + +export function createSystemServiceBus(): SystemServiceBus { + return new SystemServiceBusImpl(); +} + +const TypeRequestRegisterHandler = 'System:RegisterHandler' as const; +export class RequestRegisterHandlerFactory extends ServiceRequest< + typeof TypeRequestRegisterHandler, + SubsystemServiceBus +> { + static type = TypeRequestRegisterHandler; + private constructor(readonly requestPrefix: string, readonly handler: Handler) { + super(RequestRegisterHandlerFactory.type); + } + static is(req: ServiceRequest): req is RequestRegisterHandlerFactory { + return req instanceof RequestRegisterHandlerFactory; + } + static create(requestPrefix: string, handler: Handler) { + return new RequestRegisterHandlerFactory(requestPrefix, handler); + } +} + +const TypeRequestCreateSubsystem = 'System:CreateSubsystem' as const; +export class RequestCreateSubsystemFactory extends ServiceRequest< + typeof TypeRequestCreateSubsystem, + SubsystemServiceBus +> { + static type = TypeRequestCreateSubsystem; + private constructor(readonly name: string, readonly requestPattern: string | RegExp) { + super(RequestCreateSubsystemFactory.type); + } + static is(req: ServiceRequest): req is RequestCreateSubsystemFactory { + return req instanceof RequestCreateSubsystemFactory; + } + static create(name: string, requestPattern: string | RegExp) { + return new RequestCreateSubsystemFactory(name, requestPattern); + } +} + +interface SubsystemServiceBus extends Dispatcher { + readonly name: string; + readonly requestPattern: string | RegExp; + readonly handler: Handler; +} + +class SubsystemServiceBusImpl extends ServiceBus implements SubsystemServiceBus { + readonly handler: Handler; + private canHandleType: (requestType: string) => boolean; + constructor(readonly name: string, readonly requestPattern: string | RegExp) { + super(); + + this.canHandleType = + typeof requestPattern === 'string' + ? (reqType) => reqType.startsWith(requestPattern) + : (reqType) => requestPattern.test(reqType); + + const handleRegistration = createRequestHandler( + RequestRegisterHandlerFactory, + (req, next) => this.handleRegistrationReq(req, next), + 'Subsystem Register Handlers for ' + name, + `Matches against: <${requestPattern.toString()}>` + ); + + this.addHandler(handleRegistration); + this.handler = { + name: 'Subsystem: ' + name, + description: `Process Requests Matching: <${requestPattern.toString()}>`, + fn: (dispatcher) => this._handler(dispatcher), + }; + } + + handleRegistrationReq( + request: RequestRegisterHandlerFactory, + next: HandleRequestKnown + ) { + // console.log(`${this.name}.handleRegistrationReq %o`, request); + if (!this.canHandleType(request.requestPrefix)) { + // console.log(`${this.name}.handleRegistrationReq skip`); + return next(request); + } + // console.log(`${this.name}.handleRegistrationReq add ***`); + this.addHandler(request.handler); + return createResponse(this); + } + + _handler(dispatcher: Dispatcher): HandlerNext { + return (next) => (req) => { + if (!this.canHandleType(req.type) && !RequestRegisterHandlerFactory.is(req)) return next(req); + const dispatch = this.reduceHandlers(this.handlers, req, dispatcher, next); + return dispatch(req); + }; + } +} + +export function createSubsystemServiceBus(name: string, requestPattern: string | RegExp): SubsystemServiceBus { + return new SubsystemServiceBusImpl(name, requestPattern); +} diff --git a/packages/cspell-service-bus/src/__snapshots__/SystemServiceBus.test.ts.snap b/packages/cspell-service-bus/src/__snapshots__/SystemServiceBus.test.ts.snap new file mode 100644 index 00000000000..9c394a2a11b --- /dev/null +++ b/packages/cspell-service-bus/src/__snapshots__/SystemServiceBus.test.ts.snap @@ -0,0 +1,95 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SystemServiceBus ServiceRequestFactory Compliance 1`] = ` +Array [ + Array [ + "System:RegisterHandler", + "System:RegisterHandler", + ], + Array [ + "System:CreateSubsystem", + "System:CreateSubsystem", + ], + Array [ + "fs:readFile", + "fs:readFile", + ], + Array [ + "zlib:inflate", + "zlib:inflate", + ], +] +`; + +exports[`SystemServiceBus createSystemServiceBus 1`] = ` +Array [ + SubsystemServiceBusImpl { + "canHandleType": [Function], + "handler": Object { + "description": "Process Requests Matching: <>", + "fn": [Function], + "name": "Subsystem: Default Subsystem", + }, + "handlers": Array [ + Object { + "description": "Matches against: <>", + "fn": [Function], + "name": "Subsystem Register Handlers for Default Subsystem", + }, + ], + "name": "Default Subsystem", + "requestPattern": "", + }, + SubsystemServiceBusImpl { + "canHandleType": [Function], + "handler": Object { + "description": "Process Requests Matching: ", + "fn": [Function], + "name": "Subsystem: File System", + }, + "handlers": Array [ + Object { + "description": "Matches against: ", + "fn": [Function], + "name": "Subsystem Register Handlers for File System", + }, + ], + "name": "File System", + "requestPattern": "fs:", + }, + SubsystemServiceBusImpl { + "canHandleType": [Function], + "handler": Object { + "description": "Process Requests Matching: ", + "fn": [Function], + "name": "Subsystem: ZLib", + }, + "handlers": Array [ + Object { + "description": "Matches against: ", + "fn": [Function], + "name": "Subsystem Register Handlers for ZLib", + }, + ], + "name": "ZLib", + "requestPattern": "zlib:", + }, + SubsystemServiceBusImpl { + "canHandleType": [Function], + "handler": Object { + "description": "Process Requests Matching: ", + "fn": [Function], + "name": "Subsystem: Path", + }, + "handlers": Array [ + Object { + "description": "Matches against: ", + "fn": [Function], + "name": "Subsystem Register Handlers for Path", + }, + ], + "name": "Path", + "requestPattern": "path:", + }, +] +`; diff --git a/packages/cspell-service-bus/src/__snapshots__/index.test.ts.snap b/packages/cspell-service-bus/src/__snapshots__/index.test.ts.snap index ebfa7ba2926..28a31446b94 100644 --- a/packages/cspell-service-bus/src/__snapshots__/index.test.ts.snap +++ b/packages/cspell-service-bus/src/__snapshots__/index.test.ts.snap @@ -2,8 +2,7 @@ exports[`index API 1`] = ` Map { - "ServiceRequestAsync" => "function", - "ServiceRequestSync" => "function", + "ServiceRequest" => "function", "ServiceBus" => "function", "createServiceBus" => "function", } diff --git a/packages/cspell-service-bus/src/assert.test.ts b/packages/cspell-service-bus/src/assert.test.ts new file mode 100644 index 00000000000..f11297f71e7 --- /dev/null +++ b/packages/cspell-service-bus/src/assert.test.ts @@ -0,0 +1,27 @@ +import { assert } from './assert'; + +function catchError(fn: () => T): Error | T { + try { + return fn(); + } catch (err) { + return err as Error; + } +} + +describe('assert', () => { + test.each` + value | message | expected + ${true} | ${undefined} | ${undefined} + ${1} | ${undefined} | ${undefined} + ${'yes'} | ${undefined} | ${undefined} + ${undefined} | ${undefined} | ${Error('AssertionError')} + ${0} | ${undefined} | ${Error('AssertionError')} + ${null} | ${undefined} | ${Error('AssertionError')} + ${''} | ${undefined} | ${Error('AssertionError')} + ${false} | ${undefined} | ${Error('AssertionError')} + ${false} | ${'Must be true or fail'} | ${Error('Must be true or fail')} + ${false} | ${Error('my error')} | ${Error('my error')} + `('compare assert to node assert $value / $message', ({ value, message, expected }) => { + expect(catchError(() => assert(value, message))).toEqual(expected); + }); +}); diff --git a/packages/cspell-service-bus/src/assert.ts b/packages/cspell-service-bus/src/assert.ts new file mode 100644 index 00000000000..981c5c5a379 --- /dev/null +++ b/packages/cspell-service-bus/src/assert.ts @@ -0,0 +1,6 @@ +export function assert(value: unknown, message?: string | Error): asserts value { + if (!value) { + const err = message instanceof Error ? message : Error(message ?? 'AssertionError'); + throw err; + } +} diff --git a/packages/cspell-service-bus/src/bus.test.ts b/packages/cspell-service-bus/src/bus.test.ts index be2c0a01298..3b8d2e20962 100644 --- a/packages/cspell-service-bus/src/bus.test.ts +++ b/packages/cspell-service-bus/src/bus.test.ts @@ -1,5 +1,5 @@ -import { createRequestHandler, createServiceBus, Dispatcher, Handler } from './bus'; -import { createResponse as response, ServiceRequest, ServiceRequestSync, ServiceResponse } from './request'; +import { createIsRequestHandler, createRequestHandler, createServiceBus, Dispatcher, Handler } from './bus'; +import { createResponse as response, ServiceRequest, ServiceResponse } from './request'; function calcFib(request: FibRequest): ServiceResponse { let a = 0, @@ -17,16 +17,21 @@ function calcFib(request: FibRequest): ServiceResponse { }; } -class FibRequest extends ServiceRequestSync<'calc-fib', number> { - constructor(readonly fib: number) { - super('calc-fib'); +const TypeRequestFib = 'Computations:calc-fib' as const; +class FibRequest extends ServiceRequest { + static type = TypeRequestFib; + private constructor(readonly fib: number) { + super(TypeRequestFib); } static is(req: ServiceRequest): req is FibRequest { return req instanceof FibRequest; } + static create(fib: number) { + return new FibRequest(fib); + } } -class StringLengthRequest extends ServiceRequestSync<'calc-string-length', number> { +class StringLengthRequest extends ServiceRequest<'calc-string-length', number> { constructor(readonly str: string) { super('calc-string-length'); } @@ -35,7 +40,7 @@ class StringLengthRequest extends ServiceRequestSync<'calc-string-length', numbe } } -class StringToUpperRequest extends ServiceRequestSync<'toUpper', string> { +class StringToUpperRequest extends ServiceRequest<'toUpper', string> { constructor(readonly str: string) { super('toUpper'); } @@ -44,13 +49,13 @@ class StringToUpperRequest extends ServiceRequestSync<'toUpper', string> { } } -class DoNotHandleRequest extends ServiceRequestSync<'Do Not Handle', undefined> { +class DoNotHandleRequest extends ServiceRequest<'Do Not Handle', undefined> { constructor() { super('Do Not Handle'); } } -class RetryAgainRequest extends ServiceRequestSync<'Retry Again Request', undefined> { +class RetryAgainRequest extends ServiceRequest<'Retry Again Request', undefined> { constructor() { super('Retry Again Request'); } @@ -59,25 +64,33 @@ class RetryAgainRequest extends ServiceRequestSync<'Retry Again Request', undefi } } -const handlerStringLengthRequest = createRequestHandler(StringLengthRequest.is, (r) => response(r.str.length)); -const handlerStringToUpperRequest = createRequestHandler(StringToUpperRequest.is, (r) => - response(r.str.toLocaleUpperCase()) +const handlerStringLengthRequest = createIsRequestHandler( + StringLengthRequest.is, + (r) => response(r.str.length), + 'handlerStringLengthRequest' ); -const handlerRetryAgainRequest: Handler = (service: Dispatcher) => (next) => (request) => - RetryAgainRequest.is(request) ? service.dispatch(request) : next(request); - +const handlerStringToUpperRequest = createIsRequestHandler( + StringToUpperRequest.is, + (r) => response(r.str.toLocaleUpperCase()), + 'handlerStringToUpperRequest' +); +const handlerRetryAgainRequest: Handler = { + fn: (service: Dispatcher) => (next) => (request) => + RetryAgainRequest.is(request) ? service.dispatch(request) : next(request), + name: 'handlerRetryAgainRequest', +}; describe('Service Bus', () => { const bus = createServiceBus(); - bus.addHandler(createRequestHandler(FibRequest.is, calcFib)); + bus.addHandler(createRequestHandler(FibRequest, calcFib)); bus.addHandler(handlerStringLengthRequest); bus.addHandler(handlerStringToUpperRequest); bus.addHandler(handlerRetryAgainRequest); test.each` request | expected - ${new FibRequest(6)} | ${response(8)} - ${new FibRequest(5)} | ${response(5)} - ${new FibRequest(7)} | ${response(13)} + ${FibRequest.create(6)} | ${response(8)} + ${FibRequest.create(5)} | ${response(5)} + ${FibRequest.create(7)} | ${response(13)} ${new StringLengthRequest('hello')} | ${response(5)} ${new StringToUpperRequest('hello')} | ${response('HELLO')} ${new DoNotHandleRequest()} | ${{ error: Error('Unhandled Request: Do Not Handle') }} diff --git a/packages/cspell-service-bus/src/bus.ts b/packages/cspell-service-bus/src/bus.ts index 623a142768a..3746812fcc4 100644 --- a/packages/cspell-service-bus/src/bus.ts +++ b/packages/cspell-service-bus/src/bus.ts @@ -1,65 +1,136 @@ -import { createResponseFail, IsARequest, RequestResponseType, ServiceRequest } from './request'; +import { createResponseFail, IsARequest, RequestResponseType, ServiceRequest, ServiceRequestFactory } from './request'; export interface Dispatcher { - dispatch(request: R): R['__r']; + dispatch(request: R): RequestResponseType; } const MAX_DEPTH = 10; export class ServiceBus implements Dispatcher { - constructor(readonly handlers: Handler[]) {} + readonly handlers: Handler[] = []; + constructor(handlers: Handler[] = []) { + handlers.forEach((h) => this.addHandler(h)); + } - addHandler(handler: Handler): void { - this.handlers.push(handler); + addHandler(handler: HandlerFn, name: string, description?: string): void; + addHandler(handler: Handler): void; + addHandler(handler: HandlerFn | Handler, name = 'anonymous', description?: string): void { + const h = typeof handler === 'function' ? { fn: handler, name, description } : handler; + const { fn, name: _name, description: _description } = h; + this.handlers.push({ fn, name: _name, description: _description }); } dispatch(request: R): RequestResponseType { - type RR = RequestResponseType; let depth = 0; - const dispatcher: Dispatcher = { - dispatch, - }; - const unhandledHandler = (request: ServiceRequest): RR => { - return createResponseFail(request, new ErrorUnhandledRequest(request)) as RR; - }; - const handlers = this.handlers.reverse().map((m) => m(dispatcher)); - - function dispatch(request: R): RR { + const dispatcher: Dispatcher = { dispatch }; + const handler = this.reduceHandlers(this.handlers, request, dispatcher, this.defaultHandler); + + function dispatch(request: R): RequestResponseType { + type RR = R extends { __r?: infer R } ? R : never; ++depth; if (depth >= MAX_DEPTH) { return createResponseFail(request, new ErrorServiceRequestDepthExceeded(request, depth)) as RR; } - const defaultHandler: HandleRequest = unhandledHandler; - const handler = handlers.reduce((next, h) => h(next), defaultHandler); const response = handler(request) as RR; --depth; return response; } return dispatch(request); } + + defaultHandler(request: ServiceRequest) { + return createResponseFail(request, new ErrorUnhandledRequest(request)); + } + + protected reduceHandlers( + handlers: readonly Handler[], + request: R, + dispatcher: Dispatcher, + defaultHandler: HandleRequest + ) { + const _handlers = handlers.map((m) => ({ ...m, fn: m.fn(dispatcher) })); + const handler = _handlers.reduce((next, h) => { + const fn = h.fn(next); + return (req) => { + try { + return fn(req); + } catch (e) { + return createResponseFail(request, new UnhandledHandlerError(h.name, h.description, e)); + } + }; + }, defaultHandler); + return handler; + } } export function createServiceBus(handlers: Handler[] = []): ServiceBus { return new ServiceBus(handlers); } -export type HandleRequestFn = (request: R) => RequestResponseType; +export type HandleRequestFn = ( + request: R, + next: HandleRequestKnown, + dispatch: Dispatcher +) => RequestResponseType; export interface HandleRequest { // eslint-disable-next-line @typescript-eslint/no-explicit-any - (request: R): RequestResponseType; + (request: R): any; } -interface HandlerNext { +export interface HandleRequestKnown { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (request: R): RequestResponseType; +} + +export interface HandlerNext { (next: HandleRequest): HandleRequest; } -export interface Handler { +export interface HandlerFn { (dispatcher: Dispatcher): HandlerNext; } -export function createRequestHandler(isA: IsARequest, fn: HandleRequestFn): Handler { - return (_service) => (next) => (request) => isA(request) ? fn(request) : next(request); +export interface Handler { + /** + * Name of the Handler. + * Useful for debugging and uncaught exceptions. + */ + readonly name: string; + /** + * Optional description of the Handler. + */ + readonly description?: string | undefined; + readonly fn: HandlerFn; +} + +export function createIsRequestHandlerFn( + isA: IsARequest, + fn: HandleRequestFn +): HandlerFn { + return (dispatcher) => (next) => (request) => isA(request) ? fn(request, next, dispatcher) : next(request); +} + +export function createIsRequestHandler( + isA: IsARequest, + fn: HandleRequestFn, + name: string, + description?: string +): Handler { + return { + fn: createIsRequestHandlerFn(isA, fn), + name, + description, + }; +} + +export function createRequestHandler( + requestDef: ServiceRequestFactory, + fn: HandleRequestFn, + name?: string, + description?: string +): Handler { + return createIsRequestHandler(requestDef.is, fn, name ?? requestDef.type, description); } export class ErrorUnhandledRequest extends Error { @@ -73,3 +144,13 @@ export class ErrorServiceRequestDepthExceeded extends Error { super(`Service Request Depth ${depth} Exceeded: ${request.type}`); } } + +export class UnhandledHandlerError extends Error { + constructor( + readonly handlerName: string, + readonly handlerDescription: string | undefined, + readonly cause: unknown + ) { + super(`Unhandled Error in Handler: ${handlerName}`); + } +} diff --git a/packages/cspell-service-bus/src/index.ts b/packages/cspell-service-bus/src/index.ts index e25a9c2439e..5744d8440b0 100644 --- a/packages/cspell-service-bus/src/index.ts +++ b/packages/cspell-service-bus/src/index.ts @@ -1,2 +1,2 @@ -export { ServiceRequest, ServiceRequestAsync, ServiceRequestSync } from './request'; +export { ServiceRequest } from './request'; export { ServiceBus, createServiceBus } from './bus'; diff --git a/packages/cspell-service-bus/src/request.test.ts b/packages/cspell-service-bus/src/request.test.ts index eb1dfbb4f69..9d00549c5ff 100644 --- a/packages/cspell-service-bus/src/request.test.ts +++ b/packages/cspell-service-bus/src/request.test.ts @@ -2,11 +2,12 @@ import { isServiceResponseFailure, isServiceResponseSuccess, isInstanceOfFn, - ServiceRequestAsync, - ServiceRequestSync, - BaseServiceRequest, + ServiceRequest, + __testing__, } from './request'; +const { BaseServiceRequest } = __testing__; + describe('request', () => { test.each` response | expected @@ -30,14 +31,10 @@ describe('request', () => { }); test.each` - request | kind | expected - ${new ServiceRequestAsync('ServiceRequestAsync')} | ${BaseServiceRequest} | ${true} - ${new ServiceRequestSync('ServiceRequestSync')} | ${BaseServiceRequest} | ${true} - ${new ServiceRequestAsync('ServiceRequestAsync')} | ${ServiceRequestAsync} | ${true} - ${new ServiceRequestSync('ServiceRequestSync')} | ${ServiceRequestSync} | ${true} - ${new ServiceRequestAsync('ServiceRequestAsync')} | ${ServiceRequestSync} | ${false} - ${new ServiceRequestSync('ServiceRequestSync')} | ${ServiceRequestAsync} | ${false} - ${{ type: 'static' }} | ${BaseServiceRequest} | ${false} + request | kind | expected + ${new ServiceRequest('ServiceRequestSync')} | ${BaseServiceRequest} | ${true} + ${new ServiceRequest('ServiceRequestSync')} | ${ServiceRequest} | ${true} + ${{ type: 'static' }} | ${BaseServiceRequest} | ${false} `('isInstanceOfFn $request.type', ({ request, kind, expected }) => { const fn = isInstanceOfFn(kind); expect(fn(request)).toEqual(expected); diff --git a/packages/cspell-service-bus/src/request.ts b/packages/cspell-service-bus/src/request.ts index da686fda77b..67587f0c3e3 100644 --- a/packages/cspell-service-bus/src/request.ts +++ b/packages/cspell-service-bus/src/request.ts @@ -1,28 +1,23 @@ -export interface ServiceRequest { +export interface ServiceRequest { readonly type: T; __r?: ServiceResponseBase; } -export class BaseServiceRequest implements ServiceRequest { +class BaseServiceRequest implements ServiceRequest { readonly __r?: ServiceResponseBase; constructor(readonly type: T) {} } -export class ServiceRequestSync extends BaseServiceRequest { - readonly sync = true; - constructor(type: T) { - super(type); - } -} - -export class ServiceRequestAsync extends BaseServiceRequest { - constructor(type: T) { +export class ServiceRequest extends BaseServiceRequest { + constructor(readonly type: T) { super(type); } } interface ServiceResponseBase { ___T?: T; + value?: T; + error?: Error | undefined; } export interface ServiceResponseSuccess extends ServiceResponseBase { @@ -64,3 +59,14 @@ export function isServiceResponseFailure(res: ServiceResponseBase): res is export function isInstanceOfFn(constructor: { new (): T }): (t: unknown) => t is T { return (t): t is T => t instanceof constructor; } + +export interface ServiceRequestFactory { + type: T; + is: (r: ServiceRequest | R) => r is R; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + create(...params: any[]): R; +} + +export const __testing__ = { + BaseServiceRequest, +};