Skip to content

Commit

Permalink
fix: Add SystemServiceBus (#3234)
Browse files Browse the repository at this point in the history
- Add a concept of a SystemServiceBus
- Be able to register Subsystems
- Be able to register subsystem handlers
  • Loading branch information
Jason3S authored Jul 16, 2022
1 parent d3bed19 commit ebf666d
Show file tree
Hide file tree
Showing 12 changed files with 570 additions and 67 deletions.
3 changes: 3 additions & 0 deletions cspell.code-workspace
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@
{
"path": "packages/cspell-pipe"
},
{
"path": "packages/cspell-service-bus"
},
{
"path": "packages/cspell-tools"
},
Expand Down
104 changes: 104 additions & 0 deletions packages/cspell-service-bus/src/SystemServiceBus.test.ts
Original file line number Diff line number Diff line change
@@ -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<typeof TypeRequestFsReadFile, string> {
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<typeof TypeRequestZlibInflate, string> {
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<any>][] = 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);
});
});
172 changes: 172 additions & 0 deletions packages/cspell-service-bus/src/SystemServiceBus.ts
Original file line number Diff line number Diff line change
@@ -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<T extends ServiceRequest>(
requestDef: ServiceRequestFactory<T>,
fn: HandleRequestFn<T>,
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<R extends ServiceRequest<string, unknown>>(request: R): RequestResponseType<R> {
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<T extends ServiceRequest>(
requestDef: ServiceRequestFactory<T>,
fn: HandleRequestFn<T>,
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<RequestRegisterHandlerFactory>
) {
// 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);
}
Original file line number Diff line number Diff line change
@@ -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: <fs:>",
"fn": [Function],
"name": "Subsystem: File System",
},
"handlers": Array [
Object {
"description": "Matches against: <fs:>",
"fn": [Function],
"name": "Subsystem Register Handlers for File System",
},
],
"name": "File System",
"requestPattern": "fs:",
},
SubsystemServiceBusImpl {
"canHandleType": [Function],
"handler": Object {
"description": "Process Requests Matching: <zlib:>",
"fn": [Function],
"name": "Subsystem: ZLib",
},
"handlers": Array [
Object {
"description": "Matches against: <zlib:>",
"fn": [Function],
"name": "Subsystem Register Handlers for ZLib",
},
],
"name": "ZLib",
"requestPattern": "zlib:",
},
SubsystemServiceBusImpl {
"canHandleType": [Function],
"handler": Object {
"description": "Process Requests Matching: <path:>",
"fn": [Function],
"name": "Subsystem: Path",
},
"handlers": Array [
Object {
"description": "Matches against: <path:>",
"fn": [Function],
"name": "Subsystem Register Handlers for Path",
},
],
"name": "Path",
"requestPattern": "path:",
},
]
`;
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@

exports[`index API 1`] = `
Map {
"ServiceRequestAsync" => "function",
"ServiceRequestSync" => "function",
"ServiceRequest" => "function",
"ServiceBus" => "function",
"createServiceBus" => "function",
}
Expand Down
Loading

0 comments on commit ebf666d

Please sign in to comment.