firestore-retry
Version:
A robust retry mechanism for Firebase Firestore operations with exponential backoff and configurable retry strategies
174 lines • 6.14 kB
JavaScript
;
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