@node-idempotency/core
Version:
A Race-Condition free Node.js library that ensures idempotency for requests, preventing unintended duplicate operations.
115 lines • 4.86 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.Idempotency = void 0;
const types_1 = require("./types");
const constants_1 = require("./constants");
const error_1 = require("./error");
const crypto_1 = require("crypto");
class Idempotency {
constructor(storage, options) {
this.storage = storage;
this.options = {
cacheKeyPrefix: constants_1.IDEMPOTENCY_CACHE_KEY_PREFIX,
idempotencyKey: types_1.HttpHeaderKeysEnum.IDEMPOTENCY_KEY.toLocaleLowerCase(),
keyMaxLength: constants_1.IDEMPOTENCY_KEY_LEN,
cacheTTLMS: constants_1.IDEMPOTENCY_CACHE_TTL_MS,
enforceIdempotency: false,
...options,
};
}
buildRequestOptions(options) {
const enforceIdempotency = options?.enforceIdempotency !== undefined
? options?.enforceIdempotency
: this.options.enforceIdempotency;
return { ...this.options, ...options, enforceIdempotency };
}
getInternalRequest(req) {
return {
...req,
options: this.buildRequestOptions(req.options),
};
}
getIdempotencyKey(req) {
const key = Object.keys(req.headers).find((key) => key.toLowerCase() === req.options.idempotencyKey.toLowerCase());
return key ? req.headers[key] : undefined;
}
async isEnabled(req) {
const idempotencyKey = this.getIdempotencyKey(req);
if (!idempotencyKey) {
if (req.options?.enforceIdempotency) {
throw new error_1.IdempotencyError("Idempotency-Key is missing", error_1.IdempotencyErrorCodes.IDEMPOTENCY_KEY_LEN_EXEEDED);
}
return false;
}
if (typeof req.options.skipRequest === "function") {
const shouldSkip = await req.options.skipRequest(req);
return !shouldSkip;
}
return true;
}
validateRequest(req) {
const idempotencyKey = this.getIdempotencyKey(req);
if (idempotencyKey && idempotencyKey.length > req.options.keyMaxLength) {
throw new error_1.IdempotencyError("Idempotency-Key length exceeds max allowed length", error_1.IdempotencyErrorCodes.IDEMPOTENCY_KEY_LEN_EXEEDED);
}
}
getIdempotencyCacheKey(req) {
const idempotencyKey = this.getIdempotencyKey(req);
const { path, method } = req;
return `${req.options.cacheKeyPrefix}:${method}:${path}:${idempotencyKey}`;
}
hash(body) {
const hash = (0, crypto_1.createHash)("blake2s256");
hash.update(Buffer.from(JSON.stringify(body)));
return hash.digest("hex");
}
getFingerPrint(req) {
return req.body ? this.hash(req.body) : undefined;
}
async onRequest(req) {
const reqInternal = this.getInternalRequest(req);
if (await this.isEnabled(reqInternal)) {
const fingerPrint = this.getFingerPrint(reqInternal);
const payload = {
status: types_1.RequestStatusEnum.IN_PROGRESS,
fingerPrint,
};
const cacheKey = this.getIdempotencyCacheKey(reqInternal);
this.validateRequest(reqInternal);
const isNew = await this.storage.setIfNotExists(cacheKey, JSON.stringify(payload), { ttl: reqInternal.options.cacheTTLMS });
if (!isNew) {
const cached = await this.storage.get(cacheKey);
if (!cached) {
return undefined;
}
const data = JSON.parse(cached);
if (data.status === types_1.RequestStatusEnum.IN_PROGRESS) {
throw new error_1.IdempotencyError("A request is outstanding for this Idempotency-Key", error_1.IdempotencyErrorCodes.REQUEST_IN_PROGRESS);
}
else {
if (fingerPrint !== data.fingerPrint) {
throw new error_1.IdempotencyError("Idempotency-Key is already used", error_1.IdempotencyErrorCodes.IDEMPOTENCY_FINGERPRINT_MISSMATCH);
}
return data.response;
}
}
}
}
async onResponse(req, res) {
const reqInternal = this.getInternalRequest(req);
if (await this.isEnabled(reqInternal)) {
const fingerPrint = this.getFingerPrint(reqInternal);
const cacheKey = this.getIdempotencyCacheKey(reqInternal);
const payload = {
status: types_1.RequestStatusEnum.COMPLETE,
fingerPrint,
response: res,
};
await this.storage.set(cacheKey, JSON.stringify(payload), {
ttl: reqInternal.options.cacheTTLMS,
});
}
}
}
exports.Idempotency = Idempotency;
//# sourceMappingURL=idempotency.js.map