TypeScript distributed lock library that prevents race conditions across services. Because nobody wants their payment processed twice! 💸
Supports Firestore and Redis backends with automatic cleanup and bulletproof concurrency control.
npm install syncguard @google-cloud/firestore
# or with Redis (for the speed demons 🏎️)
npm install syncguard ioredis
import { createLock } from "syncguard/firestore";
import { Firestore } from "@google-cloud/firestore";
const db = new Firestore();
const lock = createLock(db);
// Prevent duplicate payment processing
await lock(
async () => {
const payment = await getPayment(paymentId);
if (payment.status === "pending") {
await processPayment(payment);
await updatePaymentStatus(paymentId, "completed");
}
},
{ key: `payment:${paymentId}`, ttlMs: 60000 },
);
// For long-running operations that need more control
const result = await lock.acquire({
key: "batch:daily-report",
ttlMs: 300000, // 5 minutes
timeoutMs: 10000, // Wait up to 10s to acquire
});
if (result.success) {
try {
await generateDailyReport();
// Extend lock if needed (critical: handle failures!)
const extended = await lock.extend(result.lockId, 300000);
if (!extended) {
throw new Error(
"Failed to extend lock - aborting to prevent race conditions",
);
}
await sendReportEmail();
} finally {
await lock.release(result.lockId);
}
} else {
console.error("Could not acquire lock:", result.error);
}
// Firestore
import { createLock } from "syncguard/firestore";
const firestoreLock = createLock(new Firestore());
// Redis
import { createLock } from "syncguard/redis";
const redisLock = createLock(redisClient);
// Custom backend
import { createLock } from "syncguard";
const customLock = createLock(myBackend);
All the knobs and dials you need to tune your locks to perfection:
interface LockConfig {
key: string; // Unique lock identifier
ttlMs?: number; // Lock duration (default: 30s)
timeoutMs?: number; // Max wait time to acquire (default: 5s)
maxRetries?: number; // Retry attempts (default: 10)
retryDelayMs?: number; // Delay between retries (default: 100ms)
}
const lock = createLock(db, {
collection: "app_locks", // Custom collection name (default: "locks")
retryDelayMs: 200, // Override retry delay
maxRetries: 15, // More aggressive retries
});
lockId
field for optimal performance. Without it, your locks will be slower than a sleepy sloth! 🦥
When things go sideways (and they will), handle it gracefully:
import { LockError } from "syncguard";
try {
await lock(
async () => {
// Your critical section here
},
{ key: "resource:123" },
);
} catch (error) {
if (error instanceof LockError) {
console.error("Lock operation failed:", error.code, error.message);
// Handle specific error types: ACQUISITION_FAILED, TIMEOUT, etc.
}
}
"I said do it once, not twice!" - Every developer ever
const processJob = async (jobId: string) => {
await lock(
async () => {
const job = await getJob(jobId);
if (job.status === "pending") {
await executeJob(job);
await markJobComplete(jobId);
}
// If job was already processed, this is a no-op (which is perfect!)
},
{ key: `job:${jobId}`, ttlMs: 300000 }, // 5 minute timeout
);
};
Because some users think your API is a free-for-all
const checkRateLimit = async (userId: string) => {
const result = await lock.acquire({
key: `rate:${userId}`,
ttlMs: 60000, // 1 minute window
timeoutMs: 0, // Fail immediately if locked
maxRetries: 0, // No retries for rate limiting
});
if (!result.success) {
throw new Error("Rate limit exceeded. Slow down there, speed racer! 🏁");
}
// Don't release - let it expire naturally for rate limiting
return performOperation(userId);
};
Single-file migrations only, please
const runMigration = async (version: string) => {
await lock(
async () => {
const currentVersion = await getCurrentDbVersion();
if (currentVersion < version) {
console.log(`Running migration to version ${version}...`);
await runMigrationScripts(version);
await updateDbVersion(version);
} else {
console.log("Migration already applied, skipping");
}
},
{ key: "db:migration", ttlMs: 600000 }, // 10 minutes for safety
);
};
Implement the LockBackend
interface for custom storage:
import { LockBackend, createLock } from "syncguard";
const myBackend: LockBackend = {
async acquire(config) {
/* your implementation */
},
async release(lockId) {
/* your implementation */
},
async extend(lockId, ttl) {
/* your implementation */
},
async isLocked(key) {
/* your implementation */
},
};
const lock = createLock(myBackend);
Got questions? Hit a snag? Or just want to share your awesome WebSocket creation? Find us on Discord. We promise we don't bite (usually 😉).
This project is licensed under the MIT License. See the LICENSE file for details.