Verrou Documentation

website·Indexed 32 days ago

https://verrou.dev/docs/introduction

A Node.js library for managing locks with support for multiple drivers including Redis, Postgres, MySQL, and Sqlite. Features an ESM-only package requiring Node.js 18 or higher and provides integration for AdonisJS via the @adonisjs/lock package.

Tokens
4.6K
Snippets
29
Records
29
Agent score
64%

What's inside Verrou

  1. Initialize Verrou with multiple storage drivers

    Verrou allows you to configure multiple locking stores (drivers) and define a default driver. This architecture enables switching between drivers (e.g., using Redis in production and In-Memory for testing) without changing the application logic.
    import { Verrou } from '@verrou/core'
    import { redisStore } from '@verrou/core/drivers/redis'
    import { memoryStore } from '@verrou/core/drivers/memory'
    
    const verrou = new Verrou({
      default: 'redis',
      stores: {
        redis: { driver: redisStore() },
        memory: { driver: memoryStore() }
      }
    })
  2. Integrate Verrou with AdonisJS

    To use Verrou in an AdonisJS application, use the @adonisjs/lock package. This package provides the necessary integration to manage locks within the AdonisJS ecosystem.
    npm install @adonisjs/lock
  3. Install Verrou

    Verrou is an ESM-only package and requires Node.js 18 or higher. It can be installed via npm, pnpm, bun, or yarn.
    npm i @verrou/core
  4. Create a custom DatabaseAdapter for DatabaseStore

    If you want to use a database for locks without implementing a full driver, you can create an adapter by implementing the DatabaseAdapter interface from @verrou/core/types. This adapter is then passed to the DatabaseStore class. The adapter must handle table creation, lock insertion, acquisition (checking expiration), deletion, extension, and retrieval.
    import { DatabaseStore } from '@verrou/core/drivers/database'
    import type { DatabaseAdapter } from '@verrou/core/types'
    
    export class PrismaAdapter implements DatabaseAdapter {
      setTableName(tableName: string): void { /* ... */ }
      async createTableIfNotExists(): Promise<void> { /* ... */ }
      async insertLock(lock: { key: string; owner: string; expiration: number | null }): Promise<void> { /* ... */ }
      async acquireLock(lock: { key: string; owner: string; expiration: number | null }): Promise<number> { /* ... */ }
      async deleteLock(key: string, owner?: string): Promise<void> { /* ... */ }
      async extendLock(key: string, owner: string, duration: number): Promise<number> { /* ... */ }
      async getLock(key: string): Promise<{ owner: string; expiration: number | null } | undefined> { /* ... */ }
    }
    
    export function prismaStore(config: PrismaOptions) {
      return {
        config,
        factory: () => {
          const adapter = new PrismaAdapter(config.connection)
          return new DatabaseStore(adapter, config)
        },
      }
    }
  5. Create a Lock instance

    Create a Lock instance using verrou.createLock(resource, duration). The resource is a unique string identifier for the lock. The duration is the lease time after which the lock automatically expires to prevent deadlocks if a process crashes. Duration can be a number (milliseconds), a string (e.g., '1s', '1m'), or null for a lock that never expires. Note that creating a lock instance does not acquire it.
    import { verrou } from './verrou.js'
    
    // Create a lock for 'my-resource' that expires in 1 second
    const lock = verrou.createLock('my-resource', '1s')
  6. Share and release locks across multiple processes

    By default, only the Lock instance that acquired the lock can release it. To release a lock in a different process (e.g., a worker processing a queue), you must serialize the lock in the originating process using lock.serialize() and restore it in the target process using verrou.restoreLock(serializedLock). Alternatively, lock.forceRelease() can be used to release a lock regardless of the owner.
    // Process A: Acquire and serialize
    const lock = verrou.createLock('my-resource', '2s')
    await lock.acquire()
    const serialized = lock.serialize()
    // Send 'serialized' to Process B via queue/API
    
    // Process B: Restore and release
    import { verrou } from './verrou.js'
    const restoredLock = verrou.restoreLock(serialized)
    await restoredLock.release()
  7. Extend a lock lease duration

    If a critical section takes longer than expected, use the extend() method to refresh the lock lease. Calling extend() without arguments refreshes the lock for its original duration. Passing a duration string (e.g., '5s') sets a specific new expiration. You can use lock.isExpired() or lock.getRemainingTime() to monitor the lock status.
    import { verrou } from './verrou.js'
    
    const lock = verrou.createLock('my-resource', '10s')
    await lock.acquire()
    
    // Refresh for the original duration (10s)
    await lock.extend()
    
    // Refresh for a specific duration (5s)
    await lock.extend('5s')
  8. Prevent race conditions using automatic locks

    To prevent multiple processes or concurrent code from accessing a shared resource simultaneously (a race condition), use verrou.createLock(name).run(callback). The lock is acquired before the callback executes and is automatically released once the callback completes.
    router.get('/transfer', async () => {
      // Other requests will wait here until the lock is released
      await verrou.createLock('transfer').run(async () => {
        const fromAccount = getAccountFromDb(request.input('from'))
        const toAccount = getAccountFromDb(request.input('to'))
    
        fromAccount.balance -= request.input('amount')
        toAccount.balance += request.input('amount')
    
        await fromAccount.save()
        await toAccount.save()
      }) // Lock is automatically released after the callback is executed
    })
    
  9. Implement a custom LockStore driver

    To create a custom lock driver for Verrou, implement the LockStore interface from @verrou/core/types. You must then provide a factory function that returns a CreateDriverResult containing the configuration and a factory method to instantiate the driver. This driver can then be registered in the Verrou configuration under the stores object.
    import { Verrou } from '@verrou/core'
    import type { LockStore, CreateDriverResult } from '@verrou/core/types'
    
    // 1. Implement the interface
    class MyDriver implements LockStore {
      async save(key: string, owner: string, ttl: number | undefined): Promise<boolean> { /* ... */ return true }
      async delete(key: string, owner: string): Promise<void> { /* ... */ }
      async exists(key: string): Promise<boolean> { /* ... */ return false }
      async extend(key: string, owner: string, duration: number): Promise<void> { /* ... */ }
    }
    
    // 2. Create the factory function
    export function myStore(config: MyStoreOptions): CreateDriverResult<MyStoreOptions> {
      return { config, factory: () => new MyDriver(config) }
    }
    
    // 3. Use it in Verrou
    const verrou = new Verrou({
      default: 'myStore',
      stores: {
        myStore: myStore({ /* Your driver options */ })
      }
    })
  10. Execute critical sections automatically with `run`

    The run() method provides a concise way to acquire a lock, execute a callback function, and automatically release the lock once the callback completes. It returns a tuple where the first element is a boolean indicating if the lock was successfully acquired.
    import { verrou } from './verrou.js'
    
    const lock = verrou.createLock('my-resource', '2s')
    
    const [acquired, result] = await lock.run(async () => {
      return await doSomething()
    })
    
    if (!acquired) return 'Lock not acquired'
  11. Manually acquire and release a lock

    Use the acquire() and release() methods to manually manage a lock. Because acquire() is asynchronous and can fail, it returns a boolean indicating success. To ensure a lock is always released even if an error occurs during the critical section, wrap the logic in a try...finally block.
    import { verrou } from './verrou.js'
    
    const lock = verrou.createLock('my-resource', null)
    const acquired = await lock.acquire()
    
    if (!acquired) return 'Lock not acquired'
    
    try {
      // Critical section
      doSomething()
    } catch (error) {
      // Handle error
    } finally {
      await lock.release()
    }
  12. Test a custom driver for compliance using Japa

    Verrou provides a built-in test suite to ensure custom drivers comply with expected behavior. This requires @japa/runner and @japa/assert. Use the registerStoreTestSuite function from @verrou/core/test_suite within a Japa test group.
    import { test } from '@japa/runner'
    import { MyStore } from '../src/my_store.js'
    import { registerStoreTestSuite } from '@verrou/core/test_suite'
    
    test.group('My Store', (group) => {
      registerStoreTestSuite({
        test,
        createStore: () => new MyStore()
      })
    })