### Postgres backend setup and usage example Source: https://kriasoft.com/syncguard/api Shows how to set up the Postgres schema and create the backend with custom table names and cleanupInIsLocked option. ```typescript import { setupSchema, createPostgresBackend } from "syncguard/postgres"; // Setup schema (once) await setupSchema(sql, { tableName: "app_locks", fenceTableName: "app_fence_counters", }); // Create backend (synchronous) const backend = createPostgresBackend(sql, { tableName: "app_locks", fenceTableName: "app_fence_counters", cleanupInIsLocked: true, // Optional cleanup in isLocked() }); ``` -------------------------------- ### Schema Setup: Quick Start Source: https://kriasoft.com/syncguard/postgres Recommended setup pattern: call setupSchema() once during initialization to create required tables and indexes. This is idempotent and safe to call multiple times. The setupSchema() function creates the syncguard_locks and syncguard_fence_counters tables with specified indexes. ```ts import postgres from "postgres"; import { setupSchema, createLock } from "syncguard/postgres"; const sql = postgres("postgresql://localhost:5432/myapp"); // Setup schema (once, during initialization) await setupSchema(sql); // Create lock (synchronous, can be called multiple times) const lock = createLock(sql); ``` -------------------------------- ### Production Setup: Manual Migrations Source: https://kriasoft.com/syncguard/postgres For production deployments, use database migrations instead of setupSchema(). This example shows creating the schema via psql and then deploying application code without calling setupSchema(). Manual migrations provide explicit control, version control, and avoid schema drift. ```bash # 1. Create schema via migrations (before deployment) psql -U postgres -d myapp < postgres/schema.sql # 2. Deploy application code (no setupSchema() call needed) ``` -------------------------------- ### Quick Start: create lock and setup schema Source: https://kriasoft.com/syncguard/postgres Initialize the PostgreSQL connection, set up the schema, and create a lock function. The lock function is synchronous and can be called multiple times. The example shows acquiring a lock for a critical section with a TTL of 30000 ms. ```ts import { createLock, setupSchema } from "syncguard/postgres"; import postgres from "postgres"; const sql = postgres("postgresql://localhost:5432/myapp"); // Setup schema (once, during initialization) await setupSchema(sql); // Create lock function (synchronous) const lock = createLock(sql); await lock( async () => { // Your critical section await processPayment(paymentId); }, { key: `payment:${paymentId}`, ttlMs: 30000 }, ); ``` -------------------------------- ### backend.acquire() usage example Source: https://kriasoft.com/syncguard/api Usage example for backend.acquire(), showing how to handle success and contention results. ```typescript const result = await backend.acquire({ key: "payment:123", ttlMs: 30000, // Lock expires after 30s }); if (result.ok) { const { lockId, expiresAtMs, fence } = result; // You now hold the lock } else { // result.reason === "locked" (another process holds it) } ``` -------------------------------- ### Install SyncGuard with Redis Source: https://kriasoft.com/syncguard/getting-started Install the syncguard package along with the ioredis client for Redis. ```bash npm install syncguard ioredis ``` -------------------------------- ### Install SyncGuard with PostgreSQL Source: https://kriasoft.com/syncguard/getting-started Install the syncguard package along with the postgres client for PostgreSQL. ```bash npm install syncguard postgres ``` -------------------------------- ### createRedisBackend usage example Source: https://kriasoft.com/syncguard/api Shows how to create a Redis backend with custom keyPrefix and cleanupInIsLocked options. ```typescript const backend = createRedisBackend(redis, { keyPrefix: "my-app", // Keys: "my-app:resource:123" cleanupInIsLocked: true, // Optional cleanup in isLocked() }); ``` -------------------------------- ### Install SyncGuard with Firestore Source: https://kriasoft.com/syncguard/getting-started Install the syncguard package along with the @google-cloud/firestore client for Firestore. ```bash npm install syncguard @google-cloud/firestore ``` -------------------------------- ### getByKey usage example Source: https://kriasoft.com/syncguard/api Shows how to import and use getByKey from the syncguard package. ```typescript import { getByKey } from "syncguard"; ``` -------------------------------- ### Quick Start with PostgreSQL Source: https://kriasoft.com/syncguard/getting-started Set up the schema and create a lock using PostgreSQL. Call setupSchema() once during initialization; it is idempotent and safe to call multiple times. ```typescript import { createLock, setupSchema } from "syncguard/postgres"; import postgres from "postgres"; const sql = postgres("postgresql://localhost:5432/myapp"); // Setup schema (once, during initialization) await setupSchema(sql); // Create lock function (synchronous) const lock = createLock(sql); await lock( async () => { // Your critical section }, { key: "resource:123" }, ); ``` -------------------------------- ### Monitoring Lock Status with getByKey, getById, owns Source: https://kriasoft.com/syncguard/postgres Use these functions to check if a resource is locked, verify ownership, and get detailed lock information. The example shows how to log the expiration time and fence token, check ownership, and get remaining time. ```TypeScript import { getByKey, getById, owns } from "syncguard"; // Check if a resource is currently locked const info = await getByKey(backend, "resource:123"); if (info) { console.log(`Resource locked until ${new Date(info.expiresAtMs)}`); console.log(`Fence token: ${info.fence}`); } // Quick ownership check if (await owns(backend, lockId)) { console.log("Still own the lock"); } // Detailed ownership info const owned = await getById(backend, lockId); if (owned) { console.log(`Expires in ${owned.expiresAtMs - Date.now()}ms`); } ``` -------------------------------- ### createFirestoreBackend usage example Source: https://kriasoft.com/syncguard/api Shows how to create a Firestore backend with custom collection and fenceCollection names and cleanupInIsLocked option. ```typescript const backend = createFirestoreBackend(db, { collection: "app_locks", fenceCollection: "app_fences", cleanupInIsLocked: true, // Optional cleanup in isLocked() }); ``` -------------------------------- ### getByKey() usage example Source: https://kriasoft.com/syncguard/api Checks if a resource is locked and logs lock expiration and fence token. Use when you need to know if a resource is locked and get detailed lock holder information. ```typescript const info = await getByKey(backend, "resource:123"); if (info) { console.log(`Lock expires in ${info.expiresAtMs - Date.now()}ms`); console.log(`Fence: ${info.fence}`); } else { console.log("Resource is not locked"); } ``` -------------------------------- ### Setup schema and create PostgreSQL backend with custom table names Source: https://kriasoft.com/syncguard/postgres Shows how to initialize the schema and create a backend with custom table names. The tableName and fenceTableName must match between setupSchema and createPostgresBackend. ```ts import { setupSchema, createPostgresBackend } from "syncguard/postgres"; // Setup with custom table names await setupSchema(sql, { tableName: "app_locks", fenceTableName: "app_fence_counters", }); // Create backend with matching config const backend = createPostgresBackend(sql, { tableName: "app_locks", // Lock table (default: "syncguard_locks") fenceTableName: "app_fence_counters", // Fence counter table (default: "syncguard_fence_counters") cleanupInIsLocked: false, // Enable cleanup in isLocked (default: false) }); ``` -------------------------------- ### Check NTP synchronization and install ntpdate Source: https://kriasoft.com/syncguard/firestore Commands to check NTP synchronization and install ntpdate in Docker/K8s containers to avoid time skew issues. ```bash # Check NTP synchronization timedatectl status # On Docker/K8s, ensure NTP is available in containers # Add to Dockerfile: RUN apt-get update && apt-get install -y ntpdate ``` -------------------------------- ### createRedisBackend() example Source: https://kriasoft.com/syncguard/api Creates a Redis backend for manual lock control with custom retry logic or long-running tasks. ```typescript import { createRedisBackend } from "syncguard/redis"; import Redis from "ioredis"; const redis = new Redis(); const backend = createRedisBackend(redis, { keyPrefix: "syncguard", cleanupInIsLocked: false, }); ``` -------------------------------- ### Fence Token Format Examples Source: https://kriasoft.com/syncguard/fencing Shows example fence tokens in the 15-digit zero-padded decimal string format used by SyncGuard across all backends. ```typescript "000000000000001"; // First lock "000000000000002"; // Second lock "000000000009999"; // 9,999th lock ``` -------------------------------- ### Usage example with createRedisBackend Source: https://kriasoft.com/syncguard/api Shows how to apply withTelemetry() to a Redis backend. The onEvent callback logs lock events, and includeRaw is set to false to redact raw keys/lockIds. The example then acquires a lock and shows the resulting event object. ```typescript import { withTelemetry, createRedisBackend } from "syncguard"; const backend = createRedisBackend(redis); const observed = withTelemetry(backend, { onEvent: (event) => { console.log("Lock event:", event.type, event.result); }, includeRaw: false, // Redact raw keys/lockIds (default) }); await observed.acquire({ key: "resource:123", ttlMs: 30000 }); // → { type: "acquire", result: "ok", keyHash: "...", ... } ``` -------------------------------- ### Quick Start: Create and Use a Redis Lock Source: https://kriasoft.com/syncguard/redis Import createLock from syncguard/redis, create a Redis client, and acquire a lock with a key and TTL. ```ts import { createLock } from "syncguard/redis"; import Redis from "ioredis"; const redis = new Redis(); const lock = createLock(redis); await lock( async () => { // Your critical section await processPayment(paymentId); }, { key: `payment:${paymentId}`, ttlMs: 30000 }, ); ``` -------------------------------- ### createFirestoreBackend() example Source: https://kriasoft.com/syncguard/api Creates a Firestore backend for manual lock control. Requires a single-field ascending index on the lockId field for optimal performance. ```typescript import { createFirestoreBackend } from "syncguard/firestore"; import { Firestore } from "@google-cloud/firestore"; const db = new Firestore(); const backend = createFirestoreBackend(db, { collection: "locks", fenceCollection: "fence_counters", cleanupInIsLocked: false, }); ``` -------------------------------- ### Backend capabilities logging example Source: https://kriasoft.com/syncguard/api Logs backend capabilities for fencing support and time authority. Redis always provides fences and uses server time. ```javascript console.log(backend.capabilities.supportsFencing); // true (Redis always provides fences) console.log(backend.capabilities.timeAuthority); // "server" (Redis uses server time) ``` -------------------------------- ### lock() usage example with LockConfig Source: https://kriasoft.com/syncguard/api Shows how to call lock() with a configuration object, including a custom key, ttlMs, acquisition retry strategy, and onReleaseError handler. ```typescript await lock( async () => { // Your work }, { key: "job:daily-report", ttlMs: 60000, acquisition: { timeoutMs: 10000, maxRetries: 20, }, onReleaseError: (err, ctx) => { console.error(`Failed to release ${ctx.key}:`, err); }, }, ); ``` -------------------------------- ### Constants usage Source: https://kriasoft.com/syncguard/api Example of importing and using the exported constants. Shows values for BACKEND_DEFAULTS.ttlMs, FENCE_THRESHOLDS.MAX, MAX_KEY_LENGTH_BYTES, BACKEND_LIMITS.REDIS, and RESERVE_BYTES.REDIS. ```typescript import { BACKEND_DEFAULTS, BACKEND_LIMITS, FENCE_THRESHOLDS, MAX_KEY_LENGTH_BYTES, RESERVE_BYTES, } from "syncguard"; console.log(BACKEND_DEFAULTS.ttlMs); // 30000 console.log(FENCE_THRESHOLDS.MAX); // "900000000000000" console.log(MAX_KEY_LENGTH_BYTES); // 512 // For custom backend implementations console.log(BACKEND_LIMITS.REDIS); // 1000 console.log(RESERVE_BYTES.REDIS); // 26 ``` -------------------------------- ### Usage example with LockError handling Source: https://kriasoft.com/syncguard/api Shows how to import LockError and use it in a try/catch block to handle specific error codes like AcquisitionTimeout, ServiceUnavailable, and NetworkTimeout. ```typescript import { LockError } from "syncguard"; try { await lock( async () => { // Your work }, { key: "resource:123" }, ); } catch (error) { if (error instanceof LockError) { console.error(`[${error.code}] ${error.message}`); // Handle specific codes switch (error.code) { case "AcquisitionTimeout": // Couldn't acquire lock within timeoutMs break; case "ServiceUnavailable": // Backend unavailable, retry later break; case "NetworkTimeout": // Client-side timeout break; } } } ``` -------------------------------- ### Fence usage example Source: https://kriasoft.com/syncguard/api Shows lexicographic sorting of fences and comparison to accept writes from newer lock holders. ```typescript const fences = ["000000000000003", "000000000000001"]; const sorted = fences.sort(); // ["000000000000001", "000000000000003"] - lexicographic = chronological if (newFence > storedFence) { // Accept write from newer lock holder } ``` -------------------------------- ### lock() basic usage and with configuration Source: https://kriasoft.com/syncguard/api Usage examples for the lock() function: basic usage with a key, and with configuration including ttlMs and acquisition options. ```typescript import { lock } from "syncguard"; // Basic usage await lock( async () => { // Critical section }, { key: "resource:123" }, ); // With configuration await lock( async () => { // Your work }, { key: "job:daily-report", ttlMs: 60000, // Lock expires after 60s acquisition: { timeoutMs: 10000, // Give up acquisition after 10s maxRetries: 20, // Retry up to 20 times }, }, ); ``` -------------------------------- ### Extend usage example Source: https://kriasoft.com/syncguard/api Shows how to extend a lock and schedule the next heartbeat based on the new expiration time. ```typescript const extended = await backend.extend({ lockId, ttlMs: 60000 }); if (extended.ok) { // Schedule next heartbeat based on new expiration const nextHeartbeat = extended.expiresAtMs - Date.now() - 5000; } ``` -------------------------------- ### hashKey() usage Source: https://kriasoft.com/syncguard/api Example of hashing a resource identifier. Returns a 24-character hex string with 96 bits. Used internally by lookup(); useful for custom telemetry. ```typescript import { hashKey } from "syncguard"; const hash = hashKey("resource:123"); // → "a1b2c3d4e5f6g7h8i9j0k1l2" (24 hex chars, 96 bits) ``` -------------------------------- ### AcquisitionOptions usage examples (patient and fail-fast) Source: https://kriasoft.com/syncguard/api Two lock() calls demonstrating different acquisition strategies: one with high retry tolerance (maxRetries 20, timeout 10000ms) and one fail-fast (maxRetries 3, timeout 1000ms). ```typescript // More patient (high contention tolerance) await lock( async () => { // Your work }, { key: "hot-resource", acquisition: { maxRetries: 20, timeoutMs: 10000, }, }, ); // Fail fast await lock( async () => { // Your work }, { key: "quick-check", acquisition: { maxRetries: 3, timeoutMs: 1000, }, }, ); ``` -------------------------------- ### createLock() for PostgreSQL backend Source: https://kriasoft.com/syncguard/api Factory for backend-specific auto-managed lock functions using PostgreSQL. Includes schema setup and lock creation. ```typescript import { createLock, setupSchema } from "syncguard/postgres"; import postgres from "postgres"; const sql = postgres("postgresql://localhost:5432/myapp"); // Setup schema (once, during initialization) await setupSchema(sql, { tableName: "app_locks", fenceTableName: "app_fence_counters", }); // Create lock function (synchronous) const lock = createLock(sql, { tableName: "app_locks", // Default: "syncguard_locks" fenceTableName: "app_fence_counters", // Default: "syncguard_fence_counters" }); await lock( async () => { // Your work }, { key: "resource:123" }, ); ``` -------------------------------- ### Example: Fencing required Source: https://kriasoft.com/syncguard/fencing Shows a non-idempotent operation (balance change) that must be fenced. It acquires a fence, checks if the fence is stale, and updates the account with the fence. ```typescript // Not idempotent: balance changes must be fenced const result = await backend.acquire({ key: `account:${id}`, ttlMs: 30000 }); if (result.ok) { const { fence } = result; const account = await getAccount(id); // Reject if our fence is stale if (fence <= account.fence) { throw new Error("Stale lock"); } await updateAccount(id, { balance: account.balance - amount, fence, }); } ``` -------------------------------- ### normalizeAndValidateKey() usage Source: https://kriasoft.com/syncguard/api Example of normalizing a user key before acquiring a lock. Throws LockError("InvalidArgument") if the key exceeds the limit. ```typescript import { normalizeAndValidateKey } from "syncguard"; const normalized = normalizeAndValidateKey(userKey); await backend.acquire({ key: normalized, ttlMs: 30000 }); ``` -------------------------------- ### Quick Start with Redis Source: https://kriasoft.com/syncguard/getting-started Create a lock using Redis and prevent duplicate payment processing. The lock function handles acquisition, retries, execution, and release automatically. ```typescript import { createLock } from "syncguard/redis"; import Redis from "ioredis"; const redis = new Redis(); const lock = createLock(redis); // Prevent duplicate payment processing await lock( async () => { const payment = await getPayment(paymentId); if (payment.status === "pending") { await chargeCard(payment.amount); await updateStatus(paymentId, "completed"); } }, { key: `payment:${paymentId}` }, ); ``` -------------------------------- ### getById() usage example Source: https://kriasoft.com/syncguard/api Looks up a lock by lockId and logs ownership status, fence token, and expiration. Use when you need detailed information about a lock you're holding. ```typescript import { getById } from "syncguard"; const info = await getById(backend, lockId); if (info) { console.log("Own the lock, fence:", info.fence); console.log(`Expires in ${info.expiresAtMs - Date.now()}ms`); } else { console.log("No longer own the lock"); } ``` -------------------------------- ### createLock() - PostgreSQL Source: https://kriasoft.com/syncguard/api Factory for backend-specific auto-managed lock functions for PostgreSQL. Requires schema setup before creating the lock function. ```APIDOC ## `createLock()` - PostgreSQL ### Description Factory for backend-specific auto-managed lock functions for PostgreSQL. ### Signature ```typescript import { createLock, setupSchema } from "syncguard/postgres"; ``` ### Parameters - **sql** (postgres.Sql) - Required - The PostgreSQL connection instance. - **options** (object) - Optional - Configuration options. - **tableName** (string) - Optional - Table name for locks. Default: "syncguard_locks". - **fenceTableName** (string) - Optional - Table name for fence counters. Default: "syncguard_fence_counters". ### Usage ```typescript import { createLock, setupSchema } from "syncguard/postgres"; import postgres from "postgres"; const sql = postgres("postgresql://localhost:5432/myapp"); // Setup schema (once, during initialization) await setupSchema(sql, { tableName: "app_locks", fenceTableName: "app_fence_counters", }); // Create lock function (synchronous) const lock = createLock(sql, { tableName: "app_locks", // Default: "syncguard_locks" fenceTableName: "app_fence_counters", // Default: "syncguard_fence_counters" }); await lock( async () => { // Your work }, { key: "resource:123" }, ); ``` ``` -------------------------------- ### Good lock keys Source: https://kriasoft.com/syncguard/core-concepts Examples of well-chosen lock keys that uniquely represent the protected operation, such as per-payment, per-day, per-environment, or per-event keys. ```typescript `payment:${paymentId}` // Unique per payment `job:daily-report:${date}` // Unique per day `deploy:${environment}` // One deploy at a time per env `webhook:${eventId}`; // Idempotent webhook handling ``` -------------------------------- ### getByKeyRaw() usage example Source: https://kriasoft.com/syncguard/api Looks up a lock by key and logs the raw key and lockId. Use only in development/debugging; avoid logging raw keys/lockIds in production to prevent exposure of sensitive identifiers. ```typescript import { getByKeyRaw } from "syncguard"; const debug = await getByKeyRaw(backend, "resource:123"); if (debug) { console.log("Raw key:", debug.key); // Original key console.log("Raw lockId:", debug.lockId); // Original lockId } ``` -------------------------------- ### Comparing Fences with String Comparison Source: https://kriasoft.com/syncguard/fencing Fences are lexicographically ordered strings, so direct comparison works. This example shows comparing two fences and sorting an array of fences chronologically. ```typescript const fenceA = "000000000000100"; const fenceB = "000000000000200"; // String comparison reflects chronological order console.log(fenceB > fenceA); // true console.log(fenceA === fenceB); // false // Sort fences chronologically const fences = ["000000000000003", "000000000000001", "000000000000002"]; const sorted = fences.sort(); // ["001", "002", "003"] ``` -------------------------------- ### Configure Backend Options Source: https://kriasoft.com/syncguard/redis Create a Redis backend with custom keyPrefix and cleanupInIsLocked options. ```ts import { createRedisBackend } from "syncguard/redis"; const backend = createRedisBackend(redis, { keyPrefix: "myapp", // Namespace for all keys (default: "syncguard") cleanupInIsLocked: false, // Enable cleanup in isLocked (default: false) }); ``` -------------------------------- ### setupSchema Source: https://kriasoft.com/syncguard/postgres Initializes the required database tables for the SyncGuard PostgreSQL backend. Use this function to create or update the lock and fence counter tables with optional custom table names. ```APIDOC ## setupSchema ### Description Initializes the required database tables for the SyncGuard PostgreSQL backend. Use this function to create or update the lock and fence counter tables with optional custom table names. ### Method Function call ### Signature `setupSchema(sql, options?)` ### Parameters - `sql` (Sql) - Required - The SQL executor (e.g., from Postgres.js). - `options.tableName` (string) - Optional - Custom name for the lock table. Default: `"syncguard_locks"`. - `options.fenceTableName` (string) - Optional - Custom name for the fence counter table. Default: `"syncguard_fence_counters"`. ### Example ```ts import { setupSchema } from "syncguard/postgres"; await setupSchema(sql, { tableName: "app_locks", fenceTableName: "app_fence_counters", }); ``` ### Notes - Creates required indexes on the lock table: UNIQUE on `lock_id` and B-tree on `expires_at_ms`. - If custom table names are used, ensure indexes are created for both tables (see `postgres/schema.sql`). ``` -------------------------------- ### createPostgresBackend Source: https://kriasoft.com/syncguard/postgres Creates a PostgreSQL backend instance for SyncGuard. Configure table names and cleanup behavior to match your project structure. ```APIDOC ## createPostgresBackend ### Description Creates a PostgreSQL backend instance for SyncGuard. Configure table names and cleanup behavior to match your project structure. ### Method Function call ### Signature `createPostgresBackend(sql, options?)` ### Parameters - `sql` (Sql) - Required - The SQL executor (e.g., from Postgres.js). - `options.tableName` (string) - Optional - Name of the lock table. Default: `"syncguard_locks"`. - `options.fenceTableName` (string) - Optional - Name of the fence counter table. Default: `"syncguard_fence_counters"`. - `options.cleanupInIsLocked` (boolean) - Optional - When `true`, expired locks may be cleaned up during `isLocked()` checks. Default: `false`. ### Example ```ts import { createPostgresBackend } from "syncguard/postgres"; const backend = createPostgresBackend(sql, { tableName: "app_locks", fenceTableName: "app_fence_counters", cleanupInIsLocked: false, }); ``` ### Notes - Lock records and fence counters use separate tables; configure both to match your project structure. - When `cleanupInIsLocked` is enabled, `isLocked()` may perform cleanup of expired locks. Disabled by default to maintain pure read semantics. ``` -------------------------------- ### generateLockId() usage Source: https://kriasoft.com/syncguard/api Example of generating a lockId. Used internally by acquire(); rarely needed in application code. ```typescript import { generateLockId } from "syncguard"; const lockId = generateLockId(); // → "a1b2c3d4e5f6g7h8i9j0k1" (22 base64url chars, 128 bits entropy) ``` -------------------------------- ### createPostgresBackend() - PostgresBackendOptions Source: https://kriasoft.com/syncguard/api Options for creating a PostgreSQL backend, including table names and optional cleanup in isLocked(). ```APIDOC ## createPostgresBackend() - PostgresBackendOptions ### Description Options for creating a PostgreSQL backend for SyncGuard. Allows customizing the table names and enabling cleanup during isLocked() calls. ### Method N/A (SDK function) ### Endpoint N/A (SDK function) ### Parameters #### PostgresBackendOptions - **tableName** (string) - Optional - Name of the locks table (default: "syncguard_locks") - **fenceTableName** (string) - Optional - Name of the fence counters table (default: "syncguard_fence_counters") - **cleanupInIsLocked** (boolean) - Optional - Whether to perform cleanup in isLocked() (default: false) ### Request Example ```typescript import { setupSchema, createPostgresBackend } from "syncguard/postgres"; // Setup schema (once) await setupSchema(sql, { tableName: "app_locks", fenceTableName: "app_fence_counters", }); // Create backend (synchronous) const backend = createPostgresBackend(sql, { tableName: "app_locks", fenceTableName: "app_fence_counters", cleanupInIsLocked: true, // Optional cleanup in isLocked() }); ``` ### Response N/A (SDK function) ``` -------------------------------- ### Bad lock keys Source: https://kriasoft.com/syncguard/core-concepts Examples of poorly chosen lock keys that are too generic, too broad, or random, which defeat the purpose of locking. ```typescript "lock" // Too generic, serializes everything `user:${userId}`; // Too broad, blocks all user operations Math.random().toString(); // Random keys defeat the purpose ``` -------------------------------- ### Dynamic table names based on environment Source: https://kriasoft.com/syncguard/postgres Uses environment-specific prefixes for table names to separate production and development data. The same prefix is applied to both setupSchema and createPostgresBackend. ```ts const prefix = process.env.NODE_ENV === "production" ? "prod" : "dev"; await setupSchema(sql, { tableName: `${prefix}_locks`, fenceTableName: `${prefix}_fence_counters`, }); const backend = createPostgresBackend(sql, { tableName: `${prefix}_locks`, fenceTableName: `${prefix}_fence_counters`, }); ``` -------------------------------- ### createRedisBackend() - RedisBackendOptions Source: https://kriasoft.com/syncguard/api Options for creating a Redis backend, including key prefix and optional cleanup in isLocked(). ```APIDOC ## createRedisBackend() - RedisBackendOptions ### Description Options for creating a Redis backend for SyncGuard. Allows customizing the key prefix and enabling cleanup during isLocked() calls. ### Method N/A (SDK function) ### Endpoint N/A (SDK function) ### Parameters #### RedisBackendOptions - **keyPrefix** (string) - Optional - Prefix for lock keys (default: "syncguard") - **cleanupInIsLocked** (boolean) - Optional - Whether to perform cleanup in isLocked() (default: false) ### Request Example ```typescript const backend = createRedisBackend(redis, { keyPrefix: "my-app", // Keys: "my-app:resource:123" cleanupInIsLocked: true, // Optional cleanup in isLocked() }); ``` ### Response N/A (SDK function) ``` -------------------------------- ### validateLockId() usage Source: https://kriasoft.com/syncguard/api Example of validating a user-provided lockId before releasing a lock. The valid format is exactly 22 base64url characters (^[A-Za-z0-9_-]{22}$). ```typescript import { validateLockId } from "syncguard"; try { validateLockId(userProvidedLockId); await backend.release({ lockId: userProvidedLockId }); } catch (error) { // LockError("InvalidArgument", "Invalid lockId format...") } ``` -------------------------------- ### Redis backend configuration with keyPrefix Source: https://kriasoft.com/syncguard/getting-started Configure the Redis backend with a custom key prefix. Default prefix is "syncguard". ```typescript const lock = createLock(redis, { keyPrefix: "my-app", // Default: "syncguard" }); ``` -------------------------------- ### PostgreSQL backend configuration with custom table names Source: https://kriasoft.com/syncguard/getting-started Set up the schema and create a lock with custom table names for PostgreSQL. Defaults are "syncguard_locks" and "syncguard_fence_counters". ```typescript // Setup schema with custom table names await setupSchema(sql, { tableName: "app_locks", fenceTableName: "app_fence_counters", }); // Create lock with matching config const lock = createLock(sql, { tableName: "app_locks", // Default: "syncguard_locks" fenceTableName: "app_fence_counters", // Default: "syncguard_fence_counters" }); ``` -------------------------------- ### Storage Pattern: Dual-Key Layout Source: https://kriasoft.com/syncguard/redis Shows the Redis keys used by the backend: lock data, reverse index, and fence counter. ```text syncguard:resource:123 → Lock data (JSON with lockId, fence, timestamps) syncguard:id:{lockId} → Reverse index (maps lockId → key) syncguard:fence:resource:123 → Monotonic counter (persists indefinitely) ``` -------------------------------- ### Distributed job processing with lock Source: https://kriasoft.com/syncguard/firestore Example of using the lock function to process a job only if it is pending, preventing duplicate execution across multiple Cloud Functions. ```typescript const processJob = async (jobId: string) => { await lock( async () => { const job = await getJob(jobId); if (job.status === "pending") { await executeJob(job); await markJobComplete(jobId); } }, { key: `job:${jobId}`, ttlMs: 300000 }, ); }; // Safe for multiple Cloud Functions to call simultaneously ``` -------------------------------- ### createRedisBackend() Source: https://kriasoft.com/syncguard/api Creates a Redis backend for manual lock control with custom retry logic or long-running tasks. ```APIDOC ## `createRedisBackend()` ### Description Creates a Redis backend for manual lock control. ### Signature ```typescript import { createRedisBackend } from "syncguard/redis"; ``` ### Parameters - **client** (Redis) - Required - The Redis client instance. - **options** (object) - Optional - Configuration options. - **keyPrefix** (string) - Optional - Prefix for lock keys. Default: "syncguard". - **cleanupInIsLocked** (boolean) - Optional - Whether to clean up in isLocked. Default: false. ### Usage ```typescript import { createRedisBackend } from "syncguard/redis"; import Redis from "ioredis"; const redis = new Redis(); const backend = createRedisBackend(redis, { keyPrefix: "syncguard", cleanupInIsLocked: false, }); ``` ``` -------------------------------- ### Example: Fencing not needed Source: https://kriasoft.com/syncguard/fencing Shows an idempotent operation where fencing is not required. Writing status="completed" multiple times is safe, so a simple lock suffices. ```typescript // Idempotent: writing status="completed" multiple times is safe await lock( async () => { await updateJobStatus(jobId, "completed"); }, { key: `job:${jobId}` }, ); ``` -------------------------------- ### postgres.js connection pooling configuration Source: https://kriasoft.com/syncguard/postgres Configure postgres.js connection pooling for optimal performance. Adjust max, idle_timeout, and connect_timeout as needed. ```typescript const sql = postgres("postgresql://localhost:5432/myapp", { max: 20, // Connection pool size idle_timeout: 60, // Seconds before idle connections are closed connect_timeout: 30, // Seconds before connection attempt times out }); ``` -------------------------------- ### Scheduled task coordination with lock Source: https://kriasoft.com/syncguard/firestore Example of using the lock function to ensure only one Cloud Scheduler job executes a daily report. Returns whether the lock was acquired. ```typescript // Multiple Cloud Scheduler jobs, only one executes export async function dailyReport(req: Request, res: Response) { const today = new Date().toISOString().split("T")[0]; const acquired = await lock( async () => { await generateDailyReport(); return true; }, { key: `daily-report:${today}`, ttlMs: 3600000 }, ); // 1 hour res.status(200).send({ executed: acquired }); } ``` -------------------------------- ### createFirestoreBackend() Source: https://kriasoft.com/syncguard/api Creates a Firestore backend for manual lock control. Requires a single-field ascending index on the `lockId` field for optimal performance. ```APIDOC ## `createFirestoreBackend()` ### Description Creates a Firestore backend for manual lock control. ### Signature ```typescript import { createFirestoreBackend } from "syncguard/firestore"; ``` ### Parameters - **db** (Firestore) - Required - The Firestore database instance. - **options** (object) - Optional - Configuration options. - **collection** (string) - Optional - Collection name for locks. Default: "locks". - **fenceCollection** (string) - Optional - Collection name for fence counters. Default: "fence_counters". - **cleanupInIsLocked** (boolean) - Optional - Whether to clean up in isLocked. Default: false. ### Usage ```typescript import { createFirestoreBackend } from "syncguard/firestore"; import { Firestore } from "@google-cloud/firestore"; const db = new Firestore(); const backend = createFirestoreBackend(db, { collection: "locks", fenceCollection: "fence_counters", cleanupInIsLocked: false, }); ``` ### Index Required Firestore requires a single-field ascending index on the `lockId` field for optimal performance. ``` -------------------------------- ### Check for required indexes on syncguard_locks Source: https://kriasoft.com/syncguard/postgres Run this SQL query to verify that the required indexes exist on the syncguard_locks table. Expected indexes are PRIMARY KEY on key, UNIQUE INDEX on lock_id, and INDEX on expires_at_ms. ```SQL -- Check if required indexes exist SELECT indexname, indexdef FROM pg_indexes WHERE tablename = 'syncguard_locks'; -- Expected indexes: -- - PRIMARY KEY on key -- - UNIQUE INDEX on lock_id -- - INDEX on expires_at_ms ``` -------------------------------- ### hasFence() usage in generic context Source: https://kriasoft.com/syncguard/api Example of using hasFence() in a generic function that accepts unknown backend types. Most application code uses typed backends and doesn't need this guard. ```typescript import { hasFence } from "syncguard"; // Generic function accepting unknown backend types function processWithAnyBackend( result: AcquireResult, ) { if (hasFence(result)) { // Type guard for generic contexts console.log("Fence:", result.fence); } } ``` -------------------------------- ### backend.release() Source: https://kriasoft.com/syncguard/api Release lock ownership. ```APIDOC ## `backend.release()` ### Description Release lock ownership. ### Signature ```typescript release(opts: { lockId: string; signal?: AbortSignal; }): Promise; ``` ### Parameters - **lockId** (string) - Required - The lock ID returned from acquire. - **signal** (AbortSignal) - Optional - Abort signal for cancellation. ### Returns - `ReleaseResult` - The result of the release operation. ``` -------------------------------- ### Configure Lock Options Source: https://kriasoft.com/syncguard/redis Acquire a lock with custom key, TTL, and acquisition timeout and retry settings. ```ts await lock( async () => { // Your work function }, { key: "resource:123", // Required: unique identifier ttlMs: 30000, // Lock duration (default: 30s) acquisition: { timeoutMs: 5000, // Max acquisition wait (default: 5s) maxRetries: 10, // Retry attempts (default: 10) }, }, ); ``` -------------------------------- ### Quick Start with Firestore Source: https://kriasoft.com/syncguard/getting-started Create a lock using Firestore. For optimal performance, create a single-field ascending index on the lockId field in the locks collection; Firestore typically auto-creates this for equality queries. ```typescript import { createLock } from "syncguard/firestore"; import { Firestore } from "@google-cloud/firestore"; const db = new Firestore(); const lock = createLock(db); await lock( async () => { // Your critical section }, { key: "resource:123" }, ); ``` -------------------------------- ### Acquire fence with typed backend (Redis) Source: https://kriasoft.com/syncguard/fencing Shows how to acquire a fencing token from a typed backend (e.g., Redis) and use it directly. TypeScript knows the fence exists, so no runtime assertions or optional chaining are needed. The fence is compared to a last known value before updating. ```typescript import { createRedisBackend } from "syncguard/redis"; const backend = createRedisBackend(redis); const result = await backend.acquire({ key: "resource:123", ttlMs: 30000 }); if (result.ok) { // TypeScript knows fence exists! const fence = result.fence; // Type: string (no undefined) // Direct comparison works if (fence > lastKnownFence) { await updateWithFence(data, fence); } } ``` -------------------------------- ### owns() usage example Source: https://kriasoft.com/syncguard/api Checks if you still own a lock and logs the result. For diagnostics, UI, and monitoring only; never use owns() → mutate patterns because correctness relies on atomic ownership verification in release() and extend(). ```typescript import { owns } from "syncguard"; if (await owns(backend, lockId)) { console.log("Still own the lock"); } else { console.log("Lost ownership"); } ``` -------------------------------- ### createFirestoreBackend() - FirestoreBackendOptions Source: https://kriasoft.com/syncguard/api Options for creating a Firestore backend, including collection names and optional cleanup in isLocked(). ```APIDOC ## createFirestoreBackend() - FirestoreBackendOptions ### Description Options for creating a Firestore backend for SyncGuard. Allows customizing the collection and fence collection names, and enabling cleanup during isLocked() calls. ### Method N/A (SDK function) ### Endpoint N/A (SDK function) ### Parameters #### FirestoreBackendOptions - **collection** (string) - Optional - Name of the locks collection (default: "locks") - **fenceCollection** (string) - Optional - Name of the fence counters collection (default: "fence_counters") - **cleanupInIsLocked** (boolean) - Optional - Whether to perform cleanup in isLocked() (default: false) ### Request Example ```typescript const backend = createFirestoreBackend(db, { collection: "app_locks", fenceCollection: "app_fences", cleanupInIsLocked: true, // Optional cleanup in isLocked() }); ``` ### Response N/A (SDK function) ``` -------------------------------- ### Acquire lock and log fence token Source: https://kriasoft.com/syncguard/fencing Shows how to acquire a lock and access the returned fence token. The fence is a 15-digit zero-padded string that compares lexicographically. ```typescript const result = await backend.acquire({ key: "document:123", ttlMs: 30000 }); if (result.ok) { const { lockId, fence } = result; console.log(fence); // "000000000000001" } ``` -------------------------------- ### Basic pattern: check-and-write with fence token Source: https://kriasoft.com/syncguard/fencing Shows how to store the latest fence token with data and reject writes from older fences. The code fetches the current document, compares fences, and throws an error if stale. It releases the lock in a finally block. ```typescript const result = await backend.acquire({ key: "document:123", ttlMs: 30000 }); if (result.ok) { try { const { fence } = result; // Fetch current document const doc = await getDocument("123"); // Check if our fence is newer (we haven't lost the lock) if (doc.fence && fence <= doc.fence) { throw new Error(`Stale lock: our fence ${fence} <= current ${doc.fence}`); } // Safe: we hold the newest lock await updateDocument("123", { data: processData(), fence, // Store fence with the data }); } finally { await backend.release({ lockId: result.lockId }); } } ``` -------------------------------- ### Acquire, extend, and release a lock with lockId Source: https://kriasoft.com/syncguard/core-concepts Shows how to acquire a lock, then use the returned `lockId` to extend or release the lock. Only the owner can perform these operations. ```typescript const result = await backend.acquire({ key: "resource:123", ttlMs: 30000 }); if (result.ok) { const { lockId } = result; // Only this lockId can release/extend this specific lock await backend.extend({ lockId, ttlMs: 30000 }); await backend.release({ lockId }); } ``` -------------------------------- ### Configure PostgreSQL connection with timeouts Source: https://kriasoft.com/syncguard/postgres Use this configuration to set connection pool size and timeouts. Adjust these values to resolve ServiceUnavailable errors. ```TypeScript const sql = postgres("postgresql://localhost:5432/myapp", { max: 20, // Connection pool size connect_timeout: 30, // Connection timeout (seconds) idle_timeout: 60, // Idle connection timeout (seconds) max_lifetime: 3600, // Max connection lifetime (seconds) }); ``` -------------------------------- ### Dual-table storage pattern for locks and fence counters Source: https://kriasoft.com/syncguard/postgres Shows the schema for the dual-table pattern: syncguard_locks for lock records and syncguard_fence_counters for persistent fence counters. ```text syncguard_locks (lock records) key → PRIMARY KEY lock_id → UNIQUE INDEX (enables keyless release/extend) expires_at_ms → INDEX (enables efficient cleanup) acquired_at_ms → Timestamp fence → Fencing token (15-digit string) user_key → Original user key for diagnostics syncguard_fence_counters (fence counters) fence_key → PRIMARY KEY (derived from user key) fence → BIGINT counter (persists indefinitely) ``` -------------------------------- ### Pattern matching on AcquireResult Source: https://kriasoft.com/syncguard/api Shows how to use the discriminated union to access lockId and fence when acquisition succeeds, or log the contention reason when it fails. ```typescript const result = await backend.acquire({ key: "resource:123", ttlMs: 30000 }); if (result.ok) { // TypeScript knows: lockId, expiresAtMs, fence are available const { lockId, fence } = result; } else { // TypeScript knows: reason is "locked" console.log("Contention:", result.reason); } ``` -------------------------------- ### Rate limiting with backend.acquire Source: https://kriasoft.com/syncguard/what-is-syncguard Uses the core backend object to acquire a lock for rate limiting. Throws an error if the lock is not acquired. ```typescript // One request per user per minute const result = await backend.acquire({ key: `rate:${userId}`, ttlMs: 60000, }); if (!result.ok) throw new Error("Rate limit exceeded"); ``` -------------------------------- ### Compare fence tokens lexicographically Source: https://kriasoft.com/syncguard/fencing Demonstrates that fence tokens are zero-padded strings that can be compared directly with string operators. This works because the fixed 15-digit format ensures chronological order. ```typescript const first = "000000000000001"; const second = "000000000000002"; console.log(second > first); // true (string comparison works!) console.log(first === second); // false ``` -------------------------------- ### Check NTP status with timedatectl Source: https://kriasoft.com/syncguard/firestore Command to verify system clock synchronization on Linux/macOS. Expected output includes 'System clock synchronized: yes'. Check offset against operational thresholds. ```bash timedatectl status ``` -------------------------------- ### Environment-Specific Key Prefix Source: https://kriasoft.com/syncguard/redis Use a prefix based on NODE_ENV to avoid collisions between environments. ```ts const prefix = process.env.NODE_ENV === "production" ? "prod:locks" : "dev:locks"; const backend = createRedisBackend(redis, { keyPrefix: prefix }); ```