airdcpp-apisocket
Version:
Javascript connector for AirDC++ Web API
307 lines (252 loc) • 8.49 kB
text/typescript
import { Client, Server, WebSocket } from 'mock-socket';
import { OutgoingRequest, RequestSuccessResponse, RequestErrorResponse } from '../../types/api_internal.js';
import { EventEmitter } from 'events';
import { MOCK_SERVER_URL } from './mock-data.js';
import Logger from '../../SocketLogger.js';
import { LoggerOptions } from '../../NodeSocket.js';
interface MockFunctionCreator {
fn: (...args: any[]) => any;
};
type RequestCallback = (requestData: object) => void;
const toEmitId = (path: string, method: string) => {
return `${path}_${method}`;
};
const getDefaultMockCreatorF = () => ({
fn: () => {},
});
type DelayF = () => number;
export interface MockServerOptions {
url: string;
reportMissingListeners?: boolean;
mockF: MockFunctionCreator;
delayMs: number | DelayF;
loggerOptions: LoggerOptions;
}
const DEFAULT_MOCK_SERVER_OPTIONS: MockServerOptions = {
url: MOCK_SERVER_URL,
reportMissingListeners: true,
mockF: getDefaultMockCreatorF(),
delayMs: 0,
loggerOptions: {
logLevel: 'warn',
logOutput: console,
}
}
type MockRequestResponseDataObject<DataT extends object | undefined> = Omit<RequestSuccessResponse<DataT>, 'callback_id'> | Omit<RequestErrorResponse, 'callback_id'>;
type MockRequestResponseDataHandler<DataT extends object | undefined> = (request: OutgoingRequest, s: WebSocket) => MockRequestResponseDataObject<DataT>;
type MockRequestResponseData<DataT extends object | undefined> = MockRequestResponseDataObject<DataT> | MockRequestResponseDataHandler<DataT>;
const getMockServer = (initialOptions: Partial<MockServerOptions> = {}) => {
const { url, reportMissingListeners, mockF, delayMs, loggerOptions }: MockServerOptions = {
...DEFAULT_MOCK_SERVER_OPTIONS,
...initialOptions,
};
const logger = Logger(loggerOptions);
const mockServer = new Server(url);
let socket: Client;
const emitter = new EventEmitter();
emitter.setMaxListeners(1);
const send = (data: object) => {
socket.send(JSON.stringify(data));
};
const handlers: Map<string, () => any> = new Map();
const addServerHandler = <DataT extends object | undefined>(
method: string,
path: string,
responseData: MockRequestResponseData<DataT>,
subscriptionCallback?: RequestCallback,
) => {
const requestHandler = (request: OutgoingRequest, s: WebSocket) => {
const data = typeof responseData === 'function' ? responseData(request, s) : responseData;
if (!data ||!data.code) {
throw new Error(`Mock server: response handler for path ${path} must return a status code`);
}
const response: RequestSuccessResponse | RequestErrorResponse = {
callback_id: request.callback_id,
...data,
};
const delay = typeof delayMs === 'function' ? delayMs() : delayMs;
setTimeout(() => {
logger.verbose(`Mock server: sending response for request ${request.callback_id} (${method} ${path}):`, data);
if (subscriptionCallback) {
subscriptionCallback(request);
}
s.send(JSON.stringify(response));
}, delay);
};
// Don't add duplicates
const emitId = toEmitId(path, method);
const removeExisting = handlers.get(emitId);
if (removeExisting) {
removeExisting();
handlers.delete(emitId);
}
// Add new
emitter.addListener(emitId, requestHandler);
// Prepare for removal
const removeListener = () => emitter.removeListener(emitId, requestHandler);
handlers.set(emitId, removeListener)
return removeListener;
};
const addDummyDataHandler = (method: string, path: string) => {
const handler = (request: OutgoingRequest, s: WebSocket) => {
// Do nothing
};
const emitId = toEmitId(path, method);
emitter.addListener(
emitId,
handler
);
emitter.setMaxListeners(1);
return () => emitter.removeListener(emitId, handler);
}
const addRequestHandler = <DataT extends object | undefined>(
method: string,
path: string,
data?: DataT | MockRequestResponseDataHandler<DataT>,
subscriptionCallback?: RequestCallback
) => {
const handlerData = typeof data === 'function' ? data : {
data,
code: data ? 200 : 204,
}
return addServerHandler<DataT>(
method,
path,
handlerData,
subscriptionCallback
);
}
const addErrorHandler = (
method: string,
path: string,
errorStr: string | null,
errorCode: number,
subscriptionCallback?: RequestCallback
) => {
return addServerHandler(
method,
path,
{
error: !errorStr ? null as any : {
message: errorStr,
},
code: errorCode,
},
subscriptionCallback
);
}
const addSubscriptionHandlerImpl = (
moduleName: string,
type: string,
listenerName: string,
entityId?: string | number,
) => {
const subscribeFn = mockF.fn();
const unsubscribeFn = mockF.fn();
const path = entityId ? `${moduleName}/${entityId}/${type}/${listenerName}` : `${moduleName}/${type}/${listenerName}`;
const subscribeRemove = addRequestHandler('POST', path, undefined, subscribeFn);
const unsubscribeRemove = addRequestHandler('DELETE', path, undefined, unsubscribeFn);
const fire = (data: object, entityId?: string | number) => {
if (entityId) {
logger.verbose(`Mock server: firing subscriber ${moduleName} ${listenerName} for entity ${entityId}:`, data);
} else {
logger.verbose(`Mock server: firing subscriber ${moduleName} ${listenerName}:`, data);
}
send({
event: listenerName,
data,
id: entityId,
});
}
const remove = () => {
subscribeRemove();
unsubscribeRemove();
};
return {
fire,
remove,
subscribeFn,
unsubscribeFn,
path,
}
}
const addSubscriptionHandler = (
moduleName: string,
listenerName: string,
entityId?: string | number,
) => {
return addSubscriptionHandlerImpl(moduleName, 'listeners', listenerName, entityId);
}
const addHookHandler = (
moduleName: string,
listenerName: string,
) => {
const subscriber = addSubscriptionHandlerImpl(moduleName, 'hooks', listenerName);
const addResolver = (completionId: number) => {
const resolveFn = mockF.fn();
const rejectFn = mockF.fn();
const resolveRemove = addRequestHandler(
'POST',
`${subscriber.path}/${completionId}/resolve`,
undefined,
resolveFn
);
const rejectRemove = addRequestHandler(
'POST',
`${subscriber.path}/${completionId}/reject`,
undefined,
rejectFn
);
const remove = () => {
resolveRemove();
rejectRemove();
}
const fire = (data: object) => {
logger.verbose(`Mock server: firing hook ${moduleName} ${listenerName}:`, data);
send({
event: listenerName,
data,
completion_id: completionId,
});
}
return { fire, remove, resolveFn, rejectFn };
};
return {
addResolver,
...subscriber,
}
}
const clearHandlers = () => {
emitter.removeAllListeners();
handlers.clear();
}
mockServer.on('connection', s => {
socket = s;
socket.on('message', (messageObj) => {
const request: OutgoingRequest = JSON.parse(messageObj as string);
const emitId = toEmitId(request.path, request.method);
const processed = emitter.emit(emitId, request, s);
if (reportMissingListeners && !processed) {
logger.warn(`Mock server: no listeners for event ${request.method} ${request.path}`);
}
});
});
mockServer.on('close', () => {
// Remove handlers only after the socket was disconnected
// to allow it to complete all requests
clearHandlers();
});
return {
addRequestHandler,
addErrorHandler,
addSubscriptionHandler,
addHookHandler,
ignoreMissingHandler: addDummyDataHandler,
stop: () => {
mockServer.stop();
},
send,
url,
};
};
export { getMockServer };