UNPKG

firestore-retry

Version:

A robust retry mechanism for Firebase Firestore operations with exponential backoff and configurable retry strategies

174 lines 6.14 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.RetryConfigs = exports.createRetryableBatch = exports.RetryableBatch = exports.retryFirestoreOperation = exports.retryFirestoreBatchCommit = exports.retryOperation = exports.isRetryableFirestoreError = void 0; const admin = __importStar(require("firebase-admin")); const DEFAULT_RETRY_CONFIG = { maxRetries: 5, baseDelay: 1000, maxDelay: 30000, jitterFactor: 0.1, }; const isRetryableFirestoreError = (error) => { const errorMessage = error?.message?.toLowerCase() || error?.details?.toLowerCase() || error?.msg?.toLowerCase() || ''; const errorCode = error?.code; if (errorCode === 13 && errorMessage.includes('rst_stream')) { return true; } if (errorMessage.includes('quota exceeded') || errorMessage.includes('write limit')) { return true; } if (errorCode === 14 || errorMessage.includes('unavailable')) { return true; } if (errorMessage.includes('timeout') || errorMessage.includes('deadline exceeded')) { return true; } if (errorCode === 13 || errorMessage.includes('internal server error')) { return true; } if (errorCode === 8 || errorMessage.includes('resource exhausted')) { return true; } return false; }; exports.isRetryableFirestoreError = isRetryableFirestoreError; const sleep = (ms) => { return new Promise((resolve) => setTimeout(resolve, ms)); }; const calculateDelay = (attempt, config) => { const exponentialDelay = config.baseDelay * Math.pow(2, attempt - 1); const jitter = exponentialDelay * config.jitterFactor * Math.random(); const totalDelay = exponentialDelay + jitter; return Math.min(totalDelay, config.maxDelay); }; const retryOperation = async (operation, operationName = 'operation', config = {}) => { const finalConfig = { ...DEFAULT_RETRY_CONFIG, ...config }; for (let attempt = 1; attempt <= finalConfig.maxRetries; attempt++) { try { return await operation(); } catch (error) { if (attempt === finalConfig.maxRetries || !(0, exports.isRetryableFirestoreError)(error)) { throw error; } const delay = calculateDelay(attempt, finalConfig); console.warn(`${operationName} failed, retrying in ${Math.round(delay)}ms. ` + `Attempt ${attempt}/${finalConfig.maxRetries}. Error: ${error.message}`); await sleep(delay); } } throw new Error(`Max retries exceeded for ${operationName}`); }; exports.retryOperation = retryOperation; const retryFirestoreBatchCommit = async (batch, config = {}) => { await (0, exports.retryOperation)(() => batch.commit(), 'Firestore batch commit', config); }; exports.retryFirestoreBatchCommit = retryFirestoreBatchCommit; const retryFirestoreOperation = async (operation, operationName = 'document operation', config = {}) => { return (0, exports.retryOperation)(operation, operationName, config); }; exports.retryFirestoreOperation = retryFirestoreOperation; class RetryableBatch { constructor(config = {}) { this.operationCount = 0; this.batch = admin.firestore().batch(); this.config = { ...DEFAULT_RETRY_CONFIG, ...config }; } set(docRef, data, options) { if (options) { this.batch.set(docRef, data, options); } else { this.batch.set(docRef, data); } this.operationCount++; return this; } update(docRef, data) { this.batch.update(docRef, data); this.operationCount++; return this; } delete(docRef) { this.batch.delete(docRef); this.operationCount++; return this; } async commit() { if (this.operationCount === 0) { console.warn('Attempting to commit empty batch'); return; } return (0, exports.retryFirestoreBatchCommit)(this.batch, this.config); } getOperationCount() { return this.operationCount; } } exports.RetryableBatch = RetryableBatch; const createRetryableBatch = (config = {}) => { return new RetryableBatch(config); }; exports.createRetryableBatch = createRetryableBatch; exports.RetryConfigs = { FAST: { maxRetries: 3, baseDelay: 500, maxDelay: 5000, }, STANDARD: { maxRetries: 5, baseDelay: 1000, maxDelay: 30000, }, AGGRESSIVE: { maxRetries: 10, baseDelay: 1000, maxDelay: 60000, }, PATIENT: { maxRetries: 7, baseDelay: 2000, maxDelay: 120000, }, }; //# sourceMappingURL=retry.js.map