UNPKG

@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
"use strict"; 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