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.
174 lines (172 loc) • 7.21 kB
JavaScript
const require_listener_iot_connection = require('./iot-connection.js');
const require_listener_topic = require('./topic.js');
//#region listener/WsListener.ts
var WsListener = class {
constructor() {
this.messages = [];
this.trackers = [];
this.closed = true;
this.functionPrefix = "waitFor";
this.debugMode = false;
this.fragments = /* @__PURE__ */ new Map();
}
async start(params) {
this.debugMode = !!params.debugMode;
try {
this.connection = await require_listener_iot_connection.getConnection(this.debugMode, params.serverlessSpyWsUrl);
this.closed = false;
const topic = require_listener_topic.getTopic(params.scope || "#");
this.log(`Subscribing to topic ${topic}`);
const connectionOpenResolve = this.connectionOpenResolve || params.connectionOpenResolve;
const localConnection = this.connection;
this.connection.on("connect", () => {
this.closed = false;
this.log("Connection opened");
localConnection.subscribe(topic);
if (connectionOpenResolve) connectionOpenResolve();
});
this.connection.on("message", (_topic, data) => {
if (this.closed) return;
this.log("Message received", data.toString());
const fragment = JSON.parse(data.toString());
let message = void 0;
if (!fragment.id) message = JSON.parse(fragment.data);
let pending = this.fragments.get(fragment.id);
if (!pending) {
pending = /* @__PURE__ */ new Map();
this.fragments.set(fragment.id, pending);
}
pending.set(fragment.index, fragment);
if (pending.size === fragment.count) {
const data$1 = [...pending.values()].sort((a, b) => a.index - b.index).map((item) => item.data).join("");
this.fragments.delete(fragment.id);
message = JSON.parse(data$1);
}
if (message) {
message.serviceKeyForFunction = message.serviceKey.replace(/#/g, "");
if (message.serviceKey.startsWith("Function")) message.functionContextAwsRequestId = message.data.context.awsRequestId;
this.messages.push(message);
this.resolveOldTrackerWithNewMessage(message);
}
});
this.connection.on("close", () => {
this.log("Connection closed");
this.closed = true;
});
const connectionOpenReject = this.connectionOpenReject || params.connectionOpenReject;
this.connection.on("error", (error) => {
this.log("Connection error:", error);
connectionOpenReject?.(error);
});
} catch (e) {
console.error("Failed to get connection", e);
throw e;
}
}
async stop() {
this.closed = true;
this.connection.end(true);
}
trackerMatchMessage(tracker, message) {
if (tracker.finished) return;
if (tracker.serviceKey && tracker.serviceKey === message.serviceKey || tracker.serviceKeyForFunction && tracker.serviceKeyForFunction === message.serviceKeyForFunction) {
if (this.trackerMatchCondition(tracker, message)) {
tracker.finished = true;
const spyAndJestMatchers = { getData: () => message.data };
const serviceKeyForFunction = tracker.serviceKeyForFunction;
if (serviceKeyForFunction && serviceKeyForFunction.startsWith("Function") && (serviceKeyForFunction.endsWith("Request") || serviceKeyForFunction.endsWith("Console"))) {
let serviceKeyForFunctionChain = serviceKeyForFunction;
if (serviceKeyForFunctionChain.endsWith("Request")) serviceKeyForFunctionChain = serviceKeyForFunctionChain.substring(0, serviceKeyForFunctionChain.length - 7);
else if (serviceKeyForFunctionChain.endsWith("Console")) serviceKeyForFunctionChain = serviceKeyForFunctionChain.substring(0, serviceKeyForFunctionChain.length - 7);
spyAndJestMatchers.followedByConsole = (paramsW) => {
return this.createWaitForXXXFunc(`${serviceKeyForFunctionChain}Console`, message.data.context.awsRequestId)(paramsW);
};
spyAndJestMatchers.followedByResponse = (paramsW) => {
return this.createWaitForXXXFunc(`${serviceKeyForFunctionChain}Response`, message.data.context.awsRequestId)(paramsW);
};
}
const proxy = new Proxy(spyAndJestMatchers, { get: function(target, objectKey) {
if (target.hasOwnProperty(objectKey)) return target[objectKey];
else if (objectKey !== "then") return function() {
expect(message.data)[objectKey].apply(void 0, arguments);
return proxy;
};
} });
tracker.promiseResolve(proxy);
return true;
}
}
return false;
}
resolveTrackerInOldMessages(tracker) {
for (const message of this.messages) if (this.trackerMatchMessage(tracker, message)) return true;
return false;
}
resolveOldTrackerWithNewMessage(message) {
for (let index = 0; index < this.trackers.length; index++) {
const tracker = this.trackers[index];
if (this.trackerMatchMessage(tracker, message)) {
this.trackers = this.trackers.splice(index, 1);
return true;
}
}
return false;
}
trackerMatchCondition(tracker, message) {
const matchCondition = tracker.condition && tracker.condition(message.data) || !tracker.condition;
const matchRequestId = tracker.functionContextAwsRequestId && tracker.functionContextAwsRequestId === message.functionContextAwsRequestId || !tracker.functionContextAwsRequestId;
if (matchCondition && matchRequestId) return true;
else {
if (!matchCondition && matchRequestId && !tracker.possibleSpyMessageDataForDebugging) tracker.possibleSpyMessageDataForDebugging = message.data;
return false;
}
}
createWaitForXXXFunc(serviceKeyForFunction, functionContextAwsRequestId) {
return (paramsW) => {
let resolve;
const promise = new Promise((res) => {
resolve = res;
});
const tracker = {
finished: false,
promiseResolve: resolve,
serviceKeyForFunction,
functionContextAwsRequestId
};
tracker.condition = paramsW?.condition;
let timeoutPid;
const timer = new Promise((_, reject) => {
timeoutPid = setTimeout(() => {
if (tracker.finished) return;
tracker.finished = true;
let message = `Timeout waiting for Serverless Spy message ${serviceKeyForFunction}.`;
if (tracker.possibleSpyMessageDataForDebugging) message += ` Similar matching spy event data: ${JSON.stringify(tracker.possibleSpyMessageDataForDebugging, null, 2)}`;
reject(new Error(message));
}, paramsW?.timoutMs || 1e4);
});
if (!this.resolveTrackerInOldMessages(tracker)) this.trackers.push(tracker);
return Promise.race([promise, timer]).finally(() => {
if (!!timeoutPid) clearTimeout(timeoutPid);
});
};
}
createProxy() {
const spyListener = {};
spyListener.stop = async () => {
await this.stop();
};
return new Proxy(spyListener, { get: (target, objectKey) => {
if (target.hasOwnProperty(objectKey)) return target[objectKey].bind(target);
else if (typeof objectKey === "string" && objectKey.startsWith(this.functionPrefix)) {
const serviceKeyForFunction = objectKey.substring(this.functionPrefix.length);
return this.createWaitForXXXFunc(serviceKeyForFunction);
}
} });
}
log(message, ...optionalParams) {
if (this.debugMode && !this.closed) console.debug("SSPY", message, (/* @__PURE__ */ new Date()).toISOString(), ...optionalParams);
}
};
//#endregion
exports.WsListener = WsListener;
//# sourceMappingURL=WsListener.js.map