Skip to content
reaatechREAATECH

@reaatech/idempotency-middleware-adapter-firestore

pending npm

Provides a Firestore storage adapter for `@reaatech/idempotency-middleware` that uses atomic transactions to manage distributed locks and cached responses. It exports a `FirestoreAdapter` class that requires an initialized Google Cloud Firestore client instance.

@reaatech/idempotency-middleware-adapter-firestore

npm version License: MIT CI

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

terminal
npm install @reaatech/idempotency-middleware-adapter-firestore @google-cloud/firestore
# or
pnpm add @reaatech/idempotency-middleware-adapter-firestore @google-cloud/firestore

Feature Overview

  • Transaction-gated locking — lock acquisition wrapped in a Firestore runTransaction to guarantee atomicity
  • Automatic expired lock reclaim — expired locks (based on expiresAt) are re-acquirable within the same transaction flow
  • TTL-compatible expiresAtDate-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() and disconnect() are no-ops
  • Implements StorageAdapter — drop-in replacement for any other adapter

Quick Start

typescript
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

typescript
import { FirestoreAdapter } from '@reaatech/idempotency-middleware-adapter-firestore';
 
const adapter = new FirestoreAdapter(firestore, 'my_collection');

Constructor

ParamTypeDefaultDescription
firestoreFirestore(required)Configured Firestore client instance
collectionNamestringidempotency_cacheThe Firestore collection name

Document Schema

Each document in the collection stores:

FieldTypeDescription
response(any)The cached response (serialized)
createdAtnumberEpoch milliseconds
ttlnumberTTL in milliseconds
expiresAtDateExpiry timestamp (createdAt + ttl) — compatible with Firestore TTL policies

Methods

Implements the full StorageAdapter interface:

MethodFirestore OperationDescription
connect()NoneNo-op (client is connectionless)
disconnect()NoneNo-op
get(key)doc(key).get()Returns null if missing or expired
set(key, record)doc(key).set(...) with expiresAt DateOverwrites 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:

  1. Acquire: runTransaction on the lock document lock:<key>. If the document exists and its expiresAt is in the future, the transaction throws a marker error (__idempotency_lock_held__) which is caught and surfaced as acquireLock = false. Otherwise, the document is set with { acquiredAt, expiresAt }.
  2. Release: delete() on lock:<key>.
  3. Wait: polls doc(lock:<key>).get() — returns when the document is missing or its expiresAt has 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:

terminal
gcloud firestore fields ttls update expiresAt \
  --collection-group=idempotency_cache \
  --enable-ttl

Custom Collection Name

typescript
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:

typescript
// 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:

typescript
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
});

License

MIT