UNPKG

serverless-spy

Version:

CDK-based library for writing elegant integration tests on AWS serverless architecture and an additional web console to monitor events in real time.

1 lines 14.9 kB
{"version":3,"file":"WsListener.mjs","names":["message: SpyMessageStorage | undefined","data","spyAndJestMatchers: any","resolve: (value: void | PromiseLike<any>) => void","tracker: Tracker","timeoutPid: NodeJS.Timeout | undefined"],"sources":["../../listener/WsListener.ts"],"sourcesContent":["import { device } from 'aws-iot-device-sdk';\nimport { fragment, getConnection } from './iot-connection';\nimport { ServerlessSpyListener } from './ServerlessSpyListener';\nimport { ServerlessSpyListenerParams } from './ServerlessSpyListenerParams';\nimport { getTopic } from './topic';\nimport { WaitForParams } from './WaitForParams';\nimport { FunctionRequestSpyEvent } from '../common/spyEvents/FunctionRequestSpyEvent';\nimport { SpyMessage } from '../common/spyEvents/SpyMessage';\n\nexport class WsListener<TSpyEvents> {\n private messages: SpyMessageStorage[] = [];\n private trackers: Tracker[] = [];\n\n private connectionOpenResolve?: () => void;\n private connectionOpenReject?: (reason?: any) => void;\n private closed = true;\n private functionPrefix = 'waitFor';\n private debugMode = false;\n private connection: device | undefined;\n\n private fragments = new Map<string, Map<number, fragment>>();\n\n public async start(params: ServerlessSpyListenerParams) {\n this.debugMode = !!params.debugMode;\n try {\n this.connection = await getConnection(\n this.debugMode,\n params.serverlessSpyWsUrl\n );\n this.closed = false;\n const topic = getTopic(params.scope || '#');\n this.log(`Subscribing to topic ${topic}`);\n const connectionOpenResolve =\n this.connectionOpenResolve || params.connectionOpenResolve;\n const localConnection = this.connection;\n this.connection.on('connect', () => {\n this.closed = false;\n this.log('Connection opened');\n localConnection.subscribe(topic);\n if (connectionOpenResolve) {\n connectionOpenResolve();\n }\n });\n this.connection.on('message', (_topic: string, data: Buffer) => {\n if (this.closed) return;\n\n this.log('Message received', data.toString());\n const fragment = JSON.parse(data.toString());\n let message: SpyMessageStorage | undefined = undefined;\n if (!fragment.id) {\n message = JSON.parse(fragment.data) as SpyMessageStorage;\n }\n\n let pending = this.fragments.get(fragment.id);\n if (!pending) {\n pending = new Map();\n this.fragments.set(fragment.id, pending);\n }\n pending.set(fragment.index, fragment);\n\n if (pending.size === fragment.count) {\n const data = [...pending.values()]\n .sort((a, b) => a.index - b.index)\n .map((item) => item.data)\n .join('');\n this.fragments.delete(fragment.id);\n message = JSON.parse(data) as SpyMessageStorage;\n }\n\n if (message) {\n message.serviceKeyForFunction = message.serviceKey.replace(/#/g, '');\n\n if (message.serviceKey.startsWith('Function')) {\n message.functionContextAwsRequestId = (\n message.data as FunctionRequestSpyEvent\n ).context.awsRequestId;\n }\n\n this.messages.push(message);\n this.resolveOldTrackerWithNewMessage(message);\n }\n });\n this.connection.on('close', () => {\n this.log('Connection closed');\n\n this.closed = true;\n });\n\n const connectionOpenReject =\n this.connectionOpenReject || params.connectionOpenReject;\n this.connection.on('error', (error) => {\n this.log('Connection error:', error);\n connectionOpenReject?.(error);\n });\n } catch (e) {\n console.error('Failed to get connection', e);\n throw e;\n }\n }\n\n public async stop() {\n this.closed = true;\n this.connection!.end(true);\n }\n\n private trackerMatchMessage(tracker: Tracker, message: SpyMessageStorage) {\n if (tracker.finished) return;\n\n if (\n (tracker.serviceKey && tracker.serviceKey === message.serviceKey) ||\n (tracker.serviceKeyForFunction &&\n tracker.serviceKeyForFunction === message.serviceKeyForFunction)\n ) {\n if (this.trackerMatchCondition(tracker, message)) {\n tracker.finished = true;\n\n const spyAndJestMatchers: any = {\n getData: () => message.data,\n };\n\n const serviceKeyForFunction = tracker.serviceKeyForFunction;\n if (\n serviceKeyForFunction &&\n serviceKeyForFunction.startsWith('Function') &&\n (serviceKeyForFunction.endsWith('Request') ||\n serviceKeyForFunction.endsWith('Console'))\n ) {\n let serviceKeyForFunctionChain = serviceKeyForFunction;\n\n if (serviceKeyForFunctionChain.endsWith('Request')) {\n serviceKeyForFunctionChain = serviceKeyForFunctionChain.substring(\n 0,\n serviceKeyForFunctionChain.length - 'Request'.length\n );\n } else if (serviceKeyForFunctionChain.endsWith('Console')) {\n serviceKeyForFunctionChain = serviceKeyForFunctionChain.substring(\n 0,\n serviceKeyForFunctionChain.length - 'Console'.length\n );\n }\n\n spyAndJestMatchers.followedByConsole = (paramsW: WaitForParams) => {\n return this.createWaitForXXXFunc(\n `${serviceKeyForFunctionChain}Console`,\n (message.data as FunctionRequestSpyEvent).context.awsRequestId\n )(paramsW);\n };\n\n spyAndJestMatchers.followedByResponse = (paramsW: WaitForParams) => {\n return this.createWaitForXXXFunc(\n `${serviceKeyForFunctionChain}Response`,\n (message.data as FunctionRequestSpyEvent).context.awsRequestId\n )(paramsW);\n };\n }\n\n const proxy = new Proxy(spyAndJestMatchers, {\n get: function (target: any, objectKey: string) {\n if (target.hasOwnProperty(objectKey)) {\n return target[objectKey];\n } else if (objectKey !== 'then') {\n return function () {\n const jestFunctionToExecute = (expect(message.data) as any)[\n objectKey\n ];\n jestFunctionToExecute.apply(undefined, arguments);\n return proxy;\n };\n }\n },\n });\n\n tracker.promiseResolve(proxy);\n return true;\n }\n }\n return false;\n }\n\n private resolveTrackerInOldMessages(tracker: Tracker) {\n for (const message of this.messages) {\n if (this.trackerMatchMessage(tracker, message)) {\n return true;\n }\n }\n\n return false;\n }\n\n private resolveOldTrackerWithNewMessage(message: SpyMessageStorage) {\n for (let index = 0; index < this.trackers.length; index++) {\n const tracker = this.trackers[index];\n if (this.trackerMatchMessage(tracker, message)) {\n this.trackers = this.trackers.splice(index, 1);\n return true;\n }\n }\n\n return false;\n }\n\n private trackerMatchCondition(tracker: Tracker, message: SpyMessageStorage) {\n const matchCondition =\n (tracker.condition && tracker.condition(message.data)) ||\n !tracker.condition;\n\n const matchRequestId =\n (tracker.functionContextAwsRequestId &&\n tracker.functionContextAwsRequestId ===\n message.functionContextAwsRequestId) ||\n !tracker.functionContextAwsRequestId;\n\n if (matchCondition && matchRequestId) {\n return true;\n } else {\n if (\n !matchCondition &&\n matchRequestId &&\n !tracker.possibleSpyMessageDataForDebugging\n ) {\n tracker.possibleSpyMessageDataForDebugging = message.data;\n }\n return false;\n }\n }\n\n private createWaitForXXXFunc(\n serviceKeyForFunction: string,\n functionContextAwsRequestId?: string\n ) {\n return (paramsW?: WaitForParams) => {\n let resolve: (value: void | PromiseLike<any>) => void;\n const promise = new Promise((res) => {\n resolve = res;\n });\n const tracker: Tracker = {\n finished: false,\n // @ts-ignore\n promiseResolve: resolve,\n serviceKeyForFunction,\n functionContextAwsRequestId,\n };\n\n tracker.condition = paramsW?.condition;\n\n let timeoutPid: NodeJS.Timeout | undefined;\n const timer = new Promise((_, reject) => {\n timeoutPid = setTimeout(() => {\n if (tracker.finished) return;\n tracker.finished = true;\n let message = `Timeout waiting for Serverless Spy message ${serviceKeyForFunction}.`;\n\n if (tracker.possibleSpyMessageDataForDebugging) {\n message += ` Similar matching spy event data: ${JSON.stringify(\n tracker.possibleSpyMessageDataForDebugging,\n null,\n 2\n )}`;\n }\n\n reject(new Error(message));\n }, paramsW?.timoutMs || 10000);\n });\n\n if (!this.resolveTrackerInOldMessages(tracker)) {\n this.trackers.push(tracker);\n }\n\n return Promise.race([promise, timer]).finally(() => {\n if (!!timeoutPid) {\n clearTimeout(timeoutPid);\n }\n });\n };\n }\n\n public createProxy() {\n const spyListener = {} as ServerlessSpyListener<TSpyEvents>;\n\n spyListener.stop = async () => {\n await this.stop();\n };\n\n return new Proxy<ServerlessSpyListener<TSpyEvents>>(spyListener, {\n get: (target: any, objectKey: string) => {\n if (target.hasOwnProperty(objectKey)) {\n return target[objectKey].bind(target);\n } else if (\n typeof objectKey === 'string' &&\n objectKey.startsWith(this.functionPrefix)\n ) {\n const serviceKeyForFunction = objectKey.substring(\n this.functionPrefix.length\n );\n\n return this.createWaitForXXXFunc(serviceKeyForFunction);\n }\n },\n });\n }\n\n private log(message: string, ...optionalParams: any[]) {\n if (this.debugMode && !this.closed) {\n console.debug(\n 'SSPY',\n message,\n new Date().toISOString(),\n ...optionalParams\n );\n }\n }\n}\n\ntype Tracker = {\n promiseResolve: (data: any) => void;\n finished: boolean;\n serviceKey?: string;\n serviceKeyForFunction?: string;\n condition?: (data: any) => boolean;\n timoutMs?: number;\n functionContextAwsRequestId?: string;\n possibleSpyMessageDataForDebugging?: any;\n};\n\ntype SpyMessageStorage = SpyMessage & {\n serviceKeyForFunction: string;\n functionContextAwsRequestId?: string;\n};\n"],"mappings":"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"}