@reaatech/idempotency-middleware-adapter-firestore
GCP Firestore storage adapter for @reaatech/idempotency-middleware. Uses Firestore transactions for safe distributed locking with TTL-compatible expiresAt fields, backed by the Google Cloud Firestore Node.js SDK.
Installation
npm install @reaatech/idempotency-middleware-adapter-firestore @google-cloud/firestore
# or
pnpm add @reaatech/idempotency-middleware-adapter-firestore @google-cloud/firestoreFeature Overview
- Transaction-gated locking — lock acquisition wrapped in a Firestore
runTransactionto guarantee atomicity - Automatic expired lock reclaim — expired locks (based on
expiresAt) are re-acquirable within the same transaction flow - TTL-compatible
expiresAt—Date-typed field compatible with Firestore TTL policies for automatic cleanup - Client-side TTL enforcement — explicit expiry check on
get()as a safety net before Firestore TTL scavenging - Connectionless — Firestore client is ready immediately;
connect()anddisconnect()are no-ops - Implements
StorageAdapter— drop-in replacement for any other adapter
Quick Start
import { Firestore } from '@google-cloud/firestore';
import { FirestoreAdapter } from '@reaatech/idempotency-middleware-adapter-firestore';
import { IdempotencyMiddleware } from '@reaatech/idempotency-middleware';
const firestore = new Firestore();
const storage = new FirestoreAdapter(firestore, 'idempotency_cache');
const middleware = new IdempotencyMiddleware(storage, { ttl: 3_600_000 });
const result = await middleware.execute(
'checkout-789',
{ method: 'POST', path: '/checkout', body: { cartId: 'xyz' } },
async () => ({ id: 1, status: 'confirmed' }),
);API Reference
FirestoreAdapter
import { FirestoreAdapter } from '@reaatech/idempotency-middleware-adapter-firestore';
const adapter = new FirestoreAdapter(firestore, 'my_collection');Constructor
| Param | Type | Default | Description |
|---|---|---|---|
firestore | Firestore | (required) | Configured Firestore client instance |
collectionName | string | idempotency_cache | The Firestore collection name |
Document Schema
Each document in the collection stores:
| Field | Type | Description |
|---|---|---|
response | (any) | The cached response (serialized) |
createdAt | number | Epoch milliseconds |
ttl | number | TTL in milliseconds |
expiresAt | Date | Expiry timestamp (createdAt + ttl) — compatible with Firestore TTL policies |
Methods
Implements the full StorageAdapter interface:
| Method | Firestore Operation | Description |
|---|---|---|
connect() | None | No-op (client is connectionless) |
disconnect() | None | No-op |
get(key) | doc(key).get() | Returns null if missing or expired |
set(key, record) | doc(key).set(...) with expiresAt Date | Overwrites existing documents |
delete(key) | doc(key).delete() | Removes the document |
acquireLock(key, ttl) | runTransaction on lock:<key> | Returns true on success, false if lock exists and is unexpired |
releaseLock(key) | doc(lock:<key>).delete() | Removes the lock document |
waitForLock(key, timeout, pollInterval) | doc(lock:<key>).get() | Polls until lock disappears, expires, or timeout |
Locking Design
The Firestore adapter uses transactions for safe distributed locking:
- Acquire:
runTransactionon the lock documentlock:<key>. If the document exists and itsexpiresAtis in the future, the transaction throws a marker error (__idempotency_lock_held__) which is caught and surfaced asacquireLock = false. Otherwise, the document is set with{ acquiredAt, expiresAt }. - Release:
delete()onlock:<key>. - Wait: polls
doc(lock:<key>).get()— returns when the document is missing or itsexpiresAthas passed.
Expired locks are automatically re-acquirable because the transaction only rejects when expiresAt > now.
Usage Patterns
Enabling Firestore TTL
Create a TTL policy on your collection pointing to the expiresAt field. This lets Firestore automatically delete expired documents:
gcloud firestore fields ttls update expiresAt \
--collection-group=idempotency_cache \
--enable-ttlCustom Collection Name
const adapter = new FirestoreAdapter(firestore, 'prod_idempotency_cache');Distributed Workers
Multiple Cloud Run instances or Cloud Functions sharing the same Firestore collection coordinate via transactions:
// Instance A and Instance B both execute:
const result = await middleware.execute('same-key', ctx, handler);
// Only one handler executes. Both return the same cached result.Handling Firestore Latency
Firestore transactions have higher latency than Redis but guarantee strong consistency. Consider these trade-offs:
const middleware = new IdempotencyMiddleware(storage, {
lockTimeout: 60_000, // Longer timeout for Firestore latency
lockPollInterval: 500, // Less frequent polling to save reads
ttl: 86_400_000, // 24-hour cache
});Related Packages
@reaatech/idempotency-middleware— Core middleware,StorageAdapterinterface@reaatech/idempotency-middleware-express— Express middleware@reaatech/idempotency-middleware-koa— Koa middleware@reaatech/idempotency-middleware-adapter-redis— Redis adapter@reaatech/idempotency-middleware-adapter-dynamodb— DynamoDB adapter
