@appium/base-driver
Version:
Base driver class for Appium drivers
173 lines • 6.34 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.handleIdempotency = handleIdempotency;
const logger_1 = require("./logger");
const lru_cache_1 = require("lru-cache");
const lodash_1 = __importDefault(require("lodash"));
const node_events_1 = require("node:events");
const IDEMPOTENT_RESPONSES = new lru_cache_1.LRUCache({
max: 64,
ttl: 30 * 60 * 1000,
updateAgeOnGet: true,
updateAgeOnHas: true,
dispose: ({ responseStateListener }) => {
responseStateListener?.removeAllListeners();
},
});
const MONITORED_METHODS = ['POST', 'PATCH'];
const IDEMPOTENCY_KEY_HEADER = 'x-idempotency-key';
const MAX_CACHED_PAYLOAD_SIZE_BYTES = 1 * 1024 * 1024; // 1 MiB
/**
* Middleware that caches and replays responses for idempotent requests using the
* `x-idempotency-key` header. Only POST and PATCH are cached.
*/
async function handleIdempotency(req, res, next) {
const keyOrArr = req.headers[IDEMPOTENCY_KEY_HEADER];
if (lodash_1.default.isEmpty(keyOrArr) || !keyOrArr) {
next();
return;
}
const key = lodash_1.default.isArray(keyOrArr) ? keyOrArr[0] : keyOrArr;
logger_1.log.updateAsyncContext({ idempotencyKey: key });
if (!MONITORED_METHODS.includes(req.method)) {
next();
return;
}
logger_1.log.debug(`Request idempotency key: ${key}`);
if (!IDEMPOTENT_RESPONSES.has(key)) {
cacheResponse(key, req, res);
next();
return;
}
const cached = IDEMPOTENT_RESPONSES.get(key);
if (!cached) {
next();
return;
}
const { method, path, response, responseStateListener } = cached;
if (req.method !== method || req.path !== path) {
logger_1.log.warn(`Got two different requests with the same idempotency key '${key}'`);
logger_1.log.warn('Is the client generating idempotency keys properly?');
next();
return;
}
if (response) {
logger_1.log.info(`The same request with the idempotency key '${key}' has been already processed`);
logger_1.log.info(`Rerouting its response to the current request`);
if (!res.socket?.writable) {
next();
return;
}
res.socket.write(response.toString('utf8'));
}
else {
logger_1.log.info(`The same request with the idempotency key '${key}' is being processed`);
logger_1.log.info(`Waiting for the response to be rerouted to the current request`);
if (!responseStateListener) {
next();
return;
}
responseStateListener.once('ready', (cachedResponse) => {
if (!cachedResponse || !res.socket?.writable) {
next();
return;
}
res.socket.write(cachedResponse.toString('utf8'));
});
}
}
function cacheResponse(key, req, res) {
if (!res.socket) {
return;
}
const responseStateListener = new node_events_1.EventEmitter();
IDEMPOTENT_RESPONSES.set(key, {
method: req.method,
path: req.path,
response: null,
responseStateListener,
});
const socket = res.socket;
const originalSocketWriter = socket.write.bind(socket);
const responseRef = new WeakRef(res);
let responseChunks = [];
let responseSize = 0;
let errorMessage = null;
const patchedWriter = (chunk, encoding, next) => {
if (errorMessage || !responseRef.deref()) {
responseChunks = [];
responseSize = 0;
return originalSocketWriter(chunk, encoding, next);
}
const buf = Buffer.isBuffer(chunk)
? chunk
: Buffer.from(chunk, typeof encoding === 'string' ? encoding : undefined);
responseChunks.push(buf);
responseSize += buf.length;
if (responseSize > MAX_CACHED_PAYLOAD_SIZE_BYTES) {
errorMessage =
`The actual response size exceeds ` +
`the maximum allowed limit of ${MAX_CACHED_PAYLOAD_SIZE_BYTES} bytes`;
}
return originalSocketWriter(chunk, encoding, next);
};
socket.write = patchedWriter;
let didEmitReady = false;
res.once('error', (e) => {
errorMessage = e.message;
if (socket.write === patchedWriter) {
socket.write = originalSocketWriter;
}
if (!IDEMPOTENT_RESPONSES.has(key)) {
logger_1.log.info(`Could not cache the response identified by '${key}'. ` +
`Cache consistency has been damaged`);
}
else {
logger_1.log.info(`Could not cache the response identified by '${key}': ${errorMessage}`);
IDEMPOTENT_RESPONSES.delete(key);
}
responseChunks = [];
responseSize = 0;
if (!didEmitReady) {
responseStateListener.emit('ready', null);
didEmitReady = true;
}
});
res.once('finish', () => {
if (socket.write === patchedWriter) {
socket.write = originalSocketWriter;
}
if (!IDEMPOTENT_RESPONSES.has(key)) {
logger_1.log.info(`Could not cache the response identified by '${key}'. ` +
`Cache consistency has been damaged`);
}
else if (errorMessage) {
logger_1.log.info(`Could not cache the response identified by '${key}': ${errorMessage}`);
IDEMPOTENT_RESPONSES.delete(key);
}
const value = IDEMPOTENT_RESPONSES.get(key);
if (value) {
value.response = Buffer.concat(responseChunks);
}
responseChunks = [];
responseSize = 0;
if (!didEmitReady) {
responseStateListener.emit('ready', value?.response ?? null);
didEmitReady = true;
}
});
res.once('close', () => {
if (socket.write === patchedWriter) {
socket.write = originalSocketWriter;
}
if (!didEmitReady) {
const value = IDEMPOTENT_RESPONSES.get(key);
responseStateListener.emit('ready', value?.response ?? null);
didEmitReady = true;
}
});
}
//# sourceMappingURL=idempotency.js.map