# eridu-tech > Mastering eridu-tech: Comprehensive Guides for the Backend Server Component Library ## search - [Search the documentation](/search.md) ## docs - [Tags](/docs/tags.md) - [One doc tagged with "Analog.js"](/docs/tags/analog-js.md) - [8 docs tagged with "AOP"](/docs/tags/aop.md) - [One doc tagged with "Aws s3"](/docs/tags/aws-s-3.md) - [7 docs tagged with "Cache"](/docs/tags/cache.md) - [7 docs tagged with "CircuitBreaker"](/docs/tags/circuit-breaker.md) - [One doc tagged with "Cloudflare r2"](/docs/tags/cloudflare-r-2.md) - [10 docs tagged with "Configuring adapters"](/docs/tags/configuring-adapters.md) - [One doc tagged with "Container"](/docs/tags/container.md) - [10 docs tagged with "Creating adapters"](/docs/tags/creating-adapters.md) - [2 docs tagged with "Creating database adapters"](/docs/tags/creating-database-adapters.md) - [11 docs tagged with "Dependency injection"](/docs/tags/dependency-injection.md) - [11 docs tagged with "DI"](/docs/tags/di.md) - [One doc tagged with "Digital ocean spaces"](/docs/tags/digital-ocean-spaces.md) - [7 docs tagged with "Event-bus"](/docs/tags/event-bus.md) - [6 docs tagged with "FileStorage"](/docs/tags/file-storage.md) - [One doc tagged with "File system"](/docs/tags/file-system.md) - [One doc tagged with "Ftp"](/docs/tags/ftp.md) - [One doc tagged with "Hono"](/docs/tags/hono.md) - [13 docs tagged with "HttpRouter"](/docs/tags/http-router.md) - [8 docs tagged with "In-memory"](/docs/tags/in-memory.md) - [7 docs tagged with "Kysely"](/docs/tags/kysely.md) - [7 docs tagged with "Lock"](/docs/tags/lock.md) - [One doc tagged with "Middleware"](/docs/tags/middleware.md) - [One doc tagged with "Middleware - Utilities"](/docs/tags/middleware-utilities.md) - [8 docs tagged with "Middlewares"](/docs/tags/middlewares.md) - [One doc tagged with "Minio"](/docs/tags/minio.md) - [7 docs tagged with "Mongodb"](/docs/tags/mongodb.md) - [One doc tagged with "Next.js App Router"](/docs/tags/next-js-app-router.md) - [One doc tagged with "Next.js Pages Router"](/docs/tags/next-js-pages-router.md) - [9 docs tagged with "NoOp"](/docs/tags/no-op.md) - [One doc tagged with "Node.js"](/docs/tags/node-js.md) - [One doc tagged with "Nuxt"](/docs/tags/nuxt.md) - [8 docs tagged with "Plugins"](/docs/tags/plugins.md) - [7 docs tagged with "Postgres"](/docs/tags/postgres.md) - [One doc tagged with "Propagation"](/docs/tags/propagation.md) - [7 docs tagged with "RateLimiter"](/docs/tags/rate-limiter.md) - [7 docs tagged with "Redis"](/docs/tags/redis.md) - [One doc tagged with "Resolver"](/docs/tags/resolver.md) - [8 docs tagged with "Resolvers"](/docs/tags/resolvers.md) - [One doc tagged with "Routing"](/docs/tags/routing.md) - [2 docs tagged with "Schema"](/docs/tags/schema.md) - [7 docs tagged with "Semaphore"](/docs/tags/semaphore.md) - [One doc tagged with "Sftp"](/docs/tags/sftp.md) - [7 docs tagged with "SharedLock"](/docs/tags/shared-lock.md) - [One doc tagged with "Signed"](/docs/tags/signed.md) - [One doc tagged with "SolidStart"](/docs/tags/solid-start.md) - [7 docs tagged with "Sqlite"](/docs/tags/sqlite.md) - [One doc tagged with "Supabase Storage"](/docs/tags/supabase-storage.md) - [One doc tagged with "SvelteKit"](/docs/tags/svelte-kit.md) - [One doc tagged with "TanStack Start"](/docs/tags/tan-stack-start.md) - [One doc tagged with "Tigris"](/docs/tags/tigris.md) - [6 docs tagged with "TransactionContext"](/docs/tags/transaction-context.md) - [One doc tagged with "Transactions"](/docs/tags/transactions.md) - [10 docs tagged with "Usage"](/docs/tags/usage.md) - [13 docs tagged with "Utilities"](/docs/tags/utilities.md) - [2 docs tagged with "Validation"](/docs/tags/validation.md) - [One doc tagged with "Webdav"](/docs/tags/webdav.md) - [One doc tagged with "WinterTC"](/docs/tags/winter-tc.md) - [Configuring Lock adapters](/docs/concurrency/lock/configuring_lock_adapters.md): MemoryLockAdapter - [Creating Lock adapters](/docs/concurrency/lock/creating_lock_adapters.md): Implementing your custom ILockAdapter - [LockFactoryResolver](/docs/concurrency/lock/lock_factory_resolver.md): The LockFactoryResolver class provides a flexible way to configure and switch between different lock adapters at runtime. - [Lock middlewares](/docs/concurrency/lock/lock_middlewares.md): Initial configuration - [Lock Plugins](/docs/concurrency/lock/lock_plugin.md): withLockPrefix plugin - [Lock usage](/docs/concurrency/lock/lock_usage.md): The eridu-tech/lock component provides a way for managing locks independent of underlying platform or storage. - [DI Integration](/docs/concurrency/lock/lock-di.md): The lockFactoryResolverDiFactory function creates an ILockFactoryResolver whose underlying resolver is resolved from a DI container. - [Concurrency](/docs/concurrency/overview.md): (); const memoryLockAdapter = new MemoryLockAdapter(map); ``` info `MemoryLockAdapter` lets you test your app without external dependencies like `Redis`, ideal for local development, unit tests, integration tests and fast E2E test for the backend application. danger Note the `MemoryLockAdapter` is limited to single process usage and cannot be shared across multiple servers or processes. ### Settings[​](#settings "Direct link to Settings") To clean up expired lock keys, call `removeAllExpired` at a regular interval (for example, using a cron job): ./samples/memory-lock-remove-all-expired.ts ``` import { MemoryLockAdapter } from "eridu-tech/lock/memory-lock-adapter"; const memoryLockAdapter = new MemoryLockAdapter(); // Remove all expired lock keys manually. await memoryLockAdapter.removeAllExpired(); ``` info To remove the lock map and all stored lock data, use `deInit` method: ./samples/memory-lock-adapter-deinit.ts ``` import { memoryLockAdapter } from "./memory-lock-adapter.js"; await memoryLockAdapter.deInit(); ``` ## MongodbLockAdapter[​](#mongodblockadapter "Direct link to MongodbLockAdapter") To use the `MongodbLockAdapter`, you'll need to: 1. Install the required dependency: [`mongodb`](https://www.npmjs.com/package/mongodb) package. ### Setup[​](#setup "Direct link to Setup") Connect to MongoDB: ./samples/mongodb-lock-adapter-setup.ts ``` import { MongoClient } from "mongodb"; const client = await MongoClient.connect("YOUR_MONGODB_CONNECTION_STRING"); export const database = client.db("database"); ``` info The `database` setting also accepts a [`TransactionContext`](/docs/foundation/transaction_context/transaction_context_usage/.md), which makes the adapter transaction aware. Adapters given the same instance share the same transaction when available. ### Usage[​](#usage "Direct link to Usage") Create the adapter: ./samples/mongodb-lock-adapter.ts ``` import { MongodbLockAdapter } from "eridu-tech/lock/mongodb-lock-adapter"; import { database } from "./mongodb-lock-adapter-setup.js"; export const mongodbLockAdapter = new MongodbLockAdapter({ database, }); // You need initialize the adapter once before using it. // During the initialization the indexes will be created await mongodbLockAdapter.init(); ``` You can change the collection name: ./samples/mongodb-lock-collection-name.ts ``` import { MongodbLockAdapter } from "eridu-tech/lock/mongodb-lock-adapter"; import { database } from "./mongodb-lock-adapter-setup.js"; const mongodbLockAdapter = new MongodbLockAdapter({ database, // By default "lock" is used as collection name collectionName: "my-lock", }); await mongodbLockAdapter.init(); ``` You can change the collection settings: ./samples/mongodb-lock-collection-settings.ts ``` import { MongodbLockAdapter } from "eridu-tech/lock/mongodb-lock-adapter"; import { database } from "./mongodb-lock-adapter-setup.js"; const mongodbLockAdapter = new MongodbLockAdapter({ database, // You configure additional collection settings collectionSettings: {}, }); await mongodbLockAdapter.init(); ``` info To remove the lock collection and all stored lock data, use `deInit` method: ./samples/mongodb-lock-adapter-deinit.ts ``` import { mongodbLockAdapter } from "./mongodb-lock-adapter.js"; await mongodbLockAdapter.deInit(); ``` danger Note in order to use `MongodbLockAdapter` correctly, ensure you use a single, consistent database across all server instances or processes. ## RedisLockAdapter[​](#redislockadapter "Direct link to RedisLockAdapter") To use the `RedisLockAdapter`, you'll need to: 1. Install the required dependency: [`ioredis`](https://www.npmjs.com/package/ioredis) package. ### Setup[​](#setup-1 "Direct link to Setup") Connect to Redis: ./samples/redis-lock-adapter-setup.ts ``` import { Redis } from "ioredis"; export const database = new Redis("YOUR_REDIS_CONNECTION_STRING"); ``` ### Usage[​](#usage-1 "Direct link to Usage") Create the adapter: ./samples/redis-lock-adapter.ts ``` import { RedisLockAdapter } from "eridu-tech/lock/redis-lock-adapter"; import { database } from "./redis-lock-adapter-setup.js"; const redisLockAdapter = new RedisLockAdapter(database); ``` danger Note in order to use `RedisLockAdapter` correctly, ensure you use a single, consistent database across all server instances or processes. ## KyselyLockAdapter[​](#kyselylockadapter "Direct link to KyselyLockAdapter") To use the `KyselyLockAdapter`, you'll need to: 1. Use database provider that has support for transactions. 2. Install the required dependency: [`kysely`](https://www.npmjs.com/package/kysely) package. ### Setup[​](#setup-2 "Direct link to Setup") Create a function that creates the [`TransactionContext`](/docs/foundation/transaction_context/transaction_context_usage/.md) by wrapping a `Kysely` instance in a [`KyselyTransactionAdapter`](/docs/foundation/transaction_context/configuring_transaction_context_adapters/.md#kysely_transaction_adapter): ./samples/kysely-lock-adapter-setup.ts ``` import { ExecutionContext } from "eridu-tech/execution-context"; import { AlsExecutionContextAdapter } from "eridu-tech/execution-context/als-execution-context-adapter"; import { contextToken } from "eridu-tech/execution-context/contracts"; import { TransactionContext } from "eridu-tech/transaction-context"; import { KyselyTransactionAdapter } from "eridu-tech/transaction-context/kysely-transaction-adapter"; import type { ITransactionContext } from "eridu-tech/transaction-context/contracts"; import type { Kysely } from "kysely"; const executionContext = new ExecutionContext(new AlsExecutionContextAdapter()); // `KyselyLockAdapter` is transaction aware: it runs every lock operation // through the `current` client of this context, which is the transaction-scoped // client while a transaction is active and the base client otherwise. export function createTransactionContext( kysely: Kysely, ): ITransactionContext> { return new TransactionContext>({ token: contextToken("kysely"), adapter: new KyselyTransactionAdapter({ database: kysely, }), executionContext, }); } ``` info The `transactionContext` setting makes the adapter transaction aware. Adapters given the same instance share the same transaction when available. ### With Sqlite[​](#with-sqlite "Direct link to With Sqlite") You will need to install [`better-sqlite3`](https://www.npmjs.com/package/better-sqlite3) package: ./samples/kysely-lock-sqlite.ts ``` import { KyselyLockAdapter } from "eridu-tech/lock/kysely-lock-adapter"; import Sqlite from "better-sqlite3"; import { Kysely, SqliteDialect } from "kysely"; import { createTransactionContext } from "./kysely-lock-adapter-setup.js"; const database = new Sqlite("DATABASE_NAME.db"); const kysely = new Kysely({ dialect: new SqliteDialect({ database, }), }); const transactionContext = createTransactionContext(kysely); export const kyselyLockAdapter = new KyselyLockAdapter({ transactionContext, }); // You need initialize the adapter once before using it. // During the initialization the schema will be created await kyselyLockAdapter.init(); ``` danger Note using `KyselyLockAdapter` with `sqlite` is limited to single server usage and cannot be shared across multiple servers but it can be shared between different processes. To use it correctly, ensure all process instances access the same persisted database. ### With Postgres[​](#with-postgres "Direct link to With Postgres") You will need to install [`pg`](https://www.npmjs.com/package/pg) package: ./samples/kysely-lock-postgres.ts ``` import { KyselyLockAdapter } from "eridu-tech/lock/kysely-lock-adapter"; import { Pool } from "pg"; import { Kysely, PostgresDialect } from "kysely"; import { createTransactionContext } from "./kysely-lock-adapter-setup.js"; const database = new Pool({ database: "DATABASE_NAME", host: "DATABASE_HOST", user: "DATABASE_USER", // DATABASE port port: 5432, password: "DATABASE_PASSWORD", max: 10, }); const kysely = new Kysely({ dialect: new PostgresDialect({ pool: database, }), }); const transactionContext = createTransactionContext(kysely); const kyselyLockAdapter = new KyselyLockAdapter({ transactionContext, }); // You need initialize the adapter once before using it. // During the initialization the schema will be created await kyselyLockAdapter.init(); ``` danger Note in order to use `KyselyLockAdapter` with `postgres` correctly, ensure you use a single, consistent database across all server instances. This means you can't use replication. ### Settings[​](#settings-1 "Direct link to Settings") To clean up expired lock keys, call `removeAllExpired` at a regular interval (for example, using a cron job): ./samples/kysely-lock-remove-all-expired.ts ``` import { kyselyLockAdapter } from "./kysely-lock-sqlite.js"; // Remove all expired lock keys manually. await kyselyLockAdapter.removeAllExpired(); ``` info To remove the lock table and all stored lock data, use `deInit` method: ./samples/kysely-lock-adapter-deinit.ts ``` import { kyselyLockAdapter } from "./kysely-lock-sqlite.js"; await kyselyLockAdapter.deInit(); ``` ## NoOpLockAdapter[​](#nooplockadapter "Direct link to NoOpLockAdapter") The `NoOpLockAdapter` is a no-operation implementation, it performs no actions when called: ./samples/no-op-lock-adapter.ts ``` import { NoOpLockAdapter } from "eridu-tech/lock/no-op-lock-adapter"; const noOpLockAdapter = new NoOpLockAdapter(); ``` info The `NoOpLockAdapter` is useful when you want to mock out or disable your [`LockFactory`](https://eridu-tech.github.io/eridu-tech-core/classes/Lock.LockFactory.html) instance. ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/lock`](https://eridu-tech.github.io/eridu-tech-core/modules/Lock.html) API docs. --- # Creating Lock adapters ## Implementing your custom ILockAdapter[​](#implementing-your-custom-ilockadapter "Direct link to Implementing your custom ILockAdapter") In order to create an adapter you need to implement the [`ILockAdapter`](https://eridu-tech.github.io/eridu-tech-core/types/Lock.ILockAdapter.html) contract. ## Testing your custom ILockAdapter[​](#testing-your-custom-ilockadapter "Direct link to Testing your custom ILockAdapter") We provide a complete test suite to test your lock adapter implementation. Simply use the [`lockAdapterTestSuite`](https://eridu-tech.github.io/eridu-tech-core/functions/Lock.lockAdapterTestSuite.html) function: The suite provides preconfigured Vitest test cases with common edge case coverage and standardized lock adapter contract conformance testing. Usage example: ./samples/lock-adapter-test-suite.ts ``` import { beforeEach, describe, expect, test } from "vitest"; import { lockAdapterTestSuite } from "eridu-tech/lock/test-utilities"; import { MemoryLockAdapter } from "eridu-tech/lock/memory-lock-adapter"; describe("class: MyLockAdapter", () => { lockAdapterTestSuite({ createAdapter: () => new MemoryLockAdapter(), test, beforeEach, expect, describe, }); }); ``` ## Implementing your custom ILockFactory class[​](#implementing-your-custom-ilockfactory-class "Direct link to Implementing your custom ILockFactory class") In some cases, you may need to implement a custom [`LockFactory`](https://eridu-tech.github.io/eridu-tech-core/classes/Lock.LockFactory.html) class to optimize performance for your specific technology stack. You can then directly implement the [`ILockFactory`](https://eridu-tech.github.io/eridu-tech-core/types/Lock.ILockFactory.html) contract. ## Testing your custom ILockFactory class[​](#testing-your-custom-ilockfactory-class "Direct link to Testing your custom ILockFactory class") We provide a complete test suite to verify your custom lock factory class implementation. Simply use the [`lockFactoryTestSuite`](https://eridu-tech.github.io/eridu-tech-core/functions/Lock.lockFactoryTestSuite.html) function: The suite provides preconfigured Vitest test cases with common edge case coverage and standardized lock factory contract conformance testing. Usage example: ./samples/lock-factory-test-suite.ts ``` import { beforeEach, describe, expect, test } from "vitest"; import { lockFactoryTestSuite } from "eridu-tech/lock/test-utilities"; import { LockFactory } from "eridu-tech/lock"; import { MemoryLockAdapter } from "eridu-tech/lock/memory-lock-adapter"; import { SuperJsonSerde } from "eridu-tech/serde/super-json-serde"; describe("class: MyLockFactory", () => { lockFactoryTestSuite({ createLockFactory: () => ({ lockFactory: new LockFactory({ adapter: new MemoryLockAdapter(), }), serde: new SuperJsonSerde(), }), test, beforeEach, expect, describe, }); }); ``` ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/lock`](https://eridu-tech.github.io/eridu-tech-core/modules/Lock.html) API docs. --- # LockFactoryResolver The `LockFactoryResolver` class provides a flexible way to configure and switch between different lock adapters at runtime. ## Initial configuration[​](#initial-configuration "Direct link to Initial configuration") To begin using the `ILockFactoryResolver`, you will need to register all required adapters during initialization. ./samples/lock-factory-resolver-initial-config.ts ``` import { LockFactoryResolver } from "eridu-tech/lock"; import { MemoryLockAdapter } from "eridu-tech/lock/memory-lock-adapter"; import { RedisLockAdapter } from "eridu-tech/lock/redis-lock-adapter"; import { Redis } from "ioredis"; export const lockFactoryResolver = new LockFactoryResolver({ adapters: { memory: new MemoryLockAdapter(), redis: new RedisLockAdapter(new Redis("YOUR_REDIS_CONNECTION")), }, // You can set an optional default adapter defaultAdapter: "memory", }); await lockFactoryResolver.init(); ``` ## Usage[​](#usage "Direct link to Usage") ### 1. Using the default adapter[​](#1-using-the-default-adapter "Direct link to 1. Using the default adapter") ./samples/lock-factory-resolver-default-adapter.ts ``` import { lockFactoryResolver } from "./lock-factory-resolver-initial-config.js"; await lockFactoryResolver .use() .create("shared-resource") .runOrFail(async () => { // code to run }); ``` danger Note that if you dont set a default adapter, an error will be thrown. ### 2. Specifying an adapter explicitly[​](#2-specifying-an-adapter-explicitly "Direct link to 2. Specifying an adapter explicitly") ./samples/lock-factory-resolver-specific-adapter.ts ``` import { lockFactoryResolver } from "./lock-factory-resolver-initial-config.js"; await lockFactoryResolver .use("redis") .create("shared-resource") .runOrFail(async () => { // code to run }); ``` danger Note that if you specify a non-existent adapter, an error will be thrown. ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/lock`](https://eridu-tech.github.io/eridu-tech-core/modules/Lock.html) API docs. --- # Lock middlewares ## Initial configuration[​](#initial-configuration "Direct link to Initial configuration") To begin using the lock middlewares, you'll need to create and configure a `LockFactoryResolver` instance: ./samples/lock.ts ``` import { LockFactoryResolver } from "eridu-tech/lock"; import { MemoryLockAdapter } from "eridu-tech/lock/memory-lock-adapter"; import { RedisLockAdapter } from "eridu-tech/lock/redis-lock-adapter"; import { Redis } from "ioredis"; export const lockFactoryResolver = new LockFactoryResolver({ adapters: { memory: new MemoryLockAdapter(), redis: new RedisLockAdapter(new Redis("YOUR_REDIS_CONNECTION_STRING")), }, defaultAdapter: "memory", }); await lockFactoryResolver.init(); ``` ## withLockFactory middleware[​](#with_lock_factory_middleware "Direct link to withLockFactory middleware") The Lock middleware wraps function calls with a distributed lock, ensuring mutual exclusion across processes. Before executing the wrapped function, a lock is acquired on a key derived from the function's arguments. If another process already holds the lock, the call waits (or fails immediately for non-blocking locks) until the lock is released. ### Usage[​](#usage "Direct link to Usage") ./samples/with-lock.ts ``` import { withLockFactory } from "eridu-tech/lock/middlewares"; import { use } from "eridu-tech/middleware"; import { lockFactoryResolver } from "./lock.js"; const withLock = withLockFactory(lockFactoryResolver); const processJob = async (jobId: string): Promise => { // Critical section — only one process should execute this at a time // ... process the job }; // Wrap with distributed lock using the default adapter (`memory`) const safeProcess = use( processJob, withLock({ key: ([jobId]) => `job:${jobId}`, }), ); // Wrap with distributed lock using a specific adapter (`redis`) const safeProcessOnRedis = use( processJob, withLock.use("redis")({ key: ([jobId]) => `job:${jobId}`, }), ); await safeProcess("job-123"); // Acquires lock, processes, releases lock await safeProcessOnRedis("job-456"); // Uses the Redis adapter ``` Calling `withLock()` directly uses the resolver's default adapter (`memory`). Use `withLock.use("redis")` to run the wrapped function through a specific registered adapter. info Here is a complete list of settings for the [`withLock`](https://eridu-tech.github.io/eridu-tech-core/types/Lock.WithLockSettings.html) function. ### Settings[​](#settings "Direct link to Settings") | Option | Type | Description | | -------- | -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `key` | `Invocable` | A function that produces the lock key from the wrapped function's arguments. The lock is acquired on this key, ensuring mutual exclusion across processes for the same key | | `lockId` | `Invocable` | Optional function that produces a unique identifier for the current lock acquisition attempt. Defaults to a UUID (`v4`) | | `ttl` | `ITimeSpan \| null` | Time-to-live for the lock. `null` means the lock never expires automatically; if omitted the factory's default TTL is used | ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/lock`](https://eridu-tech.github.io/eridu-tech-core/modules/Lock.html) API docs. --- # Lock Plugins ## withLockPrefix plugin[​](#withlockprefix-plugin "Direct link to withLockPrefix plugin") The Lock prefix plugin intercepts calls to a lock adapter and transparently prefixes all lock keys with a configurable string. This enables logical key namespacing without modifying the adapter implementation. ### Use cases[​](#use-cases "Direct link to Use cases") * **Multi-tenant locking** — Prefix lock keys with a tenant identifier to prevent cross-tenant lock contention * **Resource scoping** — Organize locks by resource type or module to avoid key collisions * **Environment isolation** — Separate development, staging, and production lock state * **Region isolation** — Prefix lock keys with a region identifier in multi-region deployments ### How it works[​](#how-it-works "Direct link to How it works") The `withLockPrefix` function returns a [`PluginFn`](/docs/foundation/middleware/.md) that calls `enhance` on each adapter method that accepts a lock key. When an enhanced method is invoked, the plugin intercepts the call, prepends the configured prefix to the key argument, and forwards the modified arguments to the original method. The plugin prefixes keys for the following methods: | Method | Key argument | Pattern | | -------------- | ---------------------- | -------------- | | `acquire` | First argument (`key`) | `prefix + key` | | `forceRelease` | First argument (`key`) | `prefix + key` | | `getState` | First argument (`key`) | `prefix + key` | | `refresh` | First argument (`key`) | `prefix + key` | | `release` | First argument (`key`) | `prefix + key` | ### Usage[​](#usage "Direct link to Usage") ./samples/with-lock-prefix.ts ``` import { withPlugin } from "eridu-tech/middleware"; import { MemoryLockAdapter } from "eridu-tech/lock/memory-lock-adapter"; import { withLockPrefix } from "eridu-tech/lock/plugins"; const adapter = new MemoryLockAdapter(); // Apply the prefix plugin to the adapter const prefixedAdapter = withPlugin(adapter, withLockPrefix("tenant-42:")); ``` ### Before/after behavior[​](#beforeafter-behavior "Direct link to Before/after behavior") **Before** — Lock keys are used as-is: ./samples/unprefixed-acquire.ts ``` import { MemoryLockAdapter } from "eridu-tech/lock/memory-lock-adapter"; const adapter = new MemoryLockAdapter(); const ttl = new Date(Date.now() + 60_000); await adapter.acquire("resource:42", "lock-id", ttl); // -> acquires lock on "resource:42" ``` **After** — Lock keys are automatically prefixed: ./samples/prefixed-acquire.ts ``` import { withPlugin } from "eridu-tech/middleware"; import { MemoryLockAdapter } from "eridu-tech/lock/memory-lock-adapter"; import { withLockPrefix } from "eridu-tech/lock/plugins"; const adapter = new MemoryLockAdapter(); // Apply the prefix plugin to the adapter const prefixedAdapter = withPlugin(adapter, withLockPrefix("tenant-42:")); const ttl = new Date(Date.now() + 60_000); await prefixedAdapter.acquire("resource:42", "lock-id", ttl); // -> acquires lock on "tenant-42:resource:42" ``` danger Because `withPlugin` uses `enhance` under the hood, the same edge case applies: if one enhanced method internally calls another enhanced method via `this`, the middleware will apply **twice**. Be mindful of inter-method calls when applying plugins that enhance multiple methods on the same instance. info For more information about the `withPlugin` function and applying plugins to adapters, see the [Middleware plugin](/docs/foundation/middleware/.md#plugin) documentation. --- # Lock usage The `eridu-tech/lock` component provides a way for managing locks independent of underlying platform or storage. ## Initial configuration[​](#initial-configuration "Direct link to Initial configuration") To begin using the `LockFactory` class, you'll need to create and configure an instance: ./samples/lock-factory-initial-config.ts ``` import { TimeSpan } from "eridu-tech/time-span"; import { MemoryLockAdapter } from "eridu-tech/lock/memory-lock-adapter"; import { LockFactory } from "eridu-tech/lock"; export const lockFactory = new LockFactory({ // You can provide default TTL value // If you set it to null it means locks will not expire and most be released manually by default. defaultTtl: TimeSpan.fromSeconds(2), // You can choose the adapter to use adapter: new MemoryLockAdapter(), }); await lockFactory.init(); ``` info Here is a complete list of settings for the [`LockFactory`](https://eridu-tech.github.io/eridu-tech-core/types/Lock.LockFactorySettingsBase.html) class. ## Lock basics[​](#lock-basics "Direct link to Lock basics") ### Creating a lock[​](#creating-a-lock "Direct link to Creating a lock") ./samples/lock-create.ts ``` import { lockFactory } from "./lock-factory-initial-config.js"; export const lock = lockFactory.create("shared-resource"); ``` ### Acquiring and releasing the lock[​](#acquiring-and-releasing-the-lock "Direct link to Acquiring and releasing the lock") ./samples/lock-acquire-release.ts ``` import { lock } from "./lock-create.js"; const hasAquired = await lock.acquire(); if (hasAquired) { try { // The critical section } finally { await lock.release(); } } ``` Alternatively you could write it as follows: ./samples/lock-acquire-or-fail.ts ``` import { lock } from "./lock-create.js"; try { // This method will throw if the lock is not acquired await lock.acquireOrFail(); // The critical section } finally { await lock.release(); } ``` danger You need always to wrap the critical section with `try-finally` so the lock get released when error occurs. ### Locks with custom TTL[​](#locks-with-custom-ttl "Direct link to Locks with custom TTL") You can provide a custom TTL for the lock. ./samples/lock-custom-ttl.ts ``` import { lockFactory } from "./lock-factory-initial-config.js"; import { TimeSpan } from "eridu-tech/time-span"; const lock = lockFactory.create("shared-resource", { // Default TTL is 5min if not overrided // If you set it to null it means locks will not expire and most be released manually. ttl: TimeSpan.fromSeconds(30), }); ``` ### Checking lock state[​](#checking-lock-state "Direct link to Checking lock state") You can get the lock state by using the `getState` method, it returns [`ILockState`](https://eridu-tech.github.io/eridu-tech-core/types/Lock.ILockState.html). ./samples/lock-get-state.ts ``` import { lockFactory } from "./lock-factory-initial-config.js"; import { LOCK_STATE } from "eridu-tech/lock/contracts"; const lock = lockFactory.create("shared-resource"); const state = await lock.getState(); if (state.type === LOCK_STATE.EXPIRED) { console.log("The lock doesnt exists"); } if (state.type === LOCK_STATE.UNAVAILABLE) { console.log("Lock is acquired by different owner"); } if (state.type === LOCK_STATE.ACQUIRED) { console.log("The lock is acquired"); } ``` ## Patterns[​](#patterns "Direct link to Patterns") ### Refreshing locks[​](#refreshing-locks "Direct link to Refreshing locks") The lock can be refreshed by the current owner before it expires. This is particularly useful for long-running tasks, instead of setting an excessively long TTL initially, you can start with a shorter one and use the `refresh` method to set the TTL of the lock: ./samples/lock-refresh.ts ``` import { lockFactory } from "./lock-factory-initial-config.js"; import { delay } from "eridu-tech/utilities"; import { TimeSpan } from "eridu-tech/time-span"; const lock = lockFactory.create("resource", { ttl: TimeSpan.fromMinutes(1), }); async function doWork(): Promise { // ... critical section return true; } const hasAcquired = await lock.acquire(); if (hasAcquired) { try { while (true) { await lock.refresh(TimeSpan.fromMinutes(1)); const hasFinished = await doWork(); if (hasFinished) { break; } await delay(TimeSpan.fromSeconds(1)); } } finally { await lock.release(); } } ``` warning Note: A lock must have an expiration (a `ttl` value) to be refreshed. You cannot refresh a lock that was created without an expiration (with `ttl: null`) ./samples/lock-non-refreshable.ts ``` import { lockFactory } from "./lock-factory-initial-config.js"; // Create a lock with no expiration (non-refreshable) const lock = lockFactory.create("resource", { ttl: null, }); // A refresh attempt on this lock will fail const hasRefreshed = await lock.refresh(); // This will log 'false' because the lock cannot be refreshed console.log(hasRefreshed); ``` ### Additional methods[​](#additional-methods "Direct link to Additional methods") The `releaseOrFail` method is the same `release` method but it throws an error when not enable to release the lock: ./samples/lock-release-or-fail.ts ``` import { lockFactory } from "./lock-factory-initial-config.js"; const lock = lockFactory.create("resource"); await lock.releaseOrFail(); ``` The `forceRelease` method releases the lock regardless of the owner: ./samples/lock-force-release.ts ``` import { lockFactory } from "./lock-factory-initial-config.js"; const lock = lockFactory.create("resource"); await lock.forceRelease(); ``` The `refreshOrFail` method is the same `refresh` method but it throws an error when not enable to refresh the lock: ./samples/lock-refresh-or-fail.ts ``` import { lockFactory } from "./lock-factory-initial-config.js"; const lock = lockFactory.create("resource"); await lock.refreshOrFail(); ``` The `runOrFail` method automatically manages lock acquisition and release around function execution. It calls `acquireOrFail` before invoking the function and calls `release` in a finally block, ensuring the lock is always freed, even if an error occurs during execution. ./samples/lock-run-or-fail.ts ``` import { lockFactory } from "./lock-factory-initial-config.js"; const lock = lockFactory.create("resource"); await lock.runOrFail(async () => { // ... critical section }); ``` info Note the method throws an error when the lock cannot be acquired. info You can provide synchronous or asynchronous [`Invocable<[], TValue | Promise>`](/docs/utilities/invocable/.md) as values for the `runOrFail` method. ### Lock instance variables[​](#lock-instance-variables "Direct link to Lock instance variables") The `Lock` class exposes instance variables such as: ./samples/lock-instance-variables.ts ``` import { lockFactory } from "./lock-factory-initial-config.js"; const lock = lockFactory.create("resource"); // Will return the key of the lock which is "resource" console.log(lock.key); // Will return the id of the lock console.log(lock.id); // Will return the ttl of the lock console.log(lock.ttl); ``` ### Lock id[​](#lock-id "Direct link to Lock id") By default the lock id is autogenerated but it can also manually defined. ./samples/lock-custom-id.ts ``` import { lockFactory } from "./lock-factory-initial-config.js"; const lock = lockFactory.create("lock", { lockId: "my-lock-id", }); const hasAcquire = await lock.acquire(); if (hasAcquire) { console.log("Shared resource"); await lock.release(); } ``` info Manually defining lock id is primarily useful for debugging or implementing manual resource locking by the end user. An example of manual resource locking by the end user can be found in a multi-user CMS, the end user manually locks a document during editing, this resource lock prevents simultaneous edits and data corruption. warning In most cases, setting a custom lock id is unnecessary. Misusing this feature could result in different locks sharing the same lock id while modifying the same resource simultaneously, which may lead to race conditions. ### Retrying acquiring lock by attempts[​](#retrying-acquiring-lock-by-attempts "Direct link to Retrying acquiring lock by attempts") To retry acquiring lock you can use the [`retry`](/docs/reliability/resilience/.md#retry) middleware. Retrying acquiring lock with `acquireOrFail` method: ./samples/lock-retry-acquire-or-fail.ts ``` import { lockFactory } from "./lock-factory-initial-config.js"; import { retry } from "eridu-tech/resilience"; import { FailedAcquireLockError } from "eridu-tech/lock/contracts"; import { use } from "eridu-tech/middleware"; const lock = lockFactory.create("lock"); try { await use(async () => { await lock.acquireOrFail(); }, [ retry({ maxAttempts: 4, errorPolicy: FailedAcquireLockError, }), ])(); // The critical section } finally { await lock.release(); } ``` Retrying acquiring lock with `acquire` method: ./samples/lock-retry-acquire.ts ``` import { lockFactory } from "./lock-factory-initial-config.js"; import { retry } from "eridu-tech/resilience"; import { use } from "eridu-tech/middleware"; const lock = lockFactory.create("lock"); const hasAquired = await use(async () => { return await lock.acquire(); }, [ retry({ maxAttempts: 4, errorPolicy: { treatFalseAsError: true, }, }), ])(); if (hasAquired) { try { // The critical section } finally { await lock.release(); } } ``` Retrying acquiring lock with `runOrFail` method: ./samples/lock-retry-run-or-fail.ts ``` import { lockFactory } from "./lock-factory-initial-config.js"; import { retry } from "eridu-tech/resilience"; import { FailedAcquireLockError } from "eridu-tech/lock/contracts"; import { use } from "eridu-tech/middleware"; const lock = lockFactory.create("lock"); await use(async () => { await lock.runOrFail(async () => { // The critical section }); }, [ retry({ maxAttempts: 4, errorPolicy: FailedAcquireLockError, }), ])(); ``` ### Retrying acquiring lock by interval[​](#retrying-acquiring-lock-by-interval "Direct link to Retrying acquiring lock by interval") To retry acquiring lock at regular intervals you can use the [`retryInterval`](/docs/reliability/resilience/.md#retry_by_interval) middleware: Retrying acquiring lock with `acquireOrFail` method: ./samples/lock-retry-interval-acquire-or-fail.ts ``` import { lockFactory } from "./lock-factory-initial-config.js"; import { retryInterval } from "eridu-tech/resilience"; import { FailedAcquireLockError } from "eridu-tech/lock/contracts"; import { use } from "eridu-tech/middleware"; import { TimeSpan } from "eridu-tech/time-span"; const lock = lockFactory.create("resource"); try { await use(async () => { await lock.acquireOrFail(); }, [ retryInterval({ // Time to wait 1 minute time: TimeSpan.fromMinutes(1), // Interval to try acquire the lock interval: TimeSpan.fromSeconds(1), errorPolicy: FailedAcquireLockError, }), ])(); // ... critical section } finally { await lock.release(); } ``` Retrying acquiring lock with `acquire` method: ./samples/lock-retry-interval-acquire.ts ``` import { lockFactory } from "./lock-factory-initial-config.js"; import { retryInterval } from "eridu-tech/resilience"; import { use } from "eridu-tech/middleware"; import { TimeSpan } from "eridu-tech/time-span"; const lock = lockFactory.create("resource"); const hasAcquired = await use(async () => { return await lock.acquire(); }, [ retryInterval({ time: TimeSpan.fromMinutes(1), interval: TimeSpan.fromSeconds(1), errorPolicy: { treatFalseAsError: true, }, }), ])(); if (hasAcquired) { try { // ... critical section } finally { await lock.release(); } } ``` Retrying acquiring lock with `runOrFail` method: ./samples/lock-retry-interval-run-or-fail.ts ``` import { lockFactory } from "./lock-factory-initial-config.js"; import { retryInterval } from "eridu-tech/resilience"; import { FailedAcquireLockError } from "eridu-tech/lock/contracts"; import { use } from "eridu-tech/middleware"; import { TimeSpan } from "eridu-tech/time-span"; const lock = lockFactory.create("resource"); await use(async () => { await lock.runOrFail(async () => { // ... critical section }); }, [ retryInterval({ time: TimeSpan.fromMinutes(1), interval: TimeSpan.fromSeconds(1), errorPolicy: FailedAcquireLockError, }), ])(); ``` warning Note using `retryInterval` middleware with lock acquiring in a HTTP request handler is discouraged because it blocks the HTTP request handler causing the handler wait until the lock becomes available or the timeout is reached. This will delay HTTP request handler to generate response and will make frontend app slow because of HTTP request handler. ### Serialization and deserialization of lock[​](#serialization-and-deserialization-of-lock "Direct link to Serialization and deserialization of lock") Locks can be serialized, allowing them to be transmitted over the network to another server and later deserialized for reuse. This means you can, for example, acquire the lock on the main server, transfer it to a queue worker server, and release it there. In order to serialize or deserialize a lock you need pass an object that implements [`ISerderRegister`](/docs/foundation/serde/.md) contract like the [`Serde`](/docs/foundation/serde/.md) class to `LockFactory`. Manually serializing and deserializing the lock: ./samples/lock-manual-serialization.ts ``` import { Redis } from "ioredis"; import { RedisLockAdapter } from "eridu-tech/lock/redis-lock-adapter"; import { LockFactory } from "eridu-tech/lock"; import { SuperJsonSerde } from "eridu-tech/serde/super-json-serde"; const serde = new SuperJsonSerde(); const redisClient = new Redis("YOUR_REDIS_CONNECTION"); const lockFactory = new LockFactory({ // You can laso pass in an array of Serde class instances serde, adapter: new RedisLockAdapter(redisClient), }); const lock = lockFactory.create("resource"); const serializedLock = await serde.serialize(lock); const deserializedLock = await serde.deserialize(serializedLock); ``` danger When serializing or deserializing a lock, you must use the same `Serde` instances that were provided to the `LockFactory`. This is required because the `LockFactory` injects custom serialization logic for `ILock` instance into `Serde` instances. info Note you only need manuall serialization and deserialization when integrating with external libraries. As long you pass the same `Serde` instances with all other components you dont need to serialize and deserialize the lock manually. ./samples/lock-event-bus-serialization.ts ``` import { Redis } from "ioredis"; import { RedisLockAdapter } from "eridu-tech/lock/redis-lock-adapter"; import type { ILock } from "eridu-tech/lock/contracts"; import { LockFactory } from "eridu-tech/lock"; import { RedisPubSubEventBusAdapter } from "eridu-tech/event-bus/redis-pub-sub-event-bus-adapter"; import { EventBus } from "eridu-tech/event-bus"; import { SuperJsonSerde } from "eridu-tech/serde/super-json-serde"; const serde = new SuperJsonSerde(); const redis = new Redis("YOUR_REDIS_CONNECTION"); type EventMap = { "sending-lock-over-network": { lock: ILock; }; }; const eventBus = new EventBus({ adapter: new RedisPubSubEventBusAdapter({ client: redis, serde, }), }); const lockFactory = new LockFactory({ serde, adapter: new RedisLockAdapter(redis), }); const lock = lockFactory.create("resource"); // We are sending the lock over the network to other servers. await eventBus.dispatch("sending-lock-over-network", { lock, }); // The other servers will recieve the serialized lock and automattically deserialize it. await eventBus.addListener("sending-lock-over-network", ({ lock }) => { // The lock is deserialized and can be used console.log("LOCK:", lock); }); ``` ### Separating lock creation from manipulation[​](#separating-lock-creation-from-manipulation "Direct link to Separating lock creation from manipulation") The library includes 2 additional contracts: * [`ILock`](https://eridu-tech.github.io/eridu-tech-core/types/Lock.ILock.html) - Allows only for manipulating of the lock. * [`ILockFactory`](https://eridu-tech.github.io/eridu-tech-core/types/Lock.ILockFactory.html) - Allows only for creation of locks. ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/lock`](https://eridu-tech.github.io/eridu-tech-core/modules/Lock.html) API docs. --- # DI Integration The `lockFactoryResolverDiFactory` function creates an [`ILockFactoryResolver`](/docs/concurrency/lock/lock_factory_resolver/.md) whose underlying resolver is resolved from a [DI container](/docs/foundation/di/.md). The returned proxy implements both `ILockFactoryResolver` and `ILockFactory`: * `use(adapterName?)` selects an adapter on the resolved `LockFactoryResolver`. * `create(key, settings?)` creates a lock through the resolver's default adapter. The proxy is wired to the container during `container.init()`. Create it **before** calling `init()`; calling `use()` or `create()` before the container is initialized throws an error. ## Initial configuration[​](#initial-configuration "Direct link to Initial configuration") Set up the memory and Redis lock adapters, create the container, then register the `LockFactoryResolver` and wrap it with `lockFactoryResolverDiFactory`: #### 1. Set up the memory lock adapter[​](#1-set-up-the-memory-lock-adapter "Direct link to 1. Set up the memory lock adapter") ./samples/memory-lock-adapter.ts ``` import { MemoryLockAdapter } from "eridu-tech/lock/memory-lock-adapter"; export const memoryLockAdapter = new MemoryLockAdapter(); ``` #### 2. Set up the Redis lock adapter[​](#2-set-up-the-redis-lock-adapter "Direct link to 2. Set up the Redis lock adapter") ./samples/redis-lock-adapter.ts ``` import { RedisLockAdapter } from "eridu-tech/lock/redis-lock-adapter"; import { Redis } from "ioredis"; export const redisLockAdapter = new RedisLockAdapter( new Redis("YOUR_REDIS_CONNECTION_STRING"), ); ``` #### 3. Create the dependency injection container[​](#3-create-the-dependency-injection-container "Direct link to 3. Create the dependency injection container") ./samples/container.ts ``` import { Container } from "eridu-tech/di"; import { ExecutionContext } from "eridu-tech/execution-context"; import { AlsExecutionContextAdapter } from "eridu-tech/execution-context/als-execution-context-adapter"; export const executionContext = new ExecutionContext( new AlsExecutionContextAdapter(), ); export const container = new Container({ executionContext }); ``` #### 4. Register and initialize the lock factory resolver[​](#4-register-and-initialize-the-lock-factory-resolver "Direct link to 4. Register and initialize the lock factory resolver") ./samples/lock-factory-resolver-di-factory.ts ``` import { LockFactoryResolver } from "eridu-tech/lock"; import { ProxyLockFactoryResolver } from "eridu-tech/lock/di"; import { container } from "./container.js"; import { memoryLockAdapter } from "./memory-lock-adapter.js"; import { redisLockAdapter } from "./redis-lock-adapter.js"; type Adapters = "memory" | "redis"; const lockFactoryResolver = new LockFactoryResolver({ adapters: { memory: memoryLockAdapter, redis: redisLockAdapter, }, defaultAdapter: "memory", }); await lockFactoryResolver.init(); container.registerValue({ token: LockFactoryResolver, value: lockFactoryResolver, }); // Create the proxy before container.init() export const lockFactory = new ProxyLockFactoryResolver({ container, resolverToken: LockFactoryResolver, }); await container.init(); ``` ## Usage[​](#usage "Direct link to Usage") ### 1. Using the default adapter[​](#1-using-the-default-adapter "Direct link to 1. Using the default adapter") ./samples/lock-factory-resolver-di-factory-default-adapter.ts ``` import { lockFactory } from "./lock-factory-resolver-di-factory.js"; // Uses the adapter configured as the default (memory) await lockFactory.create("shared-resource").runOrFail(async () => { // code to run }); ``` ### 2. Specifying an adapter explicitly[​](#2-specifying-an-adapter-explicitly "Direct link to 2. Specifying an adapter explicitly") ./samples/lock-factory-resolver-di-factory-specific-adapter.ts ``` import { lockFactory } from "./lock-factory-resolver-di-factory.js"; // Uses the Redis adapter await lockFactory .use("redis") .create("shared-resource") .runOrFail(async () => { // code to run }); ``` ## Using it with the middlewares[​](#using-it-with-the-middlewares "Direct link to Using it with the middlewares") Because the proxy implements `ILockFactoryResolver`, it can be passed straight to the [lock middleware factories](/docs/concurrency/lock/lock_middlewares/.md): ./samples/lock-factory-resolver-di-factory-with-middleware.ts ``` import { withLockFactory } from "eridu-tech/lock/middlewares"; import { use } from "eridu-tech/middleware"; import { lockFactory } from "./lock-factory-resolver-di-factory.js"; const withLock = withLockFactory(lockFactory); const processJob = async (jobId: string): Promise => { // ... process the job }; const safeProcess = use( processJob, withLock.use("redis")({ key: ([jobId]) => `job:${jobId}`, }), ); await safeProcess("job-123"); ``` ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/lock`](https://eridu-tech.github.io/eridu-tech-core/modules/Lock.html) API docs. --- # Concurrency Coordinate access to shared resources across processes and machines. * [Lock](/docs/concurrency/lock/lock_usage/.md) — Distributed lock primitives with lease management, blocking and non-blocking acquisition, and automatic release. * [Shared Lock](/docs/concurrency/shared_lock/shared_lock_usage/.md) — Read-write distributed locks for coordinating concurrent access with shared and exclusive modes. * [Semaphore](/docs/concurrency/semaphore/semaphore_usage/.md) — Rate-limit concurrent access to shared resources with dynamic permit allocation. ## Existing middlewares[​](#existing-middlewares "Direct link to Existing middlewares") Beyond the components themselves, every component provides middleware factories for composing component behavior around your own functions. Wrap a function with use() or method with enhance() and apply any of the following middlewares. * [withLockFactory](/docs/concurrency/lock/lock_middlewares/.md#with_lock_factory_middleware) — Wraps function calls with a distributed lock. * [withSemaphoreFactory](/docs/concurrency/semaphore/semaphore_middlewares/.md#with_semaphore_factory_middleware) — Wraps function calls with a distributed semaphore. * [withSharedLockFactory](/docs/concurrency/shared_lock/shared_lock_middlewares/.md#with_shared_lock_factory_middleware) — Wraps function calls with a shared (reader-writer) lock. --- # Configuring Semaphore adapters ## MemorySemaphoreAdapter[​](#memorysemaphoreadapter "Direct link to MemorySemaphoreAdapter") To use the `MemorySemaphoreAdapter` you only need to create instance of it: ./samples/memory-semaphore-adapter.ts ``` import { MemorySemaphoreAdapter } from "eridu-tech/semaphore/memory-semaphore-adapter"; export const memorySemaphoreAdapter = new MemorySemaphoreAdapter(); ``` You can also provide an `Map` that will be used for storing the data in memory: ./samples/memory-semaphore-adapter-with-map.ts ``` import { MemorySemaphoreAdapter } from "eridu-tech/semaphore/memory-semaphore-adapter"; const map = new Map(); const memorySemaphoreAdapter = new MemorySemaphoreAdapter(map); ``` info `MemorySemaphoreAdapter` lets you test your app without external dependencies like `Redis`, ideal for local development, unit tests, integration tests and fast E2E test for the backend application. danger Note the `MemorySemaphoreAdapter` is limited to single process usage and cannot be shared across multiple servers or processes. ### Settings[​](#settings "Direct link to Settings") To clean up expired semaphore keys, call `removeAllExpired` at a regular interval (for example, using a cron job): ./samples/memory-semaphore-remove-all-expired.ts ``` import { MemorySemaphoreAdapter } from "eridu-tech/semaphore/memory-semaphore-adapter"; const memorySemaphoreAdapter = new MemorySemaphoreAdapter(); // Remove all expired semaphore keys manually. await memorySemaphoreAdapter.removeAllExpired(); ``` info To remove the semaphore map and all stored semaphore data, use `deInit` method: ./samples/memory-semaphore-adapter-deinit.ts ``` import { memorySemaphoreAdapter } from "./memory-semaphore-adapter.js"; await memorySemaphoreAdapter.deInit(); ``` ## MongodbSemaphoreAdapter[​](#mongodbsemaphoreadapter "Direct link to MongodbSemaphoreAdapter") To use the `MongodbSemaphoreAdapter`, you'll need to: 1. Install the required dependency: [`mongodb`](https://www.npmjs.com/package/mongodb) package. ### Setup[​](#setup "Direct link to Setup") Connect to MongoDB: ./samples/mongodb-semaphore-adapter-setup.ts ``` import { MongoClient } from "mongodb"; const client = await MongoClient.connect("YOUR_MONGODB_CONNECTION_STRING"); export const database = client.db("database"); ``` info The `database` setting also accepts a [`TransactionContext`](/docs/foundation/transaction_context/transaction_context_usage/.md), which makes the adapter transaction aware. Adapters given the same instance share the same transaction when available. ### Usage[​](#usage "Direct link to Usage") Create the adapter: ./samples/mongodb-semaphore-adapter.ts ``` import { MongodbSemaphoreAdapter } from "eridu-tech/semaphore/mongodb-semaphore-adapter"; import { database } from "./mongodb-semaphore-adapter-setup.js"; export const mongodbSemaphoreAdapter = new MongodbSemaphoreAdapter({ database, }); // You need initialize the adapter once before using it. // During the initialization the indexes will be created await mongodbSemaphoreAdapter.init(); ``` You can change the collection name: ./samples/mongodb-semaphore-collection-name.ts ``` import { MongodbSemaphoreAdapter } from "eridu-tech/semaphore/mongodb-semaphore-adapter"; import { database } from "./mongodb-semaphore-adapter-setup.js"; const mongodbSemaphoreAdapter = new MongodbSemaphoreAdapter({ database, // By default "semaphore" is used as collection name collectionName: "my-semaphore", }); await mongodbSemaphoreAdapter.init(); ``` You can change the collection settings: ./samples/mongodb-semaphore-collection-settings.ts ``` import { MongodbSemaphoreAdapter } from "eridu-tech/semaphore/mongodb-semaphore-adapter"; import { database } from "./mongodb-semaphore-adapter-setup.js"; const mongodbSemaphoreAdapter = new MongodbSemaphoreAdapter({ database, // You configure additional collection settings collectionSettings: {}, }); await mongodbSemaphoreAdapter.init(); ``` info To remove the semaphore collection and all stored semaphore data, use `deInit` method: ./samples/mongodb-semaphore-adapter-deinit.ts ``` import { mongodbSemaphoreAdapter } from "./mongodb-semaphore-adapter.js"; await mongodbSemaphoreAdapter.deInit(); ``` danger Note in order to use `MongodbSemaphoreAdapter` correctly, ensure you use a single, consistent database across all server instances or processes. ## RedisSemaphoreAdapter[​](#redissemaphoreadapter "Direct link to RedisSemaphoreAdapter") To use the `RedisSemaphoreAdapter`, you'll need to: 1. Install the required dependency: [`ioredis`](https://www.npmjs.com/package/ioredis) package. ### Setup[​](#setup-1 "Direct link to Setup") Connect to Redis: ./samples/redis-semaphore-adapter-setup.ts ``` import { Redis } from "ioredis"; export const database = new Redis("YOUR_REDIS_CONNECTION_STRING"); ``` ### Usage[​](#usage-1 "Direct link to Usage") Create the adapter: ./samples/redis-semaphore-adapter.ts ``` import { RedisSemaphoreAdapter } from "eridu-tech/semaphore/redis-semaphore-adapter"; import { database } from "./redis-semaphore-adapter-setup.js"; const redisSemaphoreAdapter = new RedisSemaphoreAdapter(database); ``` danger Note in order to use `RedisSemaphoreAdapter` correctly, ensure you use a single, consistent database across all server instances or processes. ## KyselySemaphoreAdapter[​](#kyselysemaphoreadapter "Direct link to KyselySemaphoreAdapter") To use the `KyselySemaphoreAdapter`, you'll need to: 1. Use database provider that has support for transactions. 2. Install the required dependency: [`kysely`](https://www.npmjs.com/package/kysely) package. ### Setup[​](#setup-2 "Direct link to Setup") Create a function that creates the [`TransactionContext`](/docs/foundation/transaction_context/transaction_context_usage/.md) by wrapping a `Kysely` instance in a [`KyselyTransactionAdapter`](/docs/foundation/transaction_context/configuring_transaction_context_adapters/.md#kysely_transaction_adapter): ./samples/kysely-semaphore-adapter-setup.ts ``` import { ExecutionContext } from "eridu-tech/execution-context"; import { AlsExecutionContextAdapter } from "eridu-tech/execution-context/als-execution-context-adapter"; import { contextToken } from "eridu-tech/execution-context/contracts"; import { TransactionContext } from "eridu-tech/transaction-context"; import { KyselyTransactionAdapter } from "eridu-tech/transaction-context/kysely-transaction-adapter"; import type { ITransactionContext } from "eridu-tech/transaction-context/contracts"; import type { Kysely } from "kysely"; const executionContext = new ExecutionContext(new AlsExecutionContextAdapter()); // `KyselySemaphoreAdapter` is transaction aware: it runs every semaphore // operation through the `current` client of this context, which is the // transaction-scoped client while a transaction is active and the base client // otherwise. export function createTransactionContext( kysely: Kysely, ): ITransactionContext> { return new TransactionContext>({ token: contextToken("kysely"), adapter: new KyselyTransactionAdapter({ database: kysely, }), executionContext, }); } ``` info The `transactionContext` setting makes the adapter transaction aware. Adapters given the same instance share the same transaction when available. ### With Sqlite[​](#with-sqlite "Direct link to With Sqlite") You will need to install [`better-sqlite3`](https://www.npmjs.com/package/better-sqlite3) package: ./samples/kysely-semaphore-sqlite.ts ``` import { KyselySemaphoreAdapter } from "eridu-tech/semaphore/kysely-semaphore-adapter"; import Sqlite from "better-sqlite3"; import { Kysely, SqliteDialect } from "kysely"; import { createTransactionContext } from "./kysely-semaphore-adapter-setup.js"; const database = new Sqlite("DATABASE_NAME.db"); const kysely = new Kysely({ dialect: new SqliteDialect({ database, }), }); const transactionContext = createTransactionContext(kysely); export const kyselySemaphoreAdapter = new KyselySemaphoreAdapter({ transactionContext, }); // You need initialize the adapter once before using it. // During the initialization the schema will be created await kyselySemaphoreAdapter.init(); ``` danger Note using `KyselySemaphoreAdapter` with `sqlite` is limited to single server usage and cannot be shared across multiple servers but it can be shared between different processes. To use it correctly, ensure all process instances access the same persisted database. ### With Postgres[​](#with-postgres "Direct link to With Postgres") You will need to install [`pg`](https://www.npmjs.com/package/pg) package: ./samples/kysely-semaphore-postgres.ts ``` import { KyselySemaphoreAdapter } from "eridu-tech/semaphore/kysely-semaphore-adapter"; import { Pool } from "pg"; import { Kysely, PostgresDialect } from "kysely"; import { createTransactionContext } from "./kysely-semaphore-adapter-setup.js"; const database = new Pool({ database: "DATABASE_NAME", host: "DATABASE_HOST", user: "DATABASE_USER", // DATABASE port port: 5432, password: "DATABASE_PASSWORD", max: 10, }); const kysely = new Kysely({ dialect: new PostgresDialect({ pool: database, }), }); const transactionContext = createTransactionContext(kysely); const kyselySemaphoreAdapter = new KyselySemaphoreAdapter({ transactionContext, }); // You need initialize the adapter once before using it. // During the initialization the schema will be created await kyselySemaphoreAdapter.init(); ``` danger Note in order to use `KyselySemaphoreAdapter` with `postgres` correctly, ensure you use a single, consistent database across all server instances. This means you can't use replication. ### Settings[​](#settings-1 "Direct link to Settings") To clean up expired semaphore keys, call `removeAllExpired` at a regular interval (for example, using a cron job): ./samples/kysely-semaphore-remove-all-expired.ts ``` import { kyselySemaphoreAdapter } from "./kysely-semaphore-sqlite.js"; // Remove all expired semaphore keys manually. await kyselySemaphoreAdapter.removeAllExpired(); ``` info To remove the semaphore table and all stored semaphore data, use `deInit` method: ./samples/kysely-semaphore-adapter-deinit.ts ``` import { kyselySemaphoreAdapter } from "./kysely-semaphore-sqlite.js"; await kyselySemaphoreAdapter.deInit(); ``` ## NoOpSemaphoreAdapter[​](#noopsemaphoreadapter "Direct link to NoOpSemaphoreAdapter") The `NoOpSemaphoreAdapter` is a no-operation implementation, it performs no actions when called: ./samples/no-op-semaphore-adapter.ts ``` import { NoOpSemaphoreAdapter } from "eridu-tech/semaphore/no-op-semaphore-adapter"; const noOpSemaphoreAdapter = new NoOpSemaphoreAdapter(); ``` info The `NoOpSemaphoreAdapter` is useful when you want to mock out or disable your [`SemaphoreFactory`](https://eridu-tech.github.io/eridu-tech-core/classes/Semaphore.SemaphoreFactory.html) instance. ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/semaphore`](https://eridu-tech.github.io/eridu-tech-core/modules/Semaphore.html) API docs. --- # Creating Semaphore adapters ## Implementing your custom ISemaphoreAdapter[​](#implementing-your-custom-isemaphoreadapter "Direct link to Implementing your custom ISemaphoreAdapter") In order to create an adapter you need to implement the [`ISemaphoreAdapter`](https://eridu-tech.github.io/eridu-tech-core/types/Semaphore.ISemaphoreAdapter.html) contract. ## Testing your custom ISemaphoreAdapter[​](#testing-your-custom-isemaphoreadapter "Direct link to Testing your custom ISemaphoreAdapter") We provide a complete test suite to test your semaphore adapter implementation. Simply use the [`semaphoreAdapterTestSuite`](https://eridu-tech.github.io/eridu-tech-core/functions/Semaphore.semaphoreAdapterTestSuite.htmll) function: The suite provides preconfigured Vitest test cases with common edge case coverage and standardized semaphore adapter contract conformance testing. Usage example: ./samples/semaphore-adapter-test-suite.ts ``` import { beforeEach, describe, expect, test } from "vitest"; import { semaphoreAdapterTestSuite } from "eridu-tech/semaphore/test-utilities"; import { MemorySemaphoreAdapter } from "eridu-tech/semaphore/memory-semaphore-adapter"; describe("class: MySemaphoreAdapter", () => { semaphoreAdapterTestSuite({ createAdapter: () => new MemorySemaphoreAdapter(), test, beforeEach, expect, describe, }); }); ``` ## Implementing your custom ISemaphoreFactory class[​](#implementing-your-custom-isemaphorefactory-class "Direct link to Implementing your custom ISemaphoreFactory class") In some cases, you may need to implement a custom [`SemaphoreFactory`](https://eridu-tech.github.io/eridu-tech-core/classes/Semaphore.SemaphoreFactory.html) class to optimize performance for your specific technology stack. You can then directly implement the [`ISemaphoreFactory`](https://eridu-tech.github.io/eridu-tech-core/types/Semaphore.ISemaphoreFactory.html) contract. ## Testing your custom ISemaphoreFactory class[​](#testing-your-custom-isemaphorefactory-class "Direct link to Testing your custom ISemaphoreFactory class") We provide a complete test suite to verify your custom event-bus class implementation. Simply use the [`semaphoreFactoryTestSuite`](https://eridu-tech.github.io/eridu-tech-core/functions/Semaphore.semaphoreFactoryTestSuite.html) function: The suite provides preconfigured Vitest test cases with common edge case coverage and standardized semaphore factory contract conformance testing. Usage example: ./samples/semaphore-factory-test-suite.ts ``` import { beforeEach, describe, expect, test } from "vitest"; import { semaphoreFactoryTestSuite } from "eridu-tech/semaphore/test-utilities"; import { SemaphoreFactory } from "eridu-tech/semaphore"; import { MemorySemaphoreAdapter } from "eridu-tech/semaphore/memory-semaphore-adapter"; import { SuperJsonSerde } from "eridu-tech/serde/super-json-serde"; describe("class: MySemaphoreFactory", () => { semaphoreFactoryTestSuite({ createSemaphoreFactory: () => ({ semaphoreFactory: new SemaphoreFactory({ adapter: new MemorySemaphoreAdapter(), }), serde: new SuperJsonSerde(), }), test, beforeEach, expect, describe, }); }); ``` ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/semaphore`](https://eridu-tech.github.io/eridu-tech-core/modules/Semaphore.html) API docs. --- # SemaphoreFactoryResolver The `SemaphoreFactoryResolver` class provides a flexible way to configure and switch between different semaphore adapters at runtime. ## Initial configuration[​](#initial-configuration "Direct link to Initial configuration") To begin using the `ISemaphoreFactoryResolver`, you will need to register all required adapters during initialization. ./samples/semaphore-factory-resolver-initial-config.ts ``` import { SemaphoreFactoryResolver } from "eridu-tech/semaphore"; import { MemorySemaphoreAdapter } from "eridu-tech/semaphore/memory-semaphore-adapter"; import { RedisSemaphoreAdapter } from "eridu-tech/semaphore/redis-semaphore-adapter"; import { SuperJsonSerde } from "eridu-tech/serde/super-json-serde"; import { Redis } from "ioredis"; const serde = new SuperJsonSerde(); export const semaphoreFactoryResolver = new SemaphoreFactoryResolver({ serde, adapters: { memory: new MemorySemaphoreAdapter(), redis: new RedisSemaphoreAdapter(new Redis("YOUR_REDIS_CONNECTION")), }, // You can set an optional default adapter defaultAdapter: "memory", }); await semaphoreFactoryResolver.init(); ``` ## Usage[​](#usage "Direct link to Usage") ### 1. Using the default adapter[​](#1-using-the-default-adapter "Direct link to 1. Using the default adapter") ./samples/semaphore-factory-resolver-default-adapter.ts ``` import { semaphoreFactoryResolver } from "./semaphore-factory-resolver-initial-config.js"; await semaphoreFactoryResolver .use() .create("shared-resource", { limit: 2, }) .runOrFail(async () => { // code to run }); ``` danger Note that if you dont set a default adapter, an error will be thrown. ### 2. Specifying an adapter explicitly[​](#2-specifying-an-adapter-explicitly "Direct link to 2. Specifying an adapter explicitly") ./samples/semaphore-factory-resolver-specific-adapter.ts ``` import { semaphoreFactoryResolver } from "./semaphore-factory-resolver-initial-config.js"; await semaphoreFactoryResolver .use("redis") .create("shared-resource", { limit: 2, }) .runOrFail(async () => { // code to run }); ``` danger Note that if you specify a non-existent adapter, an error will be thrown. ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/semaphore`](https://eridu-tech.github.io/eridu-tech-core/modules/Semaphore.html) API docs. --- # Semaphore middlewares ## Initial configuration[​](#initial-configuration "Direct link to Initial configuration") To begin using the semaphore middlewares, you'll need to create and configure a `SemaphoreFactoryResolver` instance: ./samples/semaphore.ts ``` import { SemaphoreFactoryResolver } from "eridu-tech/semaphore"; import { MemorySemaphoreAdapter } from "eridu-tech/semaphore/memory-semaphore-adapter"; import { RedisSemaphoreAdapter } from "eridu-tech/semaphore/redis-semaphore-adapter"; import { Redis } from "ioredis"; export const semaphoreFactoryResolver = new SemaphoreFactoryResolver({ adapters: { memory: new MemorySemaphoreAdapter(), redis: new RedisSemaphoreAdapter( new Redis("YOUR_REDIS_CONNECTION_STRING"), ), }, defaultAdapter: "memory", }); await semaphoreFactoryResolver.init(); ``` ## withSemaphoreFactory middleware[​](#with_semaphore_factory_middleware "Direct link to withSemaphoreFactory middleware") The Semaphore middleware wraps function calls with a distributed semaphore, limiting the number of concurrent executions across processes. Before executing the wrapped function, a slot is acquired on a key derived from the function's arguments. If the maximum number of concurrent slots (`limit`) has already been reached, the call waits (or fails immediately for non-blocking semaphores) until a slot becomes available. ### Usage[​](#usage "Direct link to Usage") ./samples/with-semaphore.ts ``` import { withSemaphoreFactory } from "eridu-tech/semaphore/middlewares"; import { use } from "eridu-tech/middleware"; import { semaphoreFactoryResolver } from "./semaphore.js"; const withSemaphore = withSemaphoreFactory(semaphoreFactoryResolver); const processFile = async (filePath: string): Promise => { // Process file — limited concurrency // ... process the file }; // Wrap with semaphore using the default adapter (`memory`) — max 3 concurrent file processes const throttledProcess = use( processFile, withSemaphore({ key: ([filePath]) => `file-path:${filePath}`, limit: 3, }), ); // Wrap with semaphore using a specific adapter (`redis`) const throttledProcessOnRedis = use( processFile, withSemaphore.use("redis")({ key: ([filePath]) => `file-path:${filePath}`, limit: 3, }), ); // These will run up to 3 at a time await Promise.all([ throttledProcess("/data/file1.json"), throttledProcess("/data/file2.json"), throttledProcess("/data/file3.json"), throttledProcess("/data/file4.json"), // Waits for a slot ]); await throttledProcessOnRedis("/data/redis-file.json"); ``` Calling `withSemaphore()` directly uses the resolver's default adapter (`memory`). Use `withSemaphore.use("redis")` to run the wrapped function through a specific registered adapter. info Here is a complete list of settings for the [`withSemaphore`](https://eridu-tech.github.io/eridu-tech-core/types/Semaphore.WithSemaphoreSettings.html) function. ### Settings[​](#settings "Direct link to Settings") | Option | Type | Description | | -------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | `key` | `Invocable` | A function that produces the semaphore key from the wrapped function's arguments. All consumers using the same key share the same semaphore limit | | `limit` | `number` | Maximum number of concurrent slots (consumers) allowed for the semaphore key | | `slotId` | `Invocable` | Optional function that produces a unique slot identifier for the current acquisition attempt. Defaults to a UUID (`v4`) | | `ttl` | `ITimeSpan \| null` | Time-to-live for each acquired slot. `null` means slots never expire automatically; if omitted the factory's default TTL is used | ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/semaphore`](https://eridu-tech.github.io/eridu-tech-core/modules/Semaphore.html) API docs. --- # Semaphore Plugins ## withSemaphorePrefix plugin[​](#withsemaphoreprefix-plugin "Direct link to withSemaphorePrefix plugin") The Semaphore prefix plugin intercepts calls to a semaphore adapter and transparently prefixes all semaphore keys with a configurable string. This enables logical key namespacing without modifying the adapter implementation. ### Use cases[​](#use-cases "Direct link to Use cases") * **Multi-tenant semaphores** — Prefix semaphore keys with a tenant identifier to isolate concurrency limits between tenants * **Resource scoping** — Organize semaphores by resource type to avoid key collisions * **Environment isolation** — Separate development, staging, and production semaphore state * **Pool differentiation** — Prefix keys with a pool name to manage independent semaphore pools ### How it works[​](#how-it-works "Direct link to How it works") The `withSemaphorePrefix` function returns a [`PluginFn`](/docs/foundation/middleware/.md) that calls `enhance` on each adapter method that accepts a semaphore key. When an enhanced method is invoked, the plugin intercepts the call, prepends the configured prefix to the key argument, and forwards the modified arguments to the original method. The plugin prefixes keys for the following methods: | Method | Key argument | Pattern | | ----------------- | ---------------------------- | -------------- | | `acquire` | `key` within settings object | `prefix + key` | | `forceReleaseAll` | First argument (`key`) | `prefix + key` | | `getState` | First argument (`key`) | `prefix + key` | | `refresh` | First argument (`key`) | `prefix + key` | | `release` | First argument (`key`) | `prefix + key` | ### Usage[​](#usage "Direct link to Usage") ./samples/with-semaphore-prefix.ts ``` import { withPlugin } from "eridu-tech/middleware"; import { MemorySemaphoreAdapter } from "eridu-tech/semaphore/memory-semaphore-adapter"; import { withSemaphorePrefix } from "eridu-tech/semaphore/plugins"; const adapter = new MemorySemaphoreAdapter(); // Apply the prefix plugin to the adapter const prefixedAdapter = withPlugin(adapter, withSemaphorePrefix("pool-1:")); ``` ### Before/after behavior[​](#beforeafter-behavior "Direct link to Before/after behavior") **Before** — Semaphore keys are used as-is: ./samples/unprefixed-acquire.ts ``` import { MemorySemaphoreAdapter } from "eridu-tech/semaphore/memory-semaphore-adapter"; const adapter = new MemorySemaphoreAdapter(); await adapter.acquire({ key: "connections", slotId: "slot-1", limit: 1, ttl: null, }); // -> acquires slot on "connections" ``` **After** — Semaphore keys are automatically prefixed: ./samples/prefixed-acquire.ts ``` import { withPlugin } from "eridu-tech/middleware"; import { MemorySemaphoreAdapter } from "eridu-tech/semaphore/memory-semaphore-adapter"; import { withSemaphorePrefix } from "eridu-tech/semaphore/plugins"; const adapter = new MemorySemaphoreAdapter(); // Apply the prefix plugin to the adapter const prefixedAdapter = withPlugin(adapter, withSemaphorePrefix("pool-1:")); await prefixedAdapter.acquire({ key: "connections", slotId: "slot-1", limit: 1, ttl: null, }); // -> acquires slot on "pool-1:connections" ``` danger Because `withPlugin` uses `enhance` under the hood, the same edge case applies: if one enhanced method internally calls another enhanced method via `this`, the middleware will apply **twice**. Be mindful of inter-method calls when applying plugins that enhance multiple methods on the same instance. info For more information about the `withPlugin` function and applying plugins to adapters, see the [Middleware plugin](/docs/foundation/middleware/.md#plugin) documentation. --- # Semaphore usage The `eridu-tech/semaphore` component provides a way for managing semaphores independent of underlying platform or storage. ## Initial configuration[​](#initial-configuration "Direct link to Initial configuration") To begin using the `SemaphoreFactory` class, you'll need to create and configure an instance: ./samples/semaphore-factory-initial-config.ts ``` import { TimeSpan } from "eridu-tech/time-span"; import { MemorySemaphoreAdapter } from "eridu-tech/semaphore/memory-semaphore-adapter"; import { SemaphoreFactory } from "eridu-tech/semaphore"; export const semaphoreFactory = new SemaphoreFactory({ // You can provide default TTL value // If you set it to null it means semaphores will not expire and most be released manually by default. defaultTtl: TimeSpan.fromSeconds(2), // You can choose the adapter to use adapter: new MemorySemaphoreAdapter(), }); await semaphoreFactory.init(); ``` info Here is a complete list of settings for the [`SemaphoreFactory`](https://eridu-tech.github.io/eridu-tech-core/types/Semaphore.SemaphoreFactorySettingsBase.html) class. ## Semaphore basics[​](#semaphore-basics "Direct link to Semaphore basics") ### Creating a semaphore[​](#creating-a-semaphore "Direct link to Creating a semaphore") ./samples/semaphore-create.ts ``` import { semaphoreFactory } from "./semaphore-factory-initial-config.js"; export const semaphore = semaphoreFactory.create("shared-resource", { // You need to define a limit limit: 2, }); ``` ### Acquiring and releasing the semaphore[​](#acquiring-and-releasing-the-semaphore "Direct link to Acquiring and releasing the semaphore") ./samples/semaphore-acquire.ts ``` import { semaphore } from "./semaphore-create.js"; // 1 slot will be acquired if (await semaphore.acquire()) { console.log("Acquired"); try { // The concurrent section } finally { await semaphore.release(); } } else { console.log("Unable to acquire"); } // 2 slots will be acquired if (await semaphore.acquire()) { console.log("Acquired"); try { // The concurrent section } finally { await semaphore.release(); } } else { console.log("Unable to acquire"); } // Will log false because the limit is reached console.log(await semaphore.acquire()); ``` Alternatively you could write it as follows: ./samples/semaphore-acquire-or-fail.ts ``` import { semaphore } from "./semaphore-create.js"; // 1 slot will be acquired try { console.log("Acquired"); // This method will throw if the semaphore limit is reached. await semaphore.acquireOrFail(); // The critical section } catch { console.log("Unable to acquire"); } finally { await semaphore.release(); } // 2 slots will be acquired try { console.log("Acquired"); // This method will throw if the semaphore limit is reached. await semaphore.acquireOrFail(); // The critical section } catch { console.log("Unable to acquire"); } finally { await semaphore.release(); } // Will throw because the limit is reached await semaphore.acquireOrFail(); ``` danger You need always to wrap the concurrent section with `try-finally` so the semaphore get released when error occurs. ### Semaphore with custom TTL[​](#semaphore-with-custom-ttl "Direct link to Semaphore with custom TTL") You can provide a custom TTL for the semaphore. ./samples/semaphore-custom-ttl.ts ``` import { semaphoreFactory } from "./semaphore-factory-initial-config.js"; import { TimeSpan } from "eridu-tech/time-span"; const semaphore = semaphoreFactory.create("shared-resource", { // Default TTL is 5min if not overrided // If you set it to null it means semaphore will not expire and most be released manually. ttl: TimeSpan.fromSeconds(30), limit: 2, }); ``` ### Checking semaphore state[​](#checking-semaphore-state "Direct link to Checking semaphore state") You can get the semaphore state by using the `getState` method, it returns [`ISemaphoreState`](https://eridu-tech.github.io/eridu-tech-core/types/Semaphore.ISemaphoreState.html). ./samples/semaphore-get-state.ts ``` import { semaphoreFactory } from "./semaphore-factory-initial-config.js"; import { SEMAPHORE_STATE } from "eridu-tech/semaphore/contracts"; const semaphore = semaphoreFactory.create("shared-resource", { limit: 2, }); const state = await semaphore.getState(); if (state.type === SEMAPHORE_STATE.EXPIRED) { console.log("The semaphore doesnt exists"); } if (state.type === SEMAPHORE_STATE.LIMIT_REACHED) { console.log("The limit have been reached and all slots are unavailable"); } if (state.type === SEMAPHORE_STATE.ACQUIRED) { console.log("The semaphore is acquired"); } if (state.type === SEMAPHORE_STATE.UNACQUIRED) { console.log("There are avilable slots but the semaphore is not acquired"); } ``` ## Patterns[​](#patterns "Direct link to Patterns") ### Refreshing semaphores[​](#refreshing-semaphores "Direct link to Refreshing semaphores") The semaphore can be refreshed by the current owner before it expires. This is particularly useful for long-running tasks, instead of setting an excessively long TTL initially, you can start with a shorter one and use the `refresh` method to set the TTL of the semaphore: ./samples/semaphore-refresh.ts ``` import { semaphoreFactory } from "./semaphore-factory-initial-config.js"; import { delay } from "eridu-tech/utilities"; import { TimeSpan } from "eridu-tech/time-span"; const semaphore = semaphoreFactory.create("resource", { limit: 2, ttl: TimeSpan.fromMinutes(1), }); async function doWork(): Promise { // ... critical section return true; } const hasAcquired = await semaphore.acquire(); if (hasAcquired) { try { while (true) { await semaphore.refresh(TimeSpan.fromMinutes(1)); const hasFinished = await doWork(); if (hasFinished) { break; } await delay(TimeSpan.fromSeconds(1)); } } finally { await semaphore.release(); } } ``` warning Note: A semaphore must have an expiration (a `ttl` value) to be refreshed. You cannot refresh a semaphore that was created without an expiration (with `ttl: null`) ./samples/semaphore-non-refreshable.ts ``` import { semaphoreFactory } from "./semaphore-factory-initial-config.js"; // Create a semaphore with no expiration (non-refreshable) const semaphore = semaphoreFactory.create("resource", { limit: 2, ttl: null, }); // A refresh attempt on this semaphore will fail const hasRefreshed = await semaphore.refresh(); // This will log 'false' because the semaphore cannot be refreshed console.log(hasRefreshed); ``` ### Additional methods[​](#additional-methods "Direct link to Additional methods") The `releaseOrFail` method is the same `release` method but it throws an error when not enable to release the semaphore: ./samples/semaphore-release-or-fail.ts ``` import { semaphoreFactory } from "./semaphore-factory-initial-config.js"; const semaphore = semaphoreFactory.create("resource", { limit: 2, }); await semaphore.releaseOrFail(); ``` You can force release all the semaphore slots: ./samples/semaphore-force-release-all.ts ``` import { semaphoreFactory } from "./semaphore-factory-initial-config.js"; const semaphore = semaphoreFactory.create("resource", { limit: 2, }); await semaphore.forceReleaseAll(); ``` The `refreshOrFail` method is the same `refresh` method but it throws an error when not enable to refresh the semaphore: ./samples/semaphore-refresh-or-fail.ts ``` import { semaphoreFactory } from "./semaphore-factory-initial-config.js"; import { TimeSpan } from "eridu-tech/time-span"; const semaphore = semaphoreFactory.create("resource", { limit: 2, ttl: TimeSpan.fromMinutes(1), }); await semaphore.refreshOrFail(); ``` The `runOrFail` method automatically manages semaphore acquisition and release around function execution. It calls `acquireOrFail` before invoking the function and calls `release` in a finally block, ensuring the semaphore is always freed, even if an error occurs during execution. ./samples/semaphore-run-or-fail.ts ``` import { semaphoreFactory } from "./semaphore-factory-initial-config.js"; const semaphore = semaphoreFactory.create("resource", { limit: 2, }); await semaphore.runOrFail(async () => { // ... critical section }); ``` info Note the method throws an error when the semaphore cannot be acquired. info You can provide synchronous or asynchronous [`Invocable<[], TValue | Promise>`](/docs/utilities/invocable/.md) as values for the `runOrFail` method. ### Semaphore instance variables[​](#semaphore-instance-variables "Direct link to Semaphore instance variables") The `Semaphore` class exposes instance variables such as: ./samples/semaphore-instance-variables.ts ``` import { semaphoreFactory } from "./semaphore-factory-initial-config.js"; const semaphore = semaphoreFactory.create("resource", { limit: 2, }); // Will return the key of the semaphore which is "resource" console.log(semaphore.key.toString()); // Will return the id of the semaphore console.log(semaphore.id); // Will return the ttl of the semaphore console.log(semaphore.ttl); ``` ### Semaphore slot id[​](#semaphore-slot-id "Direct link to Semaphore slot id") By default the slot id is autogenerated but it can also manually defined. ./samples/semaphore-slot-id.ts ``` import { semaphoreFactory } from "./semaphore-factory-initial-config.js"; const semaphore = semaphoreFactory.create("semaphore", { limit: 2, slotId: "my-slot-id", }); const hasAcquire = await semaphore.acquire(); if (hasAcquire) { console.log("Shared resource"); await semaphore.release(); } ``` info Manually defining slot id is primarily useful for debugging or implementing manual resource controll by the end user. warning In most cases, setting a slot id is unnecessary. ### Retrying acquiring semaphore by attempts[​](#retrying-acquiring-semaphore-by-attempts "Direct link to Retrying acquiring semaphore by attempts") To retry acquiring semaphore you can use the [`retry`](/docs/reliability/resilience/.md#retry) middleware. Retrying acquiring semaphore with `acquireOrFail` method: ./samples/semaphore-retry-acquire-or-fail.ts ``` import { semaphoreFactory } from "./semaphore-factory-initial-config.js"; import { retry } from "eridu-tech/resilience"; import { LimitReachedSemaphoreError } from "eridu-tech/semaphore/contracts"; import { use } from "eridu-tech/middleware"; const semaphore = semaphoreFactory.create("semaphore", { limit: 2, }); try { await use(async () => { await semaphore.acquireOrFail(); }, [ retry({ maxAttempts: 4, errorPolicy: LimitReachedSemaphoreError, }), ])(); // The critical section } finally { await semaphore.release(); } ``` Retrying acquiring semaphore with `acquire` method: ./samples/semaphore-retry-acquire.ts ``` import { semaphoreFactory } from "./semaphore-factory-initial-config.js"; import { retry } from "eridu-tech/resilience"; import { use } from "eridu-tech/middleware"; const semaphore = semaphoreFactory.create("semaphore", { limit: 2, }); const hasAquired = await use(async () => { return await semaphore.acquire(); }, [ retry({ maxAttempts: 4, errorPolicy: { treatFalseAsError: true, }, }), ])(); if (hasAquired) { try { // The critical section } finally { await semaphore.release(); } } ``` Retrying acquiring semaphore with `runOrFail` method: ./samples/semaphore-retry-run-or-fail.ts ``` import { semaphoreFactory } from "./semaphore-factory-initial-config.js"; import { retry } from "eridu-tech/resilience"; import { LimitReachedSemaphoreError } from "eridu-tech/semaphore/contracts"; import { use } from "eridu-tech/middleware"; const semaphore = semaphoreFactory.create("semaphore", { limit: 2, }); await use(async () => { await semaphore.runOrFail(async () => { // The critical section }); }, [ retry({ maxAttempts: 4, errorPolicy: LimitReachedSemaphoreError, }), ])(); ``` ### Retrying acquiring semaphore by interval[​](#retrying-acquiring-semaphore-by-interval "Direct link to Retrying acquiring semaphore by interval") To retry acquiring semaphore at regular intervals you can use the [`retryInterval`](/docs/reliability/resilience/.md#retry_by_interval) middleware: Retrying acquiring semaphore with `acquireOrFail` method: ./samples/semaphore-retry-interval-acquire-or-fail.ts ``` import { semaphoreFactory } from "./semaphore-factory-initial-config.js"; import { retryInterval } from "eridu-tech/resilience"; import { LimitReachedSemaphoreError } from "eridu-tech/semaphore/contracts"; import { use } from "eridu-tech/middleware"; import { TimeSpan } from "eridu-tech/time-span"; const semaphore = semaphoreFactory.create("resource", { limit: 2, }); try { await use(async () => { await semaphore.acquireOrFail(); }, [ retryInterval({ // Time to wait 1 minute time: TimeSpan.fromMinutes(1), // Interval to try acquire the semaphore interval: TimeSpan.fromSeconds(1), errorPolicy: LimitReachedSemaphoreError, }), ])(); // ... critical section } finally { await semaphore.release(); } ``` Retrying acquiring semaphore with `acquire` method: ./samples/semaphore-retry-interval-acquire.ts ``` import { semaphoreFactory } from "./semaphore-factory-initial-config.js"; import { retryInterval } from "eridu-tech/resilience"; import { use } from "eridu-tech/middleware"; import { TimeSpan } from "eridu-tech/time-span"; const semaphore = semaphoreFactory.create("resource", { limit: 2, }); const hasAcquired = await use(async () => { return await semaphore.acquire(); }, [ retryInterval({ time: TimeSpan.fromMinutes(1), interval: TimeSpan.fromSeconds(1), errorPolicy: { treatFalseAsError: true, }, }), ])(); if (hasAcquired) { try { // ... critical section } finally { await semaphore.release(); } } ``` Retrying acquiring semaphore with `runOrFail` method: ./samples/semaphore-retry-interval-run-or-fail.ts ``` import { semaphoreFactory } from "./semaphore-factory-initial-config.js"; import { retryInterval } from "eridu-tech/resilience"; import { LimitReachedSemaphoreError } from "eridu-tech/semaphore/contracts"; import { use } from "eridu-tech/middleware"; import { TimeSpan } from "eridu-tech/time-span"; const semaphore = semaphoreFactory.create("resource", { limit: 2, }); await use(async () => { await semaphore.runOrFail(async () => { // ... critical section }); }, [ retryInterval({ time: TimeSpan.fromMinutes(1), interval: TimeSpan.fromSeconds(1), errorPolicy: LimitReachedSemaphoreError, }), ])(); ``` warning Note using `retryInterval` middleware with semaphore acquiring in a HTTP request handler is discouraged because it blocks the HTTP request handler causing the handler wait until the semaphore becomes available or the timeout is reached. This will delay HTTP request handler to generate response and will make frontend app slow because of HTTP request handler. ### Serialization and deserialization of semaphore[​](#serialization-and-deserialization-of-semaphore "Direct link to Serialization and deserialization of semaphore") Semaphores can be serialized, allowing them to be transmitted over the network to another server and later deserialized for reuse. This means you can, for example, acquire the semaphore on the main server, transfer it to a queue worker server, and release it there. In order to serialize or deserialize a semaphore you need pass an object that implements [`ISerderRegister`](/docs/foundation/serde/.md) contract like the [`Serde`](/docs/foundation/serde/.md) class to `SemaphoreFactory`. Manually serializing and deserializing the semaphore: ./samples/semaphore-manual-serialization.ts ``` import { Redis } from "ioredis"; import { RedisSemaphoreAdapter } from "eridu-tech/semaphore/redis-semaphore-adapter"; import { SemaphoreFactory } from "eridu-tech/semaphore"; import { SuperJsonSerde } from "eridu-tech/serde/super-json-serde"; const serde = new SuperJsonSerde(); const redisClient = new Redis("YOUR_REDIS_CONNECTION"); const semaphoreFactory = new SemaphoreFactory({ // You can laso pass in an array of Serde class instances serde, adapter: new RedisSemaphoreAdapter(redisClient), }); const semaphore = semaphoreFactory.create("resource", { limit: 2, }); const serializedSemaphore = await serde.serialize(semaphore); const deserializedSemaphore = await serde.deserialize(serializedSemaphore); ``` danger When serializing or deserializing a semaphore, you must use the same `Serde` instances that were provided to the `SemaphoreFactory`. This is required because the `SemaphoreFactory` injects custom serialization logic for `ISemaphore` instance into `Serde` instances. info Note you only need manuall serialization and deserialization when integrating with external libraries. As long you pass the same `Serde` instances with all other components you dont need to serialize and deserialize the semaphore manually. ./samples/semaphore-event-bus-serialization.ts ``` import { Redis } from "ioredis"; import { RedisSemaphoreAdapter } from "eridu-tech/semaphore/redis-semaphore-adapter"; import type { ISemaphore } from "eridu-tech/semaphore/contracts"; import { SemaphoreFactory } from "eridu-tech/semaphore"; import { RedisPubSubEventBusAdapter } from "eridu-tech/event-bus/redis-pub-sub-event-bus-adapter"; import { EventBus } from "eridu-tech/event-bus"; import { SuperJsonSerde } from "eridu-tech/serde/super-json-serde"; const serde = new SuperJsonSerde(); const redis = new Redis("YOUR_REDIS_CONNECTION"); type EventMap = { "sending-semaphore-over-network": { semaphore: ISemaphore; }; }; const eventBus = new EventBus({ adapter: new RedisPubSubEventBusAdapter({ client: redis, serde, }), }); const semaphoreFactory = new SemaphoreFactory({ serde, adapter: new RedisSemaphoreAdapter(redis), }); const semaphore = semaphoreFactory.create("resource", { limit: 2, }); // We are sending the semaphore over the network to other servers. await eventBus.dispatch("sending-semaphore-over-network", { semaphore, }); // The other servers will recieve the serialized semaphore and automattically deserialize it. await eventBus.addListener( "sending-semaphore-over-network", ({ semaphore }) => { // The semaphore is deserialized and can be used console.log("SEMAPHORE:", semaphore); }, ); ``` ### Separating semaphore creation from manipulation[​](#separating-semaphore-creation-from-manipulation "Direct link to Separating semaphore creation from manipulation") The library includes 2 additional contracts: * [`ISemaphore`](https://eridu-tech.github.io/eridu-tech-core/types/Semaphore.ISemaphore.html) - Allows only for manipulating of the semaphore. * [`ISemaphoreFactory`](https://eridu-tech.github.io/eridu-tech-core/types/Semaphore.ISemaphoreFactory.html) - Allows only for creation of semaphores. ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/semaphore`](https://eridu-tech.github.io/eridu-tech-core/modules/Semaphore.html) API docs. --- # DI Integration The `semaphoreFactoryResolverDiFactory` function creates an [`ISemaphoreFactoryResolver`](/docs/concurrency/semaphore/semaphore_factory_resolver/.md) whose underlying resolver is resolved from a [DI container](/docs/foundation/di/.md). The returned proxy implements both `ISemaphoreFactoryResolver` and `ISemaphoreFactory`: * `use(adapterName?)` selects an adapter on the resolved `SemaphoreFactoryResolver`. * `create(key, settings)` creates a semaphore through the resolver's default adapter. The proxy is wired to the container during `container.init()`. Create it **before** calling `init()`; calling `use()` or `create()` before the container is initialized throws an error. ## Initial configuration[​](#initial-configuration "Direct link to Initial configuration") Set up the memory and Redis semaphore adapters, create the container, then register the `SemaphoreFactoryResolver` and wrap it with `semaphoreFactoryResolverDiFactory`: #### 1. Set up the memory semaphore adapter[​](#1-set-up-the-memory-semaphore-adapter "Direct link to 1. Set up the memory semaphore adapter") ./samples/memory-semaphore-adapter.ts ``` import { MemorySemaphoreAdapter } from "eridu-tech/semaphore/memory-semaphore-adapter"; export const memorySemaphoreAdapter = new MemorySemaphoreAdapter(); ``` #### 2. Set up the Redis semaphore adapter[​](#2-set-up-the-redis-semaphore-adapter "Direct link to 2. Set up the Redis semaphore adapter") ./samples/redis-semaphore-adapter.ts ``` import { RedisSemaphoreAdapter } from "eridu-tech/semaphore/redis-semaphore-adapter"; import { Redis } from "ioredis"; export const redisSemaphoreAdapter = new RedisSemaphoreAdapter( new Redis("YOUR_REDIS_CONNECTION_STRING"), ); ``` #### 3. Create the dependency injection container[​](#3-create-the-dependency-injection-container "Direct link to 3. Create the dependency injection container") ./samples/container.ts ``` import { Container } from "eridu-tech/di"; import { ExecutionContext } from "eridu-tech/execution-context"; import { AlsExecutionContextAdapter } from "eridu-tech/execution-context/als-execution-context-adapter"; export const executionContext = new ExecutionContext( new AlsExecutionContextAdapter(), ); export const container = new Container({ executionContext }); ``` #### 4. Register and initialize the semaphore factory resolver[​](#4-register-and-initialize-the-semaphore-factory-resolver "Direct link to 4. Register and initialize the semaphore factory resolver") ./samples/semaphore-factory-resolver-di-factory.ts ``` import { SemaphoreFactoryResolver } from "eridu-tech/semaphore"; import { ProxySemaphoreFactoryResolver } from "eridu-tech/semaphore/di"; import { container } from "./container.js"; import { memorySemaphoreAdapter } from "./memory-semaphore-adapter.js"; import { redisSemaphoreAdapter } from "./redis-semaphore-adapter.js"; type Adapters = "memory" | "redis"; const semaphoreFactoryResolver = new SemaphoreFactoryResolver({ adapters: { memory: memorySemaphoreAdapter, redis: redisSemaphoreAdapter, }, defaultAdapter: "memory", }); await semaphoreFactoryResolver.init(); container.registerValue({ token: SemaphoreFactoryResolver, value: semaphoreFactoryResolver, }); // Create the proxy before container.init() export const semaphoreFactory = new ProxySemaphoreFactoryResolver({ container, resolverToken: SemaphoreFactoryResolver, }); await container.init(); ``` ## Usage[​](#usage "Direct link to Usage") ### 1. Using the default adapter[​](#1-using-the-default-adapter "Direct link to 1. Using the default adapter") ./samples/semaphore-factory-resolver-di-factory-default-adapter.ts ``` import { semaphoreFactory } from "./semaphore-factory-resolver-di-factory.js"; // Uses the adapter configured as the default (memory) await semaphoreFactory .create("shared-resource", { limit: 2, }) .runOrFail(async () => { // code to run }); ``` ### 2. Specifying an adapter explicitly[​](#2-specifying-an-adapter-explicitly "Direct link to 2. Specifying an adapter explicitly") ./samples/semaphore-factory-resolver-di-factory-specific-adapter.ts ``` import { semaphoreFactory } from "./semaphore-factory-resolver-di-factory.js"; // Uses the Redis adapter await semaphoreFactory .use("redis") .create("shared-resource", { limit: 2, }) .runOrFail(async () => { // code to run }); ``` ## Using it with the middlewares[​](#using-it-with-the-middlewares "Direct link to Using it with the middlewares") Because the proxy implements `ISemaphoreFactoryResolver`, it can be passed straight to the [semaphore middleware factories](/docs/concurrency/semaphore/semaphore_middlewares/.md): ./samples/semaphore-factory-resolver-di-factory-with-middleware.ts ``` import { use } from "eridu-tech/middleware"; import { withSemaphoreFactory } from "eridu-tech/semaphore/middlewares"; import { semaphoreFactory } from "./semaphore-factory-resolver-di-factory.js"; const withSemaphore = withSemaphoreFactory(semaphoreFactory); const processFile = async (filePath: string): Promise => { // ... process the file }; const throttledProcess = use( processFile, withSemaphore.use("redis")({ key: ([filePath]) => `file-path:${filePath}`, limit: 3, }), ); await throttledProcess("/data/file.json"); ``` ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/semaphore`](https://eridu-tech.github.io/eridu-tech-core/modules/Semaphore.html) API docs. --- # Configuring SharedLock adapters ## MemorySharedLockAdapter[​](#memorysharedlockadapter "Direct link to MemorySharedLockAdapter") To use the `MemorySharedLockAdapter` you only need to create instance of it: ./samples/memory-shared-lock-adapter.ts ``` import { MemorySharedLockAdapter } from "eridu-tech/shared-lock/memory-shared-lock-adapter"; export const memorySharedLockAdapter = new MemorySharedLockAdapter(); ``` You can also provide an `Map` that will be used for storing the data in memory: ./samples/memory-shared-lock-adapter-with-map.ts ``` import { MemorySharedLockAdapter } from "eridu-tech/shared-lock/memory-shared-lock-adapter"; const map = new Map(); const memorySharedLockAdapter = new MemorySharedLockAdapter(map); ``` info `MemorySharedLockAdapter` lets you test your app without external dependencies like `Redis`, ideal for local development, unit tests, integration tests and fast E2E test for the backend application. danger Note the `MemorySharedLockAdapter` is limited to single process usage and cannot be shared across multiple servers or processes. ### Settings[​](#settings "Direct link to Settings") To clean up expired shared-lock keys, call `removeAllExpired` at a regular interval (for example, using a cron job): ./samples/memory-shared-lock-remove-all-expired.ts ``` import { memorySharedLockAdapter } from "./memory-shared-lock-adapter.js"; // Remove all expired shared-lock keys manually. await memorySharedLockAdapter.removeAllExpired(); ``` To remove the shared-lock map and all stored shared-lock data, use `deInit` method: ./samples/memory-shared-lock-adapter-deinit.ts ``` import { memorySharedLockAdapter } from "./memory-shared-lock-adapter.js"; await memorySharedLockAdapter.deInit(); ``` ## MongodbSharedLockAdapter[​](#mongodbsharedlockadapter "Direct link to MongodbSharedLockAdapter") To use the `MongodbSharedLockAdapter`, you'll need to: 1. Install the required dependency: [`mongodb`](https://www.npmjs.com/package/mongodb) package. ### Setup[​](#setup "Direct link to Setup") Connect to MongoDB: ./samples/mongodb-shared-lock-adapter-setup.ts ``` import { MongoClient } from "mongodb"; const client = await MongoClient.connect("YOUR_MONGODB_CONNECTION_STRING"); export const database = client.db("database"); ``` info The `database` setting also accepts a [`TransactionContext`](/docs/foundation/transaction_context/transaction_context_usage/.md), which makes the adapter transaction aware. Adapters given the same instance share the same transaction when available. ### Usage[​](#usage "Direct link to Usage") Create the adapter: ./samples/mongodb-shared-lock-adapter.ts ``` import { MongodbSharedLockAdapter } from "eridu-tech/shared-lock/mongodb-shared-lock-adapter"; import { database } from "./mongodb-shared-lock-adapter-setup.js"; export const mongodbSharedLockAdapter = new MongodbSharedLockAdapter({ database, }); // You need initialize the adapter once before using it. // During the initialization the indexes will be created await mongodbSharedLockAdapter.init(); ``` You can change the collection name: ./samples/mongodb-shared-lock-collection-name.ts ``` import { MongodbSharedLockAdapter } from "eridu-tech/shared-lock/mongodb-shared-lock-adapter"; import { database } from "./mongodb-shared-lock-adapter-setup.js"; const mongodbSharedLockAdapter = new MongodbSharedLockAdapter({ database, // By default "shared-lock" is used as collection name collectionName: "my-shared-lock", }); await mongodbSharedLockAdapter.init(); ``` You can change the collection settings: ./samples/mongodb-shared-lock-collection-settings.ts ``` import { MongodbSharedLockAdapter } from "eridu-tech/shared-lock/mongodb-shared-lock-adapter"; import { database } from "./mongodb-shared-lock-adapter-setup.js"; const mongodbSharedLockAdapter = new MongodbSharedLockAdapter({ database, // You configure additional collection settings collectionSettings: {}, }); await mongodbSharedLockAdapter.init(); ``` info To remove the shared-lock collection and all stored shared-lock data, use `deInit` method: ./samples/mongodb-shared-lock-adapter-deinit.ts ``` import { mongodbSharedLockAdapter } from "./mongodb-shared-lock-adapter.js"; await mongodbSharedLockAdapter.deInit(); ``` danger Note in order to use `MongodbSharedLockAdapter` correctly, ensure you use a single, consistent database across all server instances or processes. ## RedisSharedLockAdapter[​](#redissharedlockadapter "Direct link to RedisSharedLockAdapter") To use the `RedisSharedLockAdapter`, you'll need to: 1. Install the required dependency: [`ioredis`](https://www.npmjs.com/package/ioredis) package. ### Setup[​](#setup-1 "Direct link to Setup") Connect to Redis: ./samples/redis-shared-lock-adapter-setup.ts ``` import { Redis } from "ioredis"; export const database = new Redis("YOUR_REDIS_CONNECTION_STRING"); ``` ### Usage[​](#usage-1 "Direct link to Usage") Create the adapter: ./samples/redis-shared-lock-adapter.ts ``` import { RedisSharedLockAdapter } from "eridu-tech/shared-lock/redis-shared-lock-adapter"; import { database } from "./redis-shared-lock-adapter-setup.js"; const redisSharedLockAdapter = new RedisSharedLockAdapter(database); ``` danger Note in order to use `RedisSharedLockAdapter` correctly, ensure you use a single, consistent database across all server instances or processes. ## KyselySharedLockAdapter[​](#kyselysharedlockadapter "Direct link to KyselySharedLockAdapter") To use the `KyselySharedLockAdapter`, you'll need to: 1. Use database provider that has support for transactions. 2. Install the required dependency: [`kysely`](https://www.npmjs.com/package/kysely) package. ### Setup[​](#setup-2 "Direct link to Setup") Create a function that creates the [`TransactionContext`](/docs/foundation/transaction_context/transaction_context_usage/.md) by wrapping a `Kysely` instance in a [`KyselyTransactionAdapter`](/docs/foundation/transaction_context/configuring_transaction_context_adapters/.md#kysely_transaction_adapter): ./samples/kysely-shared-lock-adapter-setup.ts ``` import { ExecutionContext } from "eridu-tech/execution-context"; import { AlsExecutionContextAdapter } from "eridu-tech/execution-context/als-execution-context-adapter"; import { contextToken } from "eridu-tech/execution-context/contracts"; import { TransactionContext } from "eridu-tech/transaction-context"; import { KyselyTransactionAdapter } from "eridu-tech/transaction-context/kysely-transaction-adapter"; import type { ITransactionContext } from "eridu-tech/transaction-context/contracts"; import type { Kysely } from "kysely"; const executionContext = new ExecutionContext(new AlsExecutionContextAdapter()); // `KyselySharedLockAdapter` is transaction aware: it runs every shared-lock // operation through the `current` client of this context, which is the // transaction-scoped client while a transaction is active and the base client // otherwise. export function createTransactionContext( kysely: Kysely, ): ITransactionContext> { return new TransactionContext>({ token: contextToken("kysely"), adapter: new KyselyTransactionAdapter({ database: kysely, }), executionContext, }); } ``` info The `transactionContext` setting makes the adapter transaction aware. Adapters given the same instance share the same transaction when available. ### With Sqlite[​](#with-sqlite "Direct link to With Sqlite") You will need to install [`better-sqlite3`](https://www.npmjs.com/package/better-sqlite3) package: ./samples/kysely-shared-lock-sqlite.ts ``` import { KyselySharedLockAdapter } from "eridu-tech/shared-lock/kysely-shared-lock-adapter"; import Sqlite from "better-sqlite3"; import { Kysely, SqliteDialect } from "kysely"; import { createTransactionContext } from "./kysely-shared-lock-adapter-setup.js"; const database = new Sqlite("DATABASE_NAME.db"); const kysely = new Kysely({ dialect: new SqliteDialect({ database, }), }); const transactionContext = createTransactionContext(kysely); export const kyselySharedLockAdapter = new KyselySharedLockAdapter({ transactionContext, }); // You need initialize the adapter once before using it. // During the initialization the schema will be created await kyselySharedLockAdapter.init(); ``` danger Note using `KyselySharedLockAdapter` with `sqlite` is limited to single server usage and cannot be shared across multiple servers but it can be shared between different processes. To use it correctly, ensure all process instances access the same persisted database. ### With Postgres[​](#with-postgres "Direct link to With Postgres") You will need to install [`pg`](https://www.npmjs.com/package/pg) package: ./samples/kysely-shared-lock-postgres.ts ``` import { KyselySharedLockAdapter } from "eridu-tech/shared-lock/kysely-shared-lock-adapter"; import { Pool } from "pg"; import { Kysely, PostgresDialect } from "kysely"; import { createTransactionContext } from "./kysely-shared-lock-adapter-setup.js"; const database = new Pool({ database: "DATABASE_NAME", host: "DATABASE_HOST", user: "DATABASE_USER", // DATABASE port port: 5432, password: "DATABASE_PASSWORD", max: 10, }); const kysely = new Kysely({ dialect: new PostgresDialect({ pool: database, }), }); const transactionContext = createTransactionContext(kysely); const kyselySharedLockAdapter = new KyselySharedLockAdapter({ transactionContext, }); // You need initialize the adapter once before using it. // During the initialization the schema will be created await kyselySharedLockAdapter.init(); ``` danger Note in order to use `KyselySharedLockAdapter` with `postgres` correctly, ensure you use a single, consistent database across all server instances. This means you can't use replication. ### Settings[​](#settings-1 "Direct link to Settings") To clean up expired shared-lock keys, call `removeAllExpired` at a regular interval (for example, using a cron job): ./samples/kysely-shared-lock-remove-all-expired.ts ``` import { kyselySharedLockAdapter } from "./kysely-shared-lock-sqlite.js"; // Remove all expired shared-lock keys manually. await kyselySharedLockAdapter.removeAllExpired(); ``` To remove the shared-lock table and all stored shared-lock data, use `deInit` method: ./samples/kysely-shared-lock-adapter-deinit.ts ``` import { kyselySharedLockAdapter } from "./kysely-shared-lock-sqlite.js"; await kyselySharedLockAdapter.deInit(); ``` ## NoOpSharedLockAdapter[​](#noopsharedlockadapter "Direct link to NoOpSharedLockAdapter") The `NoOpSharedLockAdapter` is a no-operation implementation, it performs no actions when called: ./samples/no-op-shared-lock-adapter.ts ``` import { NoOpSharedLockAdapter } from "eridu-tech/shared-lock/no-op-shared-lock-adapter"; const noOpSharedLockAdapter = new NoOpSharedLockAdapter(); ``` info The `NoOpSharedLockAdapter` is useful when you want to mock out or disable your [`SharedLockFactory`](https://eridu-tech.github.io/eridu-tech-core/classes/SharedLock.SharedLockFactory.html) instance. ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/shared-lock`](https://eridu-tech.github.io/eridu-tech-core/modules/SharedLock.html) API docs. --- # Creating SharedLock adapters ## Implementing your custom ISharedLockAdapter[​](#implementing-your-custom-isharedlockadapter "Direct link to Implementing your custom ISharedLockAdapter") In order to create an adapter you need to implement the [`ISharedLockAdapter`](https://eridu-tech.github.io/eridu-tech-core/types/SharedLock.ISharedLockAdapter.html) contract. ## Testing your custom ISharedLockAdapter[​](#testing-your-custom-isharedlockadapter "Direct link to Testing your custom ISharedLockAdapter") We provide a complete test suite to test your shared-lock adapter implementation. Simply use the [`sharedLockAdapterTestSuite`](https://eridu-tech.github.io/eridu-tech-core/functions/Lock.lockAdapterTestSuite.html) function: The suite provides preconfigured Vitest test cases with common edge case coverage and standardized shared-lock adapter contract conformance testing. Usage example: ./samples/shared-lock-adapter-test-suite.ts ``` import { beforeEach, describe, expect, test } from "vitest"; import { sharedLockAdapterTestSuite } from "eridu-tech/shared-lock/test-utilities"; import { MemorySharedLockAdapter } from "eridu-tech/shared-lock/memory-shared-lock-adapter"; describe("class: MySharedLockAdapter", () => { sharedLockAdapterTestSuite({ createAdapter: () => new MemorySharedLockAdapter(), test, beforeEach, expect, describe, }); }); ``` ## Implementing your custom ISharedLockFactory class[​](#implementing-your-custom-isharedlockfactory-class "Direct link to Implementing your custom ISharedLockFactory class") In some cases, you may need to implement a custom [`SharedLockFactory`](https://eridu-tech.github.io/eridu-tech-core/classes/SharedLock.SharedLockFactory.html) class to optimize performance for your specific technology stack. You can then directly implement the [`ISharedLockFactory`](https://eridu-tech.github.io/eridu-tech-core/types/SharedLock.ISharedLockFactory.html) contract. ## Testing your custom ISharedLockFactory class[​](#testing-your-custom-isharedlockfactory-class "Direct link to Testing your custom ISharedLockFactory class") We provide a complete test suite to verify your custom event-bus class implementation. Simply use the [`sharedLockProviderTestSuite`](https://eridu-tech.github.io/eridu-tech-core/functions/SharedLock.sharedLockProviderTestSuite.html) function: The suite provides preconfigured Vitest test cases with common edge case coverage and standardized shared-lock factory contract conformance testing. Usage example: ./samples/shared-lock-factory-test-suite.ts ``` import { beforeEach, describe, expect, test } from "vitest"; import { sharedLockFactoryTestSuite } from "eridu-tech/shared-lock/test-utilities"; import { SharedLockFactory } from "eridu-tech/shared-lock"; import { MemorySharedLockAdapter } from "eridu-tech/shared-lock/memory-shared-lock-adapter"; import { SuperJsonSerde } from "eridu-tech/serde/super-json-serde"; describe("class: SharedLockFactory", () => { sharedLockFactoryTestSuite({ createSharedLockFactory: () => ({ sharedLockFactory: new SharedLockFactory({ adapter: new MemorySharedLockAdapter(), }), serde: new SuperJsonSerde(), }), test, beforeEach, expect, describe, }); }); ``` ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/shared-lock`](https://eridu-tech.github.io/eridu-tech-core/modules/SharedLock.html) API docs. --- # SharedLockFactoryResolver The `SharedLockFactoryResolver` class provides a flexible way to configure and switch between different shared-lock adapters at runtime. ## Initial configuration[​](#initial-configuration "Direct link to Initial configuration") To begin using the `ISharedLockFactoryResolver`, you will need to register all required adapters during initialization. ./samples/shared-lock-factory-resolver-initial-config.ts ``` import { SharedLockFactoryResolver } from "eridu-tech/shared-lock"; import { MemorySharedLockAdapter } from "eridu-tech/shared-lock/memory-shared-lock-adapter"; import { RedisSharedLockAdapter } from "eridu-tech/shared-lock/redis-shared-lock-adapter"; import { Redis } from "ioredis"; export const sharedLockFactoryResolver = new SharedLockFactoryResolver({ adapters: { memory: new MemorySharedLockAdapter(), redis: new RedisSharedLockAdapter(new Redis("YOUR_REDIS_CONNECTION")), }, // You can set an optional default adapter defaultAdapter: "memory", }); await sharedLockFactoryResolver.init(); ``` ## Usage[​](#usage "Direct link to Usage") ### 1. Using the default adapter[​](#1-using-the-default-adapter "Direct link to 1. Using the default adapter") ./samples/shared-lock-factory-resolver-default-adapter.ts ``` import { sharedLockFactoryResolver } from "./shared-lock-factory-resolver-initial-config.js"; await sharedLockFactoryResolver .use() .create("shared-resource", { limit: 4, }) .runWriterOrFail(async () => { // code to run }); ``` danger Note that if you dont set a default adapter, an error will be thrown. ### 2. Specifying an adapter explicitly[​](#2-specifying-an-adapter-explicitly "Direct link to 2. Specifying an adapter explicitly") ./samples/shared-lock-factory-resolver-specific-adapter.ts ``` import { sharedLockFactoryResolver } from "./shared-lock-factory-resolver-initial-config.js"; await sharedLockFactoryResolver .use("redis") .create("shared-resource", { limit: 4, }) .runWriterOrFail(async () => { // code to run }); ``` danger Note that if you specify a non-existent adapter, an error will be thrown. ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/shared-lock`](https://eridu-tech.github.io/eridu-tech-core/modules/SharedLock.html) API docs. --- # SharedLock middlewares ## Initial configuration[​](#initial-configuration "Direct link to Initial configuration") To begin using the shared-lock middlewares, you'll need to create and configure a `SharedLockFactoryResolver` instance: ./samples/shared-lock.ts ``` import { SharedLockFactoryResolver } from "eridu-tech/shared-lock"; import { MemorySharedLockAdapter } from "eridu-tech/shared-lock/memory-shared-lock-adapter"; import { RedisSharedLockAdapter } from "eridu-tech/shared-lock/redis-shared-lock-adapter"; import { Redis } from "ioredis"; export const sharedLockFactoryResolver = new SharedLockFactoryResolver({ adapters: { memory: new MemorySharedLockAdapter(), redis: new RedisSharedLockAdapter( new Redis("YOUR_REDIS_CONNECTION_STRING"), ), }, defaultAdapter: "memory", }); await sharedLockFactoryResolver.init(); ``` ## withSharedLockFactory middleware[​](#with_shared_lock_factory_middleware "Direct link to withSharedLockFactory middleware") The SharedLock middleware wraps function calls with a distributed shared lock (reader-writer lock), providing concurrency control with two access modes: **Reader mode** (`"READER"`) allows multiple callers to execute the wrapped function concurrently as long as no writer holds the lock, while **Writer mode** (`"WRITER"`) grants exclusive access. Before executing the wrapped function, the appropriate lock is acquired on a key derived from the function's arguments. ### Usage[​](#usage "Direct link to Usage") ./samples/with-shared-lock.ts ``` import { withSharedLockFactory, SHARED_LOCK_WHEN, } from "eridu-tech/shared-lock/middlewares"; import { use } from "eridu-tech/middleware"; import { sharedLockFactoryResolver } from "./shared-lock.js"; const withSharedLock = withSharedLockFactory(sharedLockFactoryResolver); const readData = async (key: string): Promise => { // Safe to run concurrently with other readers return { data: "..." }; }; // Wrap with shared-lock in reader mode using the default adapter (`memory`) — multiple readers allowed const safeRead = use( readData, withSharedLock({ key: ([resourceKey]) => `data:${resourceKey}`, when: SHARED_LOCK_WHEN.READER, limit: 10, // Up to 10 concurrent readers }), ); // Wrap with shared-lock in reader mode using a specific adapter (`redis`) const safeReadOnRedis = use( readData, withSharedLock.use("redis")({ key: ([resourceKey]) => `data:${resourceKey}`, when: SHARED_LOCK_WHEN.READER, limit: 10, }), ); const writeData = async (key: string): Promise => { // Safe to run concurrently as only writer }; // Wrap with shared-lock in writer mode using the default adapter (`memory`) — only writer allowed const safeWrite = use( writeData, withSharedLock({ key: ([resourceKey]) => `data:${resourceKey}`, when: SHARED_LOCK_WHEN.WRITER, limit: 10, }), ); // Wrap with shared-lock in writer mode using a specific adapter (`redis`) const safeWriteOnRedis = use( writeData, withSharedLock.use("redis")({ key: ([resourceKey]) => `data:${resourceKey}`, when: SHARED_LOCK_WHEN.WRITER, limit: 10, }), ); await safeReadOnRedis("config"); await safeWrite("config"); await safeWriteOnRedis("config"); ``` Calling `withSharedLock()` directly uses the resolver's default adapter (`memory`). Use `withSharedLock.use("redis")` to run the wrapped function through a specific registered adapter. info Here is a complete list of settings for the [`withSharedLock`](https://eridu-tech.github.io/eridu-tech-core/types/SharedLock.WithSharedLockFactorySettings.html) function. ### Settings[​](#settings "Direct link to Settings") | Option | Type | Description | | -------- | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | `key` | `Invocable` | A function that produces the lock key from the wrapped function's arguments. All consumers using the same key share the same lock state | | `when` | `SharedLockWhenSetting` | Whether to acquire the lock in `"READER"` or `"WRITER"` mode | | `limit` | `number` | Maximum number of concurrent readers allowed when the lock is acquired in reader mode | | `lockId` | `Invocable` | Optional function that produces a unique identifier for the current lock acquisition attempt. Defaults to a UUID (`v4`) | | `ttl` | `ITimeSpan \| null` | Time-to-live for the lock. `null` means the lock never expires automatically; if omitted the factory's default TTL is used | ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/shared-lock`](https://eridu-tech.github.io/eridu-tech-core/modules/SharedLock.html) API docs. --- # SharedLock Plugins ## withSharedLockPrefix plugin[​](#withsharedlockprefix-plugin "Direct link to withSharedLockPrefix plugin") The SharedLock prefix plugin intercepts calls to a shared-lock adapter and transparently prefixes all lock keys with a configurable string. This enables logical key namespacing without modifying the adapter implementation. ### Use cases[​](#use-cases "Direct link to Use cases") * **Multi-tenant reader-writer locks** — Prefix lock keys with a tenant identifier to isolate shared lock state between tenants * **Resource scoping** — Organize shared locks by resource type or module * **Environment isolation** — Separate development, staging, and production shared lock state * **Read/write path scoping** — Apply consistent namespacing to both reader and writer lock operations ### How it works[​](#how-it-works "Direct link to How it works") The `withSharedLockPrefix` function returns a [`PluginFn`](/docs/foundation/middleware/.md) that calls `enhance` on each adapter method that accepts a shared-lock key. When an enhanced method is invoked, the plugin intercepts the call, prepends the configured prefix to the key argument, and forwards the modified arguments to the original method. The plugin prefixes keys for the following methods: | Method | Key argument | Pattern | | --------------- | ---------------------------- | -------------- | | `forceRelease` | First argument (`key`) | `prefix + key` | | `getState` | First argument (`key`) | `prefix + key` | | `acquireWriter` | First argument (`key`) | `prefix + key` | | `refreshWriter` | First argument (`key`) | `prefix + key` | | `releaseWriter` | First argument (`key`) | `prefix + key` | | `acquireReader` | `key` within settings object | `prefix + key` | | `refreshReader` | First argument (`key`) | `prefix + key` | ### Usage[​](#usage "Direct link to Usage") ./samples/with-shared-lock-prefix.ts ``` import { withPlugin } from "eridu-tech/middleware"; import { MemorySharedLockAdapter } from "eridu-tech/shared-lock/memory-shared-lock-adapter"; import { withSharedLockPrefix } from "eridu-tech/shared-lock/plugins"; const adapter = new MemorySharedLockAdapter(); // Apply the prefix plugin to the adapter const prefixedAdapter = withPlugin(adapter, withSharedLockPrefix("tenant-42:")); ``` ### Before/after behavior[​](#beforeafter-behavior "Direct link to Before/after behavior") **Before** — Shared lock keys are used as-is: ./samples/unprefixed-acquire-writer.ts ``` import { MemorySharedLockAdapter } from "eridu-tech/shared-lock/memory-shared-lock-adapter"; const adapter = new MemorySharedLockAdapter(); const ttl = new Date(Date.now() + 60_000); await adapter.acquireWriter("doc:42", "writer-1", ttl); // -> acquires writer lock on "doc:42" ``` **After** — Shared lock keys are automatically prefixed: ./samples/prefixed-acquire-writer.ts ``` import { withPlugin } from "eridu-tech/middleware"; import { MemorySharedLockAdapter } from "eridu-tech/shared-lock/memory-shared-lock-adapter"; import { withSharedLockPrefix } from "eridu-tech/shared-lock/plugins"; const adapter = new MemorySharedLockAdapter(); // Apply the prefix plugin to the adapter const prefixedAdapter = withPlugin(adapter, withSharedLockPrefix("tenant-42:")); const ttl = new Date(Date.now() + 60_000); await prefixedAdapter.acquireWriter("doc:42", "writer-1", ttl); // -> acquires writer lock on "tenant-42:doc:42" ``` danger Because `withPlugin` uses `enhance` under the hood, the same edge case applies: if one enhanced method internally calls another enhanced method via `this`, the middleware will apply **twice**. Be mindful of inter-method calls when applying plugins that enhance multiple methods on the same instance. info For more information about the `withPlugin` function and applying plugins to adapters, see the [Middleware plugin](/docs/foundation/middleware/.md#plugin) documentation. --- # SharedLock usage The `eridu-tech/shared-lock` component provides a way for managing shared-locks (a.k.a reader writer locks) independent of underlying platform or storage. ## Initial configuration[​](#initial-configuration "Direct link to Initial configuration") To begin using the `SharedLockFactory` class, you'll need to create and configure an instance: ./samples/shared-lock-factory-initial-config.ts ``` import { TimeSpan } from "eridu-tech/time-span"; import { MemorySharedLockAdapter } from "eridu-tech/shared-lock/memory-shared-lock-adapter"; import { SharedLockFactory } from "eridu-tech/shared-lock"; export const sharedLockFactory = new SharedLockFactory({ // You can provide default TTL value // If you set it to null it means shared-locks will not expire and most be released manually by default. defaultTtl: TimeSpan.fromSeconds(2), // You can choose the adapter to use adapter: new MemorySharedLockAdapter(), }); await sharedLockFactory.init(); ``` info Here is a complete list of settings for the [`SharedLockFactory`](https://eridu-tech.github.io/eridu-tech-core/types/SharedLock.SharedLockFactorySettingsBase.html) class. ## SharedLock basics[​](#sharedlock-basics "Direct link to SharedLock basics") ### Creating a shared-lock[​](#creating-a-shared-lock "Direct link to Creating a shared-lock") ./samples/shared-lock-create.ts ``` import { sharedLockFactory } from "./shared-lock-factory-initial-config.js"; export const sharedLock = sharedLockFactory.create("shared-resource", { // You need to define a limit limit: 2, }); ``` ### Acquiring and releasing the shared-lock as reader[​](#acquiring-and-releasing-the-shared-lock-as-reader "Direct link to Acquiring and releasing the shared-lock as reader") ./samples/shared-lock-acquire-reader.ts ``` import { sharedLock } from "./shared-lock-create.js"; // 1 slot will be acquired if (await sharedLock.acquireReader()) { console.log("Acquired"); try { // The concurrent section } finally { await sharedLock.releaseReader(); } } else { console.log("Unable to acquire"); } // 2 slots will be acquired if (await sharedLock.acquireReader()) { console.log("Acquired"); try { // The concurrent section } finally { await sharedLock.releaseReader(); } } else { console.log("Unable to acquire"); } // Will log false because the limit is reached console.log(await sharedLock.acquireReader()); ``` Alternatively you could write it as follows: ./samples/shared-lock-acquire-reader-or-fail.ts ``` import { sharedLock } from "./shared-lock-create.js"; // 1 slot will be acquired try { console.log("Acquired"); // This method will throw if the shared-lock limit is reached. await sharedLock.acquireReaderOrFail(); // The critical section } catch { console.log("Unable to acquire"); } finally { await sharedLock.releaseReader(); } // 2 slots will be acquired try { console.log("Acquired"); // This method will throw if the shared-lock limit is reached. await sharedLock.acquireReaderOrFail(); // The critical section } catch { console.log("Unable to acquire"); } finally { await sharedLock.releaseReader(); } // Will throw because the limit is reached await sharedLock.acquireReaderOrFail(); ``` danger You need always to wrap the concurrent section with `try-finally` so the shared-lock get released when error occurs. ### Acquiring and releasing the shared-lock as writer[​](#acquiring-and-releasing-the-shared-lock-as-writer "Direct link to Acquiring and releasing the shared-lock as writer") ./samples/shared-lock-acquire-writer.ts ``` import { sharedLock } from "./shared-lock-create.js"; const hasAquired = await sharedLock.acquireWriter(); if (hasAquired) { try { // The critical section } finally { await sharedLock.releaseWriter(); } } ``` Alternatively you could write it as follows: ./samples/shared-lock-acquire-writer-or-fail.ts ``` import { sharedLock } from "./shared-lock-create.js"; try { // This method will throw if the shared-lock is not acquired await sharedLock.acquireWriterOrFail(); // The critical section } finally { await sharedLock.releaseWriter(); } ``` danger You need always to wrap the critical section with `try-finally` so the shared-lock get released when error occurs. ### SharedLock with custom TTL[​](#sharedlock-with-custom-ttl "Direct link to SharedLock with custom TTL") You can provide a custom TTL for the shared-lock. ./samples/shared-lock-custom-ttl.ts ``` import { sharedLockFactory } from "./shared-lock-factory-initial-config.js"; import { TimeSpan } from "eridu-tech/time-span"; const sharedLock = sharedLockFactory.create("shared-resource", { // Default TTL is 5min if not overrided // If you set it to null it means shared-lock will not expire and most be released manually. ttl: TimeSpan.fromSeconds(30), limit: 2, }); ``` ### Checking shared-lock state[​](#checking-shared-lock-state "Direct link to Checking shared-lock state") You can get the shared-lock state by using the `getState` method, it returns [`ISharedLockState`](https://eridu-tech.github.io/eridu-tech-core/types/SharedLock.ISharedLockState.html). ./samples/shared-lock-get-state.ts ``` import { sharedLockFactory } from "./shared-lock-factory-initial-config.js"; import { SHARED_LOCK_STATE } from "eridu-tech/shared-lock/contracts"; const sharedLock = sharedLockFactory.create("shared-resource", { limit: 2, }); const state = await sharedLock.getState(); if (state.type === SHARED_LOCK_STATE.EXPIRED) { console.log("The shared-lock doesnt exists"); } if (state.type === SHARED_LOCK_STATE.READER_LIMIT_REACHED) { console.log( "The shared-lock is in reader mode and limit have been reached and all slots are unavailable", ); } if (state.type === SHARED_LOCK_STATE.READER_ACQUIRED) { console.log("The shared-lock is in reader mode and is acquired"); } if (state.type === SHARED_LOCK_STATE.READER_UNACQUIRED) { console.log( "The shared-lock is in reader mode and there are avilable slots but the shared-lock is not acquired", ); } if (state.type === SHARED_LOCK_STATE.WRITER_UNAVAILABLE) { console.log( "The shared-lock is in writer mode and is acquired by different owner", ); } if (state.type === SHARED_LOCK_STATE.WRITER_ACQUIRED) { console.log("The shared-lock is in writer mode and is acquired"); } ``` ## Patterns[​](#patterns "Direct link to Patterns") ### Refreshing shared-locks[​](#refreshing-shared-locks "Direct link to Refreshing shared-locks") The shared-lock can be refreshed by the current owner before it expires. This is particularly useful for long-running tasks, instead of setting an excessively long TTL initially, you can start with a shorter one and use the `refreshWriter` or `refreshReader` method to set the TTL of the shared-lock: #### As reader[​](#as-reader "Direct link to As reader") ./samples/shared-lock-refresh-reader.ts ``` import { sharedLockFactory } from "./shared-lock-factory-initial-config.js"; import { delay } from "eridu-tech/utilities"; import { TimeSpan } from "eridu-tech/time-span"; const sharedLock = sharedLockFactory.create("resource", { limit: 2, ttl: TimeSpan.fromMinutes(1), }); async function doWork(): Promise { // ... critical section return true; } const hasAcquired = await sharedLock.acquireWriter(); if (hasAcquired) { try { while (true) { await sharedLock.refreshWriter(TimeSpan.fromMinutes(1)); const hasFinished = await doWork(); if (hasFinished) { break; } await delay(TimeSpan.fromSeconds(1)); } } finally { await sharedLock.releaseWriter(); } } ``` #### As writer[​](#as-writer "Direct link to As writer") ./samples/shared-lock-refresh-writer.ts ``` import { sharedLockFactory } from "./shared-lock-factory-initial-config.js"; import { delay } from "eridu-tech/utilities"; import { TimeSpan } from "eridu-tech/time-span"; const sharedLock = sharedLockFactory.create("resource", { limit: 2, ttl: TimeSpan.fromMinutes(1), }); async function doWork(): Promise { // ... critical section return true; } const hasAcquired = await sharedLock.acquireWriter(); if (hasAcquired) { try { while (true) { await sharedLock.refreshReader(TimeSpan.fromMinutes(1)); const hasFinished = await doWork(); if (hasFinished) { break; } await delay(TimeSpan.fromSeconds(1)); } } finally { await sharedLock.releaseReader(); } } ``` warning Note: A shared-lock must have an expiration (a `ttl` value) to be refreshed. You cannot refresh a shared-lock that was created without an expiration (with `ttl: null`) ./samples/shared-lock-non-refreshable.ts ``` import { sharedLockFactory } from "./shared-lock-factory-initial-config.js"; // Create a shared-lock with no expiration (non-refreshable) const sharedLock = sharedLockFactory.create("resource", { limit: 2, ttl: null, }); // A writer refresh attempt on this shared-ock will fail const hasRefreshedWriter = await sharedLock.refreshWriter(); // This will log 'false' because the sharedLock cannot be refreshed console.log(hasRefreshedWriter); // A reader refresh attempt on this shared-ock will fail const hasRefreshedReader = await sharedLock.refreshReader(); // This will log 'false' because the sharedLock cannot be refreshed console.log(hasRefreshedReader); ``` ### Additional writer methods[​](#additional-writer-methods "Direct link to Additional writer methods") The `releaseWriterOrFail` method is the same `releaseWriter` method but it throws an error when not enable to release the shared-lock as writer: ./samples/shared-lock-release-writer-or-fail.ts ``` import { sharedLockFactory } from "./shared-lock-factory-initial-config.js"; const sharedLock = sharedLockFactory.create("resource", { limit: 2, }); await sharedLock.releaseWriterOrFail(); ``` The `refreshWriterOrFail` method is the same `refreshWriter` method but it throws an error when not enable to refresh the shared-lock as writer: ./samples/shared-lock-refresh-writer-or-fail.ts ``` import { sharedLockFactory } from "./shared-lock-factory-initial-config.js"; const sharedLock = sharedLockFactory.create("resource", { limit: 2, }); await sharedLock.refreshWriterOrFail(); ``` The `runWriterOrFail` method automatically manages shared-lock acquisition and release as writer around function execution. It calls `acquireWriterOrFail` before invoking the function and calls `releaseWriter` in a finally block, ensuring the shared-lock is always freed, even if an error occurs during execution. ./samples/shared-lock-run-writer-or-fail.ts ``` import { sharedLockFactory } from "./shared-lock-factory-initial-config.js"; const sharedLock = sharedLockFactory.create("resource", { limit: 2, }); await sharedLock.runWriterOrFail(async () => { // ... critical section }); ``` info Note the method throws an error when the shared-lock cannot be acquired as writer. info You can provide synchronous Invocable or async/promisable invocable as values for the `runWriterOrFail` method. ### Additional reader methods[​](#additional-reader-methods "Direct link to Additional reader methods") The `releaseReaderOrFail` method is the same `releaseReader` method but it throws an error when not enable to release the shared-lock as reader: ./samples/shared-lock-release-reader-or-fail.ts ``` import { sharedLockFactory } from "./shared-lock-factory-initial-config.js"; const sharedLock = sharedLockFactory.create("resource", { limit: 2, }); await sharedLock.releaseReaderOrFail(); ``` The `refreshReaderOrFail` method is the same `refreshReader` method but it throws an error when not enable to refresh the shared-lock as reader: ./samples/shared-lock-refresh-reader-or-fail.ts ``` import { sharedLockFactory } from "./shared-lock-factory-initial-config.js"; const sharedLock = sharedLockFactory.create("resource", { limit: 2, }); await sharedLock.refreshReaderOrFail(); ``` The `runReaderOrFail` method automatically manages shared-lock acquisition and release as reader around function execution. It calls `acquireReaderOrFail` before invoking the function and calls `releaseReader` in a finally block, ensuring the shared-lock is always freed, even if an error occurs during execution. ./samples/shared-lock-run-reader-or-fail.ts ``` import { sharedLockFactory } from "./shared-lock-factory-initial-config.js"; const sharedLock = sharedLockFactory.create("resource", { limit: 2, }); await sharedLock.runReaderOrFail(async () => { // ... critical section }); ``` info Note the method throws an error when the shared-lock cannot be acquired as reader. info You can provide synchronous Invocable or async/promisable invocable as values for the `runReaderOrFail` method. ### Additional methods[​](#additional-methods "Direct link to Additional methods") The `forceRelease` method releases the shared-lock regardless it its in reader or writer mode: ./samples/shared-lock-force-release.ts ``` import { sharedLockFactory } from "./shared-lock-factory-initial-config.js"; const sharedLock = sharedLockFactory.create("resource", { limit: 2, }); await sharedLock.forceRelease(); ``` ### SharedLock instance variables[​](#sharedlock-instance-variables "Direct link to SharedLock instance variables") The `SharedLock` class exposes instance variables such as: ./samples/shared-lock-instance-variables.ts ``` import { sharedLockFactory } from "./shared-lock-factory-initial-config.js"; const sharedLock = sharedLockFactory.create("resource", { limit: 2, }); // Will return the key of the shared-lock which is "resource" console.log(sharedLock.key); // Will return the id of the shared-lock console.log(sharedLock.id); // Will return the ttl of the shared-lock console.log(sharedLock.ttl); ``` ### SharedLock id[​](#sharedlock-id "Direct link to SharedLock id") By default the shared-lock id is autogenerated but it can also manually defined. ./samples/shared-lock-custom-id.ts ``` import { sharedLockFactory } from "./shared-lock-factory-initial-config.js"; const sharedLock = sharedLockFactory.create("shared-lock", { limit: 2, lockId: "my-shared-lock-id", }); const hasAcquire = await sharedLock.acquireWriter(); if (hasAcquire) { console.log("Shared resource"); await sharedLock.releaseWriter(); } ``` info Manually defining shared-lock id is primarily useful for debugging or implementing manual resource controll by the end user. warning In most cases, setting a shared-lock id is unnecessary. ### Retrying acquiring shared-lock as writer by attempts[​](#retrying-acquiring-shared-lock-as-writer-by-attempts "Direct link to Retrying acquiring shared-lock as writer by attempts") To retry acquiring shared-lock as writer you can use the [`retry`](/docs/reliability/resilience/.md#retry) middleware. Retrying acquiring shared-lock as writer with `acquireWriterOrFail` method: ./samples/shared-lock-retry-writer-acquire-or-fail.ts ``` import { sharedLockFactory } from "./shared-lock-factory-initial-config.js"; import { retry } from "eridu-tech/resilience"; import { FailedAcquireWriterLockError } from "eridu-tech/shared-lock/contracts"; import { use } from "eridu-tech/middleware"; const sharedLock = sharedLockFactory.create("shared-lock", { limit: 2, }); try { await use(async () => { await sharedLock.acquireWriterOrFail(); }, [ retry({ maxAttempts: 4, errorPolicy: FailedAcquireWriterLockError, }), ])(); // The critical section } finally { await sharedLock.releaseWriter(); } ``` Retrying acquiring sharedLock as writer with `acquireWriter` method: ./samples/shared-lock-retry-writer-acquire.ts ``` import { sharedLockFactory } from "./shared-lock-factory-initial-config.js"; import { retry } from "eridu-tech/resilience"; import { use } from "eridu-tech/middleware"; const sharedLock = sharedLockFactory.create("shared-lock", { limit: 2, }); const hasAquired = await use(async () => { return await sharedLock.acquireWriter(); }, [ retry({ maxAttempts: 4, errorPolicy: { treatFalseAsError: true, }, }), ])(); if (hasAquired) { try { // The critical section } finally { await sharedLock.releaseWriter(); } } ``` Retrying acquiring shared-lock as writer with `runWriterOrFail` method: ./samples/shared-lock-retry-writer-run-or-fail.ts ``` import { sharedLockFactory } from "./shared-lock-factory-initial-config.js"; import { retry } from "eridu-tech/resilience"; import { FailedAcquireWriterLockError } from "eridu-tech/shared-lock/contracts"; import { use } from "eridu-tech/middleware"; const sharedLock = sharedLockFactory.create("shared-lock", { limit: 2, }); await use(async () => { await sharedLock.runWriterOrFail(async () => { // The critical section }); }, [ retry({ maxAttempts: 4, errorPolicy: FailedAcquireWriterLockError, }), ])(); ``` ### Retrying acquiring shared-lock as reader by attempts[​](#retrying-acquiring-shared-lock-as-reader-by-attempts "Direct link to Retrying acquiring shared-lock as reader by attempts") To retry acquiring shared-lock as reader you can use the [`retry`](/docs/reliability/resilience/.md#retry) middleware. Retrying acquiring shared-lock as reader with `acquireReaderOrFail` method: ./samples/shared-lock-retry-reader-acquire-or-fail.ts ``` import { sharedLockFactory } from "./shared-lock-factory-initial-config.js"; import { retry } from "eridu-tech/resilience"; import { LimitReachedReaderSemaphoreError } from "eridu-tech/shared-lock/contracts"; import { use } from "eridu-tech/middleware"; const sharedLock = sharedLockFactory.create("shared-lock", { limit: 2, }); try { await use(async () => { await sharedLock.acquireReaderOrFail(); }, [ retry({ maxAttempts: 4, errorPolicy: LimitReachedReaderSemaphoreError, }), ])(); // The critical section } finally { await sharedLock.releaseReader(); } ``` Retrying acquiring sharedLock as reader with `acquireReader` method: ./samples/shared-lock-retry-reader-acquire.ts ``` import { sharedLockFactory } from "./shared-lock-factory-initial-config.js"; import { retry } from "eridu-tech/resilience"; import { use } from "eridu-tech/middleware"; const sharedLock = sharedLockFactory.create("shared-lock", { limit: 2, }); const hasAquired = await use(async () => { return await sharedLock.acquireReader(); }, [ retry({ maxAttempts: 4, errorPolicy: { treatFalseAsError: true, }, }), ])(); if (hasAquired) { try { // The critical section } finally { await sharedLock.releaseReader(); } } ``` Retrying acquiring shared-lock as reader with `runReaderOrFail` method: ./samples/shared-lock-retry-reader-run-or-fail.ts ``` import { sharedLockFactory } from "./shared-lock-factory-initial-config.js"; import { retry } from "eridu-tech/resilience"; import { LimitReachedReaderSemaphoreError } from "eridu-tech/shared-lock/contracts"; import { use } from "eridu-tech/middleware"; const sharedLock = sharedLockFactory.create("shared-lock", { limit: 2, }); await use(async () => { await sharedLock.runReaderOrFail(async () => { // The critical section }); }, [ retry({ maxAttempts: 4, errorPolicy: LimitReachedReaderSemaphoreError, }), ])(); ``` ### Retrying acquiring shared-lock as writer by interval[​](#retrying-acquiring-shared-lock-as-writer-by-interval "Direct link to Retrying acquiring shared-lock as writer by interval") To retry acquiring shared-lockas as writer at regular intervals you can use the [`retryInterval`](/docs/reliability/resilience/.md#retry_by_interval) middleware. Retrying acquiring shared-lock with `acquireWriterOrFail` method: ./samples/shared-lock-retry-interval-writer-acquire-or-fail.ts ``` import { sharedLockFactory } from "./shared-lock-factory-initial-config.js"; import { retryInterval } from "eridu-tech/resilience"; import { FailedAcquireWriterLockError } from "eridu-tech/shared-lock/contracts"; import { use } from "eridu-tech/middleware"; import { TimeSpan } from "eridu-tech/time-span"; const sharedLock = sharedLockFactory.create("resource", { limit: 2, }); try { await use(async () => { await sharedLock.acquireWriterOrFail(); }, [ retryInterval({ // Time to wait 1 minute time: TimeSpan.fromMinutes(1), // Interval to try acquire the shared-lock interval: TimeSpan.fromSeconds(1), errorPolicy: FailedAcquireWriterLockError, }), ])(); // ... critical section } finally { await sharedLock.releaseWriter(); } ``` Retrying acquiring shared-lock with `acquireWriter` method: ./samples/shared-lock-retry-interval-writer-acquire.ts ``` import { sharedLockFactory } from "./shared-lock-factory-initial-config.js"; import { retryInterval } from "eridu-tech/resilience"; import { use } from "eridu-tech/middleware"; import { TimeSpan } from "eridu-tech/time-span"; const sharedLock = sharedLockFactory.create("resource", { limit: 2, }); const hasAcquired = await use(async () => { return await sharedLock.acquireWriter(); }, [ retryInterval({ time: TimeSpan.fromMinutes(1), interval: TimeSpan.fromSeconds(1), errorPolicy: { treatFalseAsError: true, }, }), ])(); if (hasAcquired) { try { // ... critical section } finally { await sharedLock.releaseWriter(); } } ``` Retrying acquiring shared-lock with `runWriterOrFail` method: ./samples/shared-lock-retry-interval-writer-run-or-fail.ts ``` import { sharedLockFactory } from "./shared-lock-factory-initial-config.js"; import { retryInterval } from "eridu-tech/resilience"; import { FailedAcquireWriterLockError } from "eridu-tech/shared-lock/contracts"; import { use } from "eridu-tech/middleware"; import { TimeSpan } from "eridu-tech/time-span"; const sharedLock = sharedLockFactory.create("resource", { limit: 2, }); await use(async () => { await sharedLock.runWriterOrFail(async () => { // ... critical section }); }, [ retryInterval({ time: TimeSpan.fromMinutes(1), interval: TimeSpan.fromSeconds(1), errorPolicy: FailedAcquireWriterLockError, }), ])(); ``` warning Note using `retryInterval` middleware with shared-lock acquiring in a HTTP request handler is discouraged because it blocks the HTTP request handler causing the handler wait until the shared-lock becomes available or the timeout is reached. This will delay HTTP request handler to generate response and will make frontend app slow because of HTTP request handler. ### Retrying acquiring shared-lock as reader by interval[​](#retrying-acquiring-shared-lock-as-reader-by-interval "Direct link to Retrying acquiring shared-lock as reader by interval") To retry acquiring shared-lockas as reader at regular intervals you can use the [`retryInterval`](/docs/reliability/resilience/.md#retry_by_interval) middleware. Retrying acquiring shared-lock with `acquireReaderOrFail` method: ./samples/shared-lock-retry-interval-reader-acquire-or-fail.ts ``` import { sharedLockFactory } from "./shared-lock-factory-initial-config.js"; import { retryInterval } from "eridu-tech/resilience"; import { LimitReachedReaderSemaphoreError } from "eridu-tech/shared-lock/contracts"; import { use } from "eridu-tech/middleware"; import { TimeSpan } from "eridu-tech/time-span"; const sharedLock = sharedLockFactory.create("resource", { limit: 2, }); try { await use(async () => { await sharedLock.acquireReaderOrFail(); }, [ retryInterval({ // Time to wait 1 minute time: TimeSpan.fromMinutes(1), // Interval to try acquire the shared-lock interval: TimeSpan.fromSeconds(1), errorPolicy: LimitReachedReaderSemaphoreError, }), ])(); // ... critical section } finally { await sharedLock.releaseReader(); } ``` Retrying acquiring shared-lock with `acquireReader` method: ./samples/shared-lock-retry-interval-reader-acquire.ts ``` import { sharedLockFactory } from "./shared-lock-factory-initial-config.js"; import { retryInterval } from "eridu-tech/resilience"; import { use } from "eridu-tech/middleware"; import { TimeSpan } from "eridu-tech/time-span"; const sharedLock = sharedLockFactory.create("resource", { limit: 2, }); const hasAcquired = await use(async () => { return await sharedLock.acquireReader(); }, [ retryInterval({ time: TimeSpan.fromMinutes(1), interval: TimeSpan.fromSeconds(1), errorPolicy: { treatFalseAsError: true, }, }), ])(); if (hasAcquired) { try { // ... critical section } finally { await sharedLock.releaseReader(); } } ``` Retrying acquiring shared-lock with `runReaderOrFail` method: ./samples/shared-lock-retry-interval-reader-run-or-fail.ts ``` import { sharedLockFactory } from "./shared-lock-factory-initial-config.js"; import { retryInterval } from "eridu-tech/resilience"; import { LimitReachedReaderSemaphoreError } from "eridu-tech/shared-lock/contracts"; import { use } from "eridu-tech/middleware"; import { TimeSpan } from "eridu-tech/time-span"; const sharedLock = sharedLockFactory.create("resource", { limit: 2, }); await use(async () => { await sharedLock.runReaderOrFail(async () => { // ... critical section }); }, [ retryInterval({ time: TimeSpan.fromMinutes(1), interval: TimeSpan.fromSeconds(1), errorPolicy: LimitReachedReaderSemaphoreError, }), ])(); ``` warning Note using `retryInterval` middleware with shared-lock acquiring in a HTTP request handler is discouraged because it blocks the HTTP request handler causing the handler wait until the shared-lock becomes available or the timeout is reached. This will delay HTTP request handler to generate response and will make frontend app slow because of HTTP request handler. ### Serialization and deserialization of shared-lock[​](#serialization-and-deserialization-of-shared-lock "Direct link to Serialization and deserialization of shared-lock") SharedLocks can be serialized, allowing them to be transmitted over the network to another server and later deserialized for reuse. This means you can, for example, acquire the shared-lock on the main server, transfer it to a queue worker server, and release it there. In order to serialize or deserialize a shared-lock you need pass an object that implements [`ISerderRegister`](/docs/foundation/serde/.md) contract like the [`Serde`](/docs/foundation/serde/.md) class to `SharedLockFactory`. Manually serializing and deserializing the shared-lock: ./samples/shared-lock-manual-serialization.ts ``` import { Redis } from "ioredis"; import { RedisSharedLockAdapter } from "eridu-tech/shared-lock/redis-shared-lock-adapter"; import { SharedLockFactory } from "eridu-tech/shared-lock"; import { SuperJsonSerde } from "eridu-tech/serde/super-json-serde"; const serde = new SuperJsonSerde(); const redisClient = new Redis("YOUR_REDIS_CONNECTION"); const sharedLockFactory = new SharedLockFactory({ // You can laso pass in an array of Serde class instances serde, adapter: new RedisSharedLockAdapter(redisClient), }); const sharedLock = sharedLockFactory.create("resource", { limit: 2, }); const serializedSharedLock = await serde.serialize(sharedLock); const deserializedSharedLock = await serde.deserialize(serializedSharedLock); ``` danger When serializing or deserializing a shared-lock, you must use the same `Serde` instances that were provided to the `SharedLockFactory`. This is required because the `SharedLockFactory` injects custom serialization logic for `ISharedLock` instance into `Serde` instances. info Note you only need manuall serialization and deserialization when integrating with external libraries. As long you pass the same `Serde` instances with all other components you dont need to serialize and deserialize the shared-lock manually. ./samples/shared-lock-event-bus-serialization.ts ``` import { Redis } from "ioredis"; import { RedisSharedLockAdapter } from "eridu-tech/shared-lock/redis-shared-lock-adapter"; import type { ISharedLock } from "eridu-tech/shared-lock/contracts"; import { SharedLockFactory } from "eridu-tech/shared-lock"; import { RedisPubSubEventBusAdapter } from "eridu-tech/event-bus/redis-pub-sub-event-bus-adapter"; import { EventBus } from "eridu-tech/event-bus"; import { SuperJsonSerde } from "eridu-tech/serde/super-json-serde"; const serde = new SuperJsonSerde(); const redis = new Redis("YOUR_REDIS_CONNECTION"); type EventMap = { "sending-shared-lock-over-network": { sharedLock: ISharedLock; }; }; const eventBus = new EventBus({ adapter: new RedisPubSubEventBusAdapter({ client: redis, serde, }), }); const sharedLockFactory = new SharedLockFactory({ serde, adapter: new RedisSharedLockAdapter(redis), }); const sharedLock = sharedLockFactory.create("resource", { limit: 2, }); // We are sending the shared-lock over the network to other servers. await eventBus.dispatch("sending-shared-lock-over-network", { sharedLock, }); // The other servers will recieve the serialized shared-lock and automattically deserialize it. await eventBus.addListener( "sending-shared-lock-over-network", ({ sharedLock }) => { // The shared-lock is deserialized and can be used console.log("SHARED_LOCK:", sharedLock); }, ); ``` ### Separating shared-lock creation from manipulation[​](#separating-shared-lock-creation-from-manipulation "Direct link to Separating shared-lock creation from manipulation") The library includes 4 additional contracts: * [`ISharedLock`](https://eridu-tech.github.io/eridu-tech-core/types/SharedLock.ISharedLock.html) - Allows only for manipulating of the shared-lock. * [`IWriterLock`](https://eridu-tech.github.io/eridu-tech-core/types/SharedLock.IWriterLock.html) - Allows only for manipulating of the shared-lock as writer. * [`IReaderSemaphore`](https://eridu-tech.github.io/eridu-tech-core/types/SharedLock.IReaderSemaphore.html) - Allows only for manipulating of the shared-lock as reader. * [`ISharedLockFactory`](https://eridu-tech.github.io/eridu-tech-core/types/SharedLock.ISharedLockFactory.html) - Allows only for creation of shared-locks. ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/shared-lock`](https://eridu-tech.github.io/eridu-tech-core/modules/SharedLock.html) API docs. --- # DI Integration The `sharedLockFactoryResolverDiFactory` function creates an [`ISharedLockFactoryResolver`](/docs/concurrency/shared_lock/shared_lock_factory_resolver/.md) whose underlying resolver is resolved from a [DI container](/docs/foundation/di/.md). The returned proxy implements both `ISharedLockFactoryResolver` and `ISharedLockFactory`: * `use(adapterName?)` selects an adapter on the resolved `SharedLockFactoryResolver`. * `create(key, settings)` creates a shared lock through the resolver's default adapter. The proxy is wired to the container during `container.init()`. Create it **before** calling `init()`; calling `use()` or `create()` before the container is initialized throws an error. ## Initial configuration[​](#initial-configuration "Direct link to Initial configuration") Set up the memory and Redis shared-lock adapters, create the container, then register the `SharedLockFactoryResolver` and wrap it with `sharedLockFactoryResolverDiFactory`: #### 1. Set up the memory shared-lock adapter[​](#1-set-up-the-memory-shared-lock-adapter "Direct link to 1. Set up the memory shared-lock adapter") ./samples/memory-shared-lock-adapter.ts ``` import { MemorySharedLockAdapter } from "eridu-tech/shared-lock/memory-shared-lock-adapter"; export const memorySharedLockAdapter = new MemorySharedLockAdapter(); ``` #### 2. Set up the Redis shared-lock adapter[​](#2-set-up-the-redis-shared-lock-adapter "Direct link to 2. Set up the Redis shared-lock adapter") ./samples/redis-shared-lock-adapter.ts ``` import { RedisSharedLockAdapter } from "eridu-tech/shared-lock/redis-shared-lock-adapter"; import { Redis } from "ioredis"; export const redisSharedLockAdapter = new RedisSharedLockAdapter( new Redis("YOUR_REDIS_CONNECTION_STRING"), ); ``` #### 3. Create the dependency injection container[​](#3-create-the-dependency-injection-container "Direct link to 3. Create the dependency injection container") ./samples/container.ts ``` import { Container } from "eridu-tech/di"; import { ExecutionContext } from "eridu-tech/execution-context"; import { AlsExecutionContextAdapter } from "eridu-tech/execution-context/als-execution-context-adapter"; export const executionContext = new ExecutionContext( new AlsExecutionContextAdapter(), ); export const container = new Container({ executionContext }); ``` #### 4. Register and initialize the shared-lock factory resolver[​](#4-register-and-initialize-the-shared-lock-factory-resolver "Direct link to 4. Register and initialize the shared-lock factory resolver") ./samples/shared-lock-factory-resolver-di-factory.ts ``` import { SharedLockFactoryResolver } from "eridu-tech/shared-lock"; import { ProxySharedLockFactoryResolver } from "eridu-tech/shared-lock/di"; import { container } from "./container.js"; import { memorySharedLockAdapter } from "./memory-shared-lock-adapter.js"; import { redisSharedLockAdapter } from "./redis-shared-lock-adapter.js"; type Adapters = "memory" | "redis"; const sharedLockFactoryResolver = new SharedLockFactoryResolver({ adapters: { memory: memorySharedLockAdapter, redis: redisSharedLockAdapter, }, defaultAdapter: "memory", }); await sharedLockFactoryResolver.init(); container.registerValue({ token: SharedLockFactoryResolver, value: sharedLockFactoryResolver, }); // Create the proxy before container.init() export const sharedLockFactory = new ProxySharedLockFactoryResolver({ container, resolverToken: SharedLockFactoryResolver, }); await container.init(); ``` ## Usage[​](#usage "Direct link to Usage") ### 1. Using the default adapter[​](#1-using-the-default-adapter "Direct link to 1. Using the default adapter") ./samples/shared-lock-factory-resolver-di-factory-default-adapter.ts ``` import { sharedLockFactory } from "./shared-lock-factory-resolver-di-factory.js"; // Uses the adapter configured as the default (memory) await sharedLockFactory .create("shared-resource", { limit: 4, }) .runWriterOrFail(async () => { // code to run }); ``` ### 2. Specifying an adapter explicitly[​](#2-specifying-an-adapter-explicitly "Direct link to 2. Specifying an adapter explicitly") ./samples/shared-lock-factory-resolver-di-factory-specific-adapter.ts ``` import { sharedLockFactory } from "./shared-lock-factory-resolver-di-factory.js"; // Uses the Redis adapter await sharedLockFactory .use("redis") .create("shared-resource", { limit: 4, }) .runWriterOrFail(async () => { // code to run }); ``` ## Using it with the middlewares[​](#using-it-with-the-middlewares "Direct link to Using it with the middlewares") Because the proxy implements `ISharedLockFactoryResolver`, it can be passed straight to the [shared-lock middleware factories](/docs/concurrency/shared_lock/shared_lock_middlewares/.md): ./samples/shared-lock-factory-resolver-di-factory-with-middleware.ts ``` import { use } from "eridu-tech/middleware"; import { SHARED_LOCK_WHEN, withSharedLockFactory, } from "eridu-tech/shared-lock/middlewares"; import { sharedLockFactory } from "./shared-lock-factory-resolver-di-factory.js"; const withSharedLock = withSharedLockFactory(sharedLockFactory); const readData = async (key: string): Promise => { // Safe to run concurrently with other readers return { data: "..." }; }; const safeRead = use( readData, withSharedLock.use("redis")({ key: ([resourceKey]) => `data:${resourceKey}`, when: SHARED_LOCK_WHEN.READER, limit: 10, }), ); await safeRead("config"); ``` ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/shared-lock`](https://eridu-tech.github.io/eridu-tech-core/modules/SharedLock.html) API docs. --- # Codec The `eridu-tech/codec` component provides seamless way to encode/decode data. ## Usage[​](#usage "Direct link to Usage") ./samples/usage.ts ``` import { Base64Codec } from "eridu-tech/codec/base-64-codec"; const codec = new Base64Codec(); const encodedStr = codec.encode("This is base-64 encoded"); const decodedStr = codec.decode(encodedStr); ``` ## Separating encoding and decoding[​](#separating-encoding-and-decoding "Direct link to Separating encoding and decoding") The library includes 4 additional contracts: * `IEncoder` - Allows only for encoding. * `IDecoder` - Allows only for decoding. * `ICodec` - Allows for both encoding and decoding. ## Existing Codec:s[​](#existing-codec "Direct link to existing-codec") Currently the library only included `Base64Codec` class. ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/codec`](https://eridu-tech.github.io/eridu-tech-core/modules/Codec.html) API docs. --- # Config accessor The `eridu-tech/config-accessor` component provides standardized type-safe access to domain configuration variables. It supports optional schema validation useful accessing dynamic configurations (e.g tenneat configurations). ## ConfigAccessor class[​](#configaccessor-class "Direct link to ConfigAccessor class") ### Initial configuration[​](#initial-configuration "Direct link to Initial configuration") ./samples/config-accessor-initial-config.ts ``` import { ConfigAccessor } from "eridu-tech/config-accessor"; import { z } from "zod"; const config = {}; const schema = z.object({ // Suppports primtive string, number, boolean values a: z.string(), // Suppports nested object with fields of string, number, boolean values b: z.object({ a: z.string(), }), // Suppports array with item of string, number, boolean values c: z.string().array(), // Suppports array of object with fields of string, number, boolean values d: z .object({ a: z.string(), }) .array(), }); export const accessor = new ConfigAccessor({ config, // Schema is optional, you can pass in a type schema, }); ``` ### Accessing configuration variables[​](#accessing-configuration-variables "Direct link to Accessing configuration variables") #### get[​](#get "Direct link to get") Will return null if path is missing: ./samples/get.ts ``` import { accessor } from "./config-accessor-initial-config.js"; // Return the value of field a accessor.get("a"); // Return the value of field b which is an object accessor.get("b"); // Return the value of field b.a which is an primtive accessor.get("b.a"); // Return the first item of field c wich an primtive accessor.get("c.1"); // Return the first item of field d which an object accessor.get("d.2"); ``` info Note you can only access fields up to 2 levels deep. #### getOr[​](#getor "Direct link to getOr") Will return default value if path is missing: ./samples/get-or.ts ``` import { accessor } from "./config-accessor-initial-config.js"; accessor.getOr("a", ""); ``` ## IConfigAccessor contract[​](#iconfigaccessor-contract "Direct link to IConfigAccessor contract") ./samples/contract.ts ``` import type { RestrictedPaths, PathValue, } from "eridu-tech/config-accessor/contracts"; import type { Get } from "type-fest"; import type { OneOrArray } from "eridu-tech/utilities"; type FieldConfigValue = string | number | boolean; type BaseConfig = Partial< Record< string, OneOrArray>> > >; type IConfigAccessor = { get>( path: TPath, ): PathValue; getOr>( path: TPath, defaultValue: NonNullable>, ): NonNullable>; }; ``` ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/config-accessor`](https://eridu-tech.github.io/eridu-tech-core/modules/ConfigAccessor.html) API docs. --- # DI Container usage The `eridu-tech/di` component provides an Inversion of Control (IoC) container for managing service registrations, dependency resolution, and object lifetimes. ### Initial Configuration[​](#initial-configuration "Direct link to Initial Configuration") To begin using the DI container, create a `Container` instance and provide an [`IExecutionContext`](/docs/foundation/execution_context/.md): ./samples/container.ts ``` import { Container } from "eridu-tech/di"; import { AlsExecutionContextAdapter } from "eridu-tech/execution-context/als-execution-context-adapter"; import { ExecutionContext } from "eridu-tech/execution-context"; const executionContext = new ExecutionContext(new AlsExecutionContextAdapter()); export const container = new Container({ executionContext, }); ``` ## DI Basics[​](#di-basics "Direct link to DI Basics") ### Overview[​](#overview "Direct link to Overview") The container follows a strict lifecycle. #### Register Services and Container Hooks[​](#register-services-and-container-hooks "Direct link to Register Services and Container Hooks") Register your services by defining their lifespans, dependencies, and service factories. Register hooks that will run after container initialization or de-initialization. All registrations must occur before initialization. The following methods are used to register services: [`registerFactory`](#register_factory), [`registerValue`](#register_value), [`registerDynamic`](#dynamic), [`registerAlias`](#register_alias). The following are used to register container hooks: [`onInit`](#container_hooks) and [`onDeInit`](#container_hooks). info Services and hooks can only be registered before the container is initialized. Once the container is initialized, registering new services or hooks will throw [`InvalidMethodCallDiError`](#invalid_method_call_di_error). #### Initialize and Activate the Container[​](#initialize-and-activate-the-container "Direct link to Initialize and Activate the Container") Call `init()` to prepare the container. The method `init()` executes all registered initialization hooks. info The current implementation of `IContainer` is ***eager***. The container will instantiate all services ahead of time rather than lazily upon first resolution. The current implementation also validates the dependency graph when `init()` is called. #### Use the Container[​](#use-the-container "Direct link to Use the Container") Resolve service instances and run scoped executions. The following methods are used to resolve services: [`resolve`](#resolve), [`resolveOr`](#resolve_or), [`resolveOrFail`](#resolve_or_fail). The following method is used to check if a service is resolvable: [`has`](#has). The following method is used to run scoped executions: [`run`](#scoped). info Services can only be resolved while the container is in an active state (after initialization and before de-initialization). Resolving services before initialization or after de-initialization will throw [`InvalidMethodCallDiError`](#invalid_method_call_di_error). #### De-Initialize the Container[​](#de-initialize-the-container "Direct link to De-Initialize the Container") Call `deInit()` to tear down the container. The method `deInit()` executes all registered de-initialization hooks. ### Tokens[​](#tokens "Direct link to Tokens") A **token** is the key that identifies a service in the container. It is used both to **register** a service and to **resolve** it later. A token can be either a ***class constructor*** or a ***generic token*** created via `genericToken()`. To create a token using `genericToken()`, pass a string describing the service and an optional phantom type parameter. The phantom type exists purely for static type checking. It holds no runtime value and is used by TypeScript to infer the correct service type upon resolution. #### Generic token[​](#generic-token "Direct link to Generic token") Example of a generic token created with the `genericToken` method: ./samples/generic-token.ts ``` import { genericToken } from "eridu-tech/di/contracts"; import type { IDatabase } from "./idatabase.js"; // token created with genericToken where // `"Database service"` is the description and `IDatabase` is the phantom type. export const IDATABASE = genericToken("Database service"); ``` The `Database` service interface: ./samples/idatabase.ts ``` export interface IDatabase { query(sql: string, params: Array): Promise; connect(): Promise; disconnect(): Promise; } ``` #### Class constructor token[​](#class-constructor-token "Direct link to Class constructor token") Example of a class constructor used as a token: ./samples/class-constructor-token.ts ``` import { Database } from "./database.js"; // Database's class constructor used as token. const DATABASE = Database; ``` The `Database` class: ./samples/database.ts ``` import type { IDatabase } from "./idatabase.js"; export class Database implements IDatabase { query(sql: string, params: Array): Promise { /* ... */ return Promise.resolve(); } async connect(): Promise { console.log("db connected"); } async disconnect(): Promise { console.log("db disconnected"); } } ``` ### Lifetime[​](#lifetime "Direct link to Lifetime") When registering a service, you also define its lifetime. There are four different service lifetimes: * **Singleton** — The container creates a single instance of the service for its entire lifetime and shares it across every resolve call and scope. * **Scoped** — The container creates one instance of the service per [`run()`](#scoped) scope and shares it whenever you resolve the service within that scope. For more details, see the [scoped execution](#scoped) section. * **Transient** — The container creates a new instance of the service every time you resolve the service and never shares it. * **Dynamic** — The service is declared but has no service factory registered with it. The service factory will be provided dynamically within a [`run()`](#scoped) scope before it can be resolved. For more details, see the [dynamic registration](#dynamic) section. ### Registration[​](#registration "Direct link to Registration") The container provides four registration methods: * **`registerFactory`** — Registers a service using a factory function that creates the instance. Use it to register **Singleton**, **Scoped**, or **Transient** services with full control over how the instance is constructed. * **`registerValue`** — Registers a pre-constructed value or constant. Values are always resolved as singletons. * **`registerDynamic`** — Registers a token whose value is not known at registration time and is provided later at runtime, per [`run()`](#scoped) scope. * **`registerAlias`** — Registers an additional token for an existing service, so both tokens resolve the same instance. #### `registerFactory`[​](#register_factory "Direct link to register_factory") Use `registerFactory()` to register a **Singleton**, **Scoped**, or **Transient** service using a service factory function. It takes the following arguments: * **`token`** — The key that identifies the service. * **`deps`** — The dependencies required by the service, defined as a record where each value is a **token** identifying a dependency. Pass an empty object literal `{}` if the service has no dependencies. * **`factory`** — [`invocable`](/docs/utilities/invocable/.md) (function or object with `invoke` method) that creates and returns the service instance. It receives a record of resolved dependencies as its first argument and the [`execution context`](/docs/foundation/execution_context/.md) as its second argument. The factory can also be `async` and return a `Promise`. * **`lifetime`** — The lifetime of the service. Must be either `LIFETIME.SINGLETON`, `LIFETIME.TRANSIENT` or `LIFETIME.SCOPED`. Here is a simple example of `registerFactory()` with no dependencies: ./samples/register-factory-no-dependencies.ts ``` import { LIFETIME } from "eridu-tech/di/contracts"; import { container } from "./container.js"; import { Database } from "./database.js"; import { IDATABASE } from "./generic-token.js"; // `IDATABASE` service requires no dependency container.registerFactory({ token: IDATABASE, deps: {}, // No dependencies factory: (deps) => new Database(), lifetime: LIFETIME.SINGLETON, }); ``` The `UserProvider` service used below depends on the `Database` service: ./samples/user-provider.ts ``` import type { IDatabase } from "./idatabase.js"; export interface User { firstName: string; lastName: string; email: string; id: string; } export class UserProvider { constructor(private database: IDatabase) { /* ... */ } getUser(id: string): User { /* ... */ return { email: "someone@email.com", firstName: "some", lastName: "one", id: "000001", }; } } ``` Here is a simple example of `registerFactory()` with one dependency: ./samples/register-factory-with-dependency.ts ``` import { LIFETIME } from "eridu-tech/di/contracts"; import { container } from "./container.js"; import { IDATABASE } from "./generic-token.js"; import { UserProvider } from "./user-provider.js"; // `UserProvider` service requires `IDATABASE` dependency container.registerFactory({ token: UserProvider, deps: { db: IDATABASE }, factory: (deps) => new UserProvider(deps.db), lifetime: LIFETIME.SINGLETON, }); ``` The `REQUEST_ID` token: ./samples/request-id.ts ``` import { genericToken } from "eridu-tech/di/contracts"; // A context token for the current request id export const REQUEST_ID = genericToken("RequestId"); ``` Here is an example of `registerFactory()` that reads a value from the `executionContext`: ./samples/register-factory-execution-context.ts ``` import { LIFETIME } from "eridu-tech/di/contracts"; import { container } from "./container.js"; import { REQUEST_ID } from "./request-id.js"; class RequestService { constructor(private requestId: string) { /* ... */ } } container.registerFactory({ token: RequestService, deps: {}, factory: (deps, executionContext) => { // Read a contextual value propagated through the resolution chain const requestId = executionContext.get(REQUEST_ID) ?? "unknown"; return new RequestService(requestId); }, lifetime: LIFETIME.TRANSIENT, }); ``` Here is an example of a service factory defined as an object with an `invoke` method. ./samples/service-factory-object-invoke.ts ``` import type { ServiceFactory } from "eridu-tech/di/contracts"; const serviceAsObject = { invoke() { return "hello"; }, } satisfies ServiceFactory; // functionally equivalent to serviceAsFunction const serviceAsFunction = (() => "hello") satisfies ServiceFactory; ``` #### `registerValue`[​](#register_value "Direct link to register_value") The `CONFIG` token: ./samples/app-config.ts ``` import { genericToken } from "eridu-tech/di/contracts"; export interface AppConfig { apiUrl: string; timeout: number; } export const CONFIG = genericToken("AppConfig"); ``` Use `registerValue()` to register values as singletons. ./samples/register-value.ts ``` import { container } from "./container.js"; import { CONFIG } from "./app-config.js"; container.registerValue({ token: CONFIG, value: { apiUrl: "https://api.example.com", timeout: 5000, }, }); ``` #### Grouping Registrations[​](#grouping_registrations "Direct link to Grouping Registrations") Group related registrations into a reusable function that receives an `IServiceRegister` and calls its registration methods. The container itself implements `IServiceRegister`, so it can be passed directly. The `Logger` services: ./samples/logger.ts ``` export interface ILogger { log(message: string): void; } export class Logger implements ILogger { log(message: string): void { /* ... */ } } export class ConsoleLogger implements ILogger { log(message: string): void { /* ... */ } } export class FileLogger implements ILogger { log(message: string): void { /* ... */ } } ``` ./samples/register-provider.ts ``` import { LIFETIME, type IServiceRegister } from "eridu-tech/di/contracts"; import { container } from "./container.js"; import { Database } from "./database.js"; import { FileLogger, Logger } from "./logger.js"; import { UserProvider } from "./user-provider.js"; // A plain function that groups related registrations function registerLogging(register: IServiceRegister): void { register.registerFactory({ token: Logger, factory: () => new Logger(), deps: {}, lifetime: LIFETIME.SINGLETON, }); register.registerFactory({ token: FileLogger, factory: () => new FileLogger(), deps: {}, lifetime: LIFETIME.SINGLETON, }); } function registerDatabase(register: IServiceRegister): void { register.registerFactory({ token: Database, factory: () => new Database(), deps: {}, lifetime: LIFETIME.SINGLETON, }); register.registerFactory({ token: UserProvider, factory: ({ db }) => new UserProvider(db), deps: { db: Database }, lifetime: LIFETIME.SCOPED, }); } // The container implements IServiceRegister, so pass it directly registerLogging(container); registerDatabase(container); ``` tip Grouping registrations is the recommended way to organize your registrations. Keep each group focused on a single concern. #### `registerAlias`[​](#register_alias "Direct link to register_alias") Use `registerAlias()` to expose an existing service under an additional token. Resolving the alias resolves the same instance as its target. * **`target`** — The token of the already-registered service to alias. * **`alias`** — The new token that resolves the same service as `target`. Aliases are registered as **Singleton** services that depend on their target, so the target must be a registered **Singleton** — a value registered with [`registerValue()`](#register_value) qualifies. ./samples/register-alias.ts ``` import { genericToken } from "eridu-tech/di/contracts"; import { container } from "./container.js"; import { IDATABASE } from "./generic-token.js"; import type { IDatabase } from "./idatabase.js"; // An alternative token for the same `IDatabase` service const DATABASE_ALIAS = genericToken( "Alias for the database service", ); container.registerAlias({ target: IDATABASE, alias: DATABASE_ALIAS, }); ``` #### Registering a Service as Dynamic[​](#registering-a-service-as-dynamic "Direct link to Registering a Service as Dynamic") Registering a service as dynamic is covered in its own section. For details on how to register and use dynamic services, see the [Dynamic Registration](#dynamic) section. ### Resolving a Service[​](#resolving-a-service "Direct link to Resolving a Service") There are three methods for resolving a service, and one method for checking whether a service can be resolved. info Before resolving any service, the container **must be initialized** by calling and awaiting `init()`. #### `resolve`[​](#resolve "Direct link to resolve") Returns the service if found, `null` otherwise: ./samples/resolve.ts ``` import { container } from "./container.js"; import { Logger } from "./logger.js"; container.registerValue({ token: Logger, value: new Logger(), }); await container.init(); const logger = await container.resolve(Logger); if (logger) { logger.log("Logger is available"); } ``` #### `resolveOr`[​](#resolve_or "Direct link to resolve_or") Returns the service if found, otherwise returns the provided default value: ./samples/resolve-or.ts ``` import { container } from "./container.js"; import { ConsoleLogger, Logger } from "./logger.js"; await container.init(); const logger = await container.resolveOr(Logger, new ConsoleLogger()); logger.log("Always has a logger"); ``` #### `resolveOrFail`[​](#resolve_or_fail "Direct link to resolve_or_fail") Returns the service if found, otherwise throws `CanNotResolveServiceDiError`: ./samples/resolve-or-fail.ts ``` import { CanNotResolveServiceDiError } from "eridu-tech/di/contracts"; import { container } from "./container.js"; import { Logger } from "./logger.js"; await container.init(); // Throws CanNotResolveServiceDiError if Logger is not registered try { await container.resolveOrFail(Logger); } catch (error) { if (error instanceof CanNotResolveServiceDiError) { console.error(error); } else { throw error; } } ``` #### `has`[​](#has "Direct link to has") Returns `true` if the token can be resolved, or `false` otherwise. ./samples/has.ts ``` import { container } from "./container.js"; import { Logger } from "./logger.js"; container.registerValue({ token: Logger, value: new Logger(), }); await container.init(); if (await container.has(Logger)) { console.log("Logger is resolvable"); } ``` info The method `has()` checks whether a service **can be resolved**, not whether it is registered. warning Calling `has()` may invoke service factories as a side effect. ### Scoped[​](#scoped "Direct link to Scoped") The `run()` method creates an isolated scope where scoped services are resolved once and then discarded. ./samples/scoped-execution.ts ``` import { LIFETIME } from "eridu-tech/di/contracts"; import { container } from "./container.js"; class A { // ... } // Register a scoped service container.registerFactory({ token: A, deps: {}, factory: () => new A(), lifetime: LIFETIME.SCOPED, }); await container.init(); await container.run({ scope: async () => { // Scoped services are resolved once within this scope const a1 = await container.resolveOrFail(A); const a2 = await container.resolveOrFail(A); console.log(a1 === a2); // true // A nested scope creates a new scoped registry, so it gets its own // instance of the scoped service await container.run({ scope: async () => { const nestedA = await container.resolveOrFail(A); console.log(nestedA === a1); // false }, }); }, }); // Outside the scope, scoped services are no longer available // A new scope would create new instances ``` info Before calling `run()`, the container **must be initialized** by calling and awaiting `init()`. ### Dynamic[​](#dynamic "Direct link to Dynamic") Use `registerDynamic()` when a token's value is not known at registration time and must be provided later at runtime — for example, values derived from an incoming request: ./samples/register-dynamic.ts ``` import { container } from "./container.js"; import { REQUEST_ID } from "./request-id.js"; // Register as dynamic — value will be provided later container.registerDynamic(REQUEST_ID); ``` Dynamic values are set at runtime using the `IDynamicServiceRegister` interface, inside a scoped [`run()`](#scoped) execution. ./samples/dynamic-value-set.ts ``` import { LIFETIME } from "eridu-tech/di/contracts"; import { container } from "./container.js"; import { REQUEST_ID } from "./request-id.js"; import { RequestHandler } from "./request-handler.js"; // Declare the token as dynamic — its value is only known per request container.registerDynamic(REQUEST_ID); // A scoped service can use the dynamic value as a dependency container.registerFactory({ token: RequestHandler, deps: { requestId: REQUEST_ID }, factory: ({ requestId }) => new RequestHandler(requestId), lifetime: LIFETIME.SCOPED, }); await container.init(); await container.run({ registration: async (register) => { // Set the dynamic value before the scope executes register.set({ token: REQUEST_ID, value: crypto.randomUUID(), }); }, scope: async () => { // The scoped service is injected with the per-request dynamic value const handler = await container.resolveOrFail(RequestHandler); await handler.handle(); }, }); ``` The `RequestHandler`: ./samples/request-handler.ts ``` export class RequestHandler { constructor(private requestId: string) { /* ... */ } async handle(): Promise { console.log(`Handling request: ${this.requestId}`); } } ``` `IDynamicServiceRegister` exposes `get()`, `getOrFail()` and `has()` to retrieve values from the execution context, alongside `set` which stores a value in it. For example, `CORRELATION_ID` is another dynamic token (registered with `registerDynamic()`) whose value may already be present in the execution context: ./samples/dynamic-value-callback.ts ``` import { LIFETIME } from "eridu-tech/di/contracts"; import { container } from "./container.js"; import { REQUEST_ID } from "./request-id.js"; import { RequestHandler } from "./request-handler.js"; // Declare the token as dynamic — its value is only known per request container.registerDynamic(REQUEST_ID); // A scoped service can use the dynamic value as a dependency container.registerFactory({ token: RequestHandler, deps: { requestId: REQUEST_ID }, factory: ({ requestId }) => new RequestHandler(requestId), lifetime: LIFETIME.SCOPED, }); await container.init(); await container.run({ registration: async (register) => { // Set the dynamic value before the scope executes register.set({ token: REQUEST_ID, value: crypto.randomUUID(), }); }, scope: async () => { // The scoped service is injected with the per-request dynamic value const handler = await container.resolveOrFail(RequestHandler); await handler.handle(); }, }); ``` `IDynamicServiceRegister` also provide following methods: `getOrFail()` throws `CanNotResolveServiceDiError` when no value is available, and `has()` lets you check for a value without reading it. The methods `get()`, `has()` and `getOrFail()` only consider a token as existing when it is **registered as dynamic** **and** has a value in the execution context. if the token is not registered as dynamic, or it is registered as dynamic but has no value in the execution context it will not considered as existing. warning `set()` writes the value **directly to the execution context**. If the token already has a value in the execution context, that value is **implicitly overwritten**. If the token does not exist in the execution context yet, the value is stored with the token as key. info Dynamic values are **saved to and retrieved from the execution context**. `set()` stores the value in the execution context, while `get()`, `has()` and `getOrFail` read it from there. ### Lifetime Relationship[​](#lifetime-relationship "Direct link to Lifetime Relationship") The container validates the lifetime relationships between a service and its declared dependencies, enforcing the rules described below. Any relationship not listed above throws `InvalidGraphDiError`. | Service lifetime | Can depend on service lifetimes | | ---------------- | ------------------------------------------- | | **Singleton** | **Singleton** | | **Scoped** | **Singleton**, **Scoped** or **Dynamic** | | **Transient** | **Singleton**, **Scoped** | | **Dynamic** | **None** — Dynamic can not depend on others | info The current implementation of `IContainer` will validate the dependency graph at `init()` or when a service is overridden with [`overrideFactory()`](#overriding_registrations). If any invalid relationship is found, `InvalidGraphDiError` will be thrown. info Only a **Scoped** service can depend on a **Dynamic** service. A **Transient** service cannot depend directly on a **Dynamic** service. A **Dynamic** service cannot depend on others, even on other **Dynamic** services. Example of a valid relationship — a transient service depending on a singleton service: ./samples/valid-relationship-transient-singleton.ts ``` import { LIFETIME } from "eridu-tech/di/contracts"; import { container } from "./container.js"; import { Database } from "./database.js"; import { UserProvider } from "./user-provider.js"; container.registerFactory({ token: Database, factory: () => new Database(), deps: {}, lifetime: LIFETIME.SINGLETON, }); // ✅ Service is registered as `LIFETIME.TRANSIENT` // and its `db` dependency is `LIFETIME.SINGLETON` container.registerFactory({ token: UserProvider, factory: ({ db }) => new UserProvider(db), deps: { db: Database }, lifetime: LIFETIME.TRANSIENT, }); await container.init(); // will not throw InvalidGraphDiError ``` The dependency chain used below: ./samples/dependency-chain.ts ``` // Dependency chain used to demonstrate lifetime relationships. // The chain is: C → B → A export class A {} export class B { constructor(private a: A) { /* ... */ } } export class C { constructor(private b: B) { /* ... */ } } ``` Example of an invalid relationship — a singleton service depending on a transient service: ./samples/invalid-relationship-singleton-transient.ts ``` import { InvalidGraphDiError, LIFETIME } from "eridu-tech/di/contracts"; import { container } from "./container.js"; import { A, B, C } from "./dependency-chain.js"; container.registerFactory({ token: A, factory: () => new A(), deps: {}, lifetime: LIFETIME.TRANSIENT, }); container.registerFactory({ token: B, factory: ({ a }) => new B(a), deps: { a: A }, lifetime: LIFETIME.TRANSIENT, }); // ❌ Service is registered as `LIFETIME.SINGLETON` // and its `transient` dependency is `LIFETIME.TRANSIENT` container.registerFactory({ token: C, factory: ({ b }) => new C(b), deps: { b: B }, lifetime: LIFETIME.SINGLETON, }); // will throw InvalidGraphDiError try { await container.init(); } catch (error) { if (error instanceof InvalidGraphDiError) { console.error(error); } else { throw error; } } ``` ### Container Hooks[​](#container_hooks "Direct link to Container Hooks") You can register multiple initialization hooks by calling `onInit()` multiple times, and multiple de-initialization hooks by calling `onDeInit()` multiple times. Initialization hooks run when `container.init()` is called, while de-initialization hooks run when `container.deInit()` is called. Both `onInit()` and `onDeInit()` accept an optional record that maps argument names to **tokens**. The container resolves those tokens before the listener runs and passes the resolved values to it. When no record is passed, the listener receives no arguments. info Hooks must be registered before `container.init()` is called. Calling `onInit()` or `onDeInit()` after `container.init()` throws [`InvalidMethodCallDiError`](#invalid_method_call_di_error). ./samples/container-hooks.ts ``` import { LIFETIME } from "eridu-tech/di/contracts"; import { container } from "./container.js"; import { Database } from "./database.js"; container.registerFactory({ token: Database, factory: () => new Database(), deps: {}, lifetime: LIFETIME.SINGLETON, }); container.onInit({ db: Database }, async ({ db }) => { // Runs when container.init() is called // The container resolves the tokens passed as deps before the hook runs await db.connect(); console.log("Container initialized"); }); container.onDeInit({ db: Database }, async ({ db }) => { // Runs when container.deInit() is called await db.disconnect(); console.log("Container deinitialized"); }); // Trigger the lifecycle await container.init(); // ... application runs ... await container.deInit(); ``` ### Overriding Registrations[​](#overriding_registrations "Direct link to Overriding Registrations") To override a registered service factory, use `overrideFactory()`; to override a registered singleton value, use `overrideValue()`. A service can only be overridden once. If the token is not registered, is registered as dynamic, or has already been overridden, a [`CanNotOverrideServiceDiError`](#can_not_override_service_di_error) is thrown. tip We recommend using overrides only during testing, not in production code. Overriding is useful for mocking services or swapping implementations. For example, replacing a real database with an in-memory adapter. info Overriding a registration is **forbidden after the container is initialized**. Calling `overrideFactory()` or `overrideValue()` after `container.init()` throws [`InvalidMethodCallDiError`](#invalid_method_call_di_error). ./samples/override-registrations.ts ``` import { LIFETIME } from "eridu-tech/di/contracts"; import { container } from "./container.js"; import { CONFIG } from "./app-config.js"; import { Database } from "./database.js"; import type { IDatabase } from "./idatabase.js"; import { IDATABASE } from "./generic-token.js"; container.registerFactory({ token: IDATABASE, factory: () => new Database(), deps: {}, lifetime: LIFETIME.SINGLETON, }); container.registerValue({ token: CONFIG, value: { apiUrl: "https://api.example.com", timeout: 5000 }, }); class MockDatabase implements IDatabase { query(sql: string, params: Array): Promise { /* ... */ return Promise.resolve(); } async connect(): Promise { console.log("connected"); } async disconnect(): Promise { console.log("disconnected"); } } // Override a registered factory service container.overrideFactory({ token: IDATABASE, factory: async (_deps, _executionContext) => { // Return a mock database for testing return new MockDatabase(); }, deps: {}, }); // Override a registered singleton value container.overrideValue({ token: CONFIG, value: { apiUrl: "http://localhost:9999", timeout: 100 }, }); ``` ### Forking a Container[​](#forking_a_container "Direct link to Forking a Container") The `fork()` method creates a child container that inherits all registrations and overrides from the parent at the moment of forking. After that, the two containers are fully isolated: registering or overriding services in the child does not affect the parent, and registering or overriding services in the parent does not affect the child. tip We recommend using forking only during testing. It is useful for testing different adapters by having one common base container and a fork for each adapter. info Forking is forbidden after the container is initialized. Calling `fork()` after `container.init()` throws [`InvalidMethodCallDiError`](#invalid_method_call_di_error). ./samples/fork-container.ts ``` import { container } from "./container.js"; import { CONFIG } from "./app-config.js"; container.registerValue({ token: CONFIG, value: { apiUrl: "https://api.example.com", timeout: 5000 }, }); const childContainer = container.fork(); // Override in the child container — parent is unaffected childContainer.overrideValue({ token: CONFIG, value: { apiUrl: "http://test.local", timeout: 100 }, }); // Both containers must be initialized before resolving await container.init(); await childContainer.init(); // Original container still has the original config const parentConfig = await container.resolveOrFail(CONFIG); const childConfig = await childContainer.resolveOrFail(CONFIG); console.log(parentConfig.apiUrl); // "https://api.example.com" console.log(childConfig.apiUrl); // "http://test.local" ``` ### Errors[​](#errors "Direct link to Errors") Most errors expose an error flag via the `flag` class field, along with detailed context via the `info` class field. | Error | Description | | -------------------------------------------------------------------- | ----------------------------------------------------------------------- | | [`CanNotResolveServiceDiError`](#can_not_resolve_service_di_error) | Thrown when a service cannot be resolved. | | [`InvalidGraphDiError`](#invalid_graph_di_error) | Thrown when the service graph is invalid. | | [`CanNotRegisterServiceDiError`](#can_not_register_service_di_error) | Thrown when a service cannot be registered. | | [`CanNotOverrideServiceDiError`](#can_not_override_service_di_error) | Thrown when a registration cannot be overridden. | | [`InvalidMethodCallDiError`](#invalid_method_call_di_error) | Thrown when a container method is called at an invalid time or context. | #### `CanNotRegisterServiceDiError`[​](#can_not_register_service_di_error "Direct link to can_not_register_service_di_error") Thrown when a service cannot be registered. It has the following flags: | Flag | Description | | ------------------------------------------------------------ | ------------------------------------------------------------------------------------ | | `ALREADY_REGISTERED` | Thrown when the token already has a registration. | | `DYNAMIC_SERVICE_PROVIDER_REGISTRATION_TOKEN_IS_NOT_DYNAMIC` | Thrown when the token provided to a dynamic service provider is not a dynamic token. | | `DYNAMIC_SERVICE_PROVIDER_REGISTRATION_TOKEN_DO_NOT_EXIST` | Thrown when the token provided to a dynamic service provider does not exist. | Here is an example where `CanNotRegisterServiceDiError` is thrown. ./samples/error-can-not-register-service.ts ``` import { CanNotRegisterServiceDiError } from "eridu-tech/di/contracts"; import { container } from "./container.js"; import { CONFIG } from "./app-config.js"; container.registerValue({ token: CONFIG, value: { apiUrl: "https://api.example.com", timeout: 5000 }, }); // Throws CanNotRegisterServiceDiError because CONFIG token is already registered try { container.registerValue({ token: CONFIG, value: { apiUrl: "https://another.example.com", timeout: 3000 }, }); } catch (error) { if (error instanceof CanNotRegisterServiceDiError) { console.error(error); } else { throw error; } } ``` #### `InvalidGraphDiError`[​](#invalid_graph_di_error "Direct link to invalid_graph_di_error") Thrown when the service graph is invalid. It has the following flags: | Flag | Description | | --------------------------- | ------------------------------------------------------------------------------- | | `INVALID_EDGE_RELATIONSHIP` | Thrown when a service depends on another service with an incompatible lifetime. | | `CYCLE_DEPENDENCY` | Thrown when there is a dependency cycle among services. | | `UNDECLARED_DEPENDENCIES` | Thrown when a declared dependency is not registered. | Here is an example where `InvalidGraphDiError` is thrown. ./samples/error-invalid-graph.ts ``` import { InvalidGraphDiError, LIFETIME } from "eridu-tech/di/contracts"; import { container } from "./container.js"; import { A, B, C } from "./dependency-chain.js"; container.registerFactory({ token: A, factory: () => new A(), deps: {}, lifetime: LIFETIME.TRANSIENT, }); container.registerFactory({ token: B, factory: ({ a }) => new B(a), deps: { a: A }, lifetime: LIFETIME.TRANSIENT, }); // ❌ A singleton (C) cannot depend on a transient (B) container.registerFactory({ token: C, factory: ({ b }) => new C(b), deps: { b: B }, lifetime: LIFETIME.SINGLETON, }); // Throws InvalidGraphDiError because a singleton depends on a transient service try { await container.init(); } catch (error) { if (error instanceof InvalidGraphDiError) { console.error(error); } else { throw error; } } ``` #### `CanNotResolveServiceDiError`[​](#can_not_resolve_service_di_error "Direct link to can_not_resolve_service_di_error") Thrown when a service cannot be resolved. It has the following flags: | Flag | Description | | ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | | `NOT_REGISTERED_TOKEN` | Thrown when the token is not registered. | | `SCOPED_SERVICE_OUTSIDE_RUN` | Thrown when a scoped service is resolved outside a [`run()`](#scoped) scope. | | `DYNAMIC_SERVICE_OUTSIDE_RUN` | Thrown when a dynamic service is resolved outside a [`run()`](#scoped) scope. | | `TRANSIENT_SERVICE_DEPEND_ON_SCOPED_WHO_CALLED_OUTSIDE_RUN` | Thrown when a transient service depends on a scoped service and is resolved outside a [`run()`](#scoped) scope. | | `RESOLVED_VALUE_IS_NULL` | Thrown when the resolved value is `null`. | | `NO_DYNAMIC_VALUE_SET_FOR_TOKENS` | Thrown when a dynamic token has no value set. | | `DYNAMIC_SERVICE_PROVIDER_NOT_DYNAMIC_TOKEN` | Thrown when the token provided to a dynamic service provider is not a dynamic token. | ./samples/error-can-not-resolve-service.ts ``` import { CanNotResolveServiceDiError } from "eridu-tech/di/contracts"; import { container } from "./container.js"; import { Logger } from "./logger.js"; await container.init(); // Throws CanNotResolveServiceDiError because Logger is not registered try { await container.resolveOrFail(Logger); } catch (error) { if (error instanceof CanNotResolveServiceDiError) { console.error(error); } else { throw error; } } ``` #### `CanNotOverrideServiceDiError`[​](#can_not_override_service_di_error "Direct link to can_not_override_service_di_error") Thrown when a registration cannot be overridden. It has the following flags: | Flag | Description | | ---------------------- | ------------------------------------------------------------------------ | | `TOKEN_NOT_REGISTERED` | Thrown when the token is not registered. | | `DYNAMIC_TOKEN` | Thrown when the token is registered as dynamic and cannot be overridden. | | `ALREADY_OVERRIDDEN` | Thrown when the service has already been overridden. | Here is an example where `CanNotOverrideServiceDiError` is thrown. ./samples/error-can-not-override-service.ts ``` import { CanNotOverrideServiceDiError } from "eridu-tech/di/contracts"; import { container } from "./container.js"; import { CONFIG } from "./app-config.js"; // Throws CanNotOverrideServiceDiError because CONFIG token is not registered // and hence cannot be overridden try { container.overrideValue({ token: CONFIG, value: { apiUrl: "http://localhost:9999", timeout: 100 }, }); } catch (error) { if (error instanceof CanNotOverrideServiceDiError) { console.error(error); } else { throw error; } } ``` #### `InvalidMethodCallDiError`[​](#invalid_method_call_di_error "Direct link to invalid_method_call_di_error") Thrown when a container method is called at an invalid time or context. It has the following flags: | Flag | Description | | ----------------------------- | -------------------------------------------------------------------------------------------- | | `NOT_ACTIVE` | Thrown when a method is called while the container is not active (not initialized). | | `ALREADY_INITIALIZED` | Thrown when a registration method is called after the container was initialized. | | `INSIDE_RUN` | Thrown when a method is called inside a [`run()`](#scoped) scope where it is not allowed. | | `INSIDE_DYNAMIC_REGISTRATION` | Thrown when a method is called inside the dynamic `registration` callback. | | `OUTSIDE_RUN` | Thrown when a method is called outside a [`run()`](#scoped) scope where a scope is required. | Here is an example where `InvalidMethodCallDiError` is thrown. ./samples/error-invalid-method-call.ts ``` import { InvalidMethodCallDiError } from "eridu-tech/di/contracts"; import { container } from "./container.js"; import { CONFIG } from "./app-config.js"; await container.init(); // Throws InvalidMethodCallDiError because registration is attempted after init() try { container.registerValue({ token: CONFIG, value: { apiUrl: "https://another.example.com", timeout: 3000 }, }); } catch (error) { if (error instanceof InvalidMethodCallDiError) { console.error(error); } else { throw error; } } ``` ## Patterns[​](#patterns "Direct link to Patterns") ### Separating Registration and Resolution Concerns[​](#separating-registration-and-resolution-concerns "Direct link to Separating Registration and Resolution Concerns") The container exposes several contracts that separate concerns for different use cases: * `IServiceRegister` — for **registering** services ([`registerFactory`](#register_factory), [`registerValue`](#register_value), [`registerDynamic`](#dynamic), [`registerAlias`](#register_alias)) and registering container lifecycle hooks. * `IServiceResolver` — for **resolving** services ([`resolve`](#resolve), [`resolveOr`](#resolve_or), [`resolveOrFail`](#resolve_or_fail), [`has`](#has)). * `IServiceOverrider` — for **overriding** existing registrations ([`overrideFactory`](#overriding_registrations), [`overrideValue`](#overriding_registrations)), useful for testing. * `IContainerScope` — for running scoped container executions ([`run`](#scoped)). * `IContainerFork` — for **forking** a child container ([`fork`](#forking_a_container)), useful for testing. * `IDynamicServiceRegister` — for setting dynamic values at runtime ([`set`](#dynamic)). #### `IServiceRegister`[​](#iserviceregister "Direct link to iserviceregister") * [`registerFactory(settings)`](#register_factory) * [`registerValue(settings)`](#register_value) * [`registerDynamic(token)`](#dynamic) * [`registerAlias(settings)`](#register_alias) * [`onInit(handler)`](#container_hooks) * [`onDeInit(handler)`](#container_hooks) #### `IServiceResolver`[​](#iserviceresolver "Direct link to iserviceresolver") * [`resolve(token)`](#resolve) * [`resolveOr(token, defaultValue)`](#resolve_or) * [`resolveOrFail(token)`](#resolve_or_fail) * [`has(token)`](#has) #### `IServiceOverrider`[​](#iserviceoverrider "Direct link to iserviceoverrider") * [`overrideFactory(settings)`](#overriding_registrations) * [`overrideValue(settings)`](#overriding_registrations) #### `IContainerScope`[​](#icontainerscope "Direct link to icontainerscope") * [`run(settings)`](#scoped) #### `IContainerFork`[​](#icontainerfork "Direct link to icontainerfork") * [`fork()`](#forking_a_container) ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/di`](https://eridu-tech.github.io/eridu-tech-core/modules/DI.html) API docs. --- # Env accessor The `eridu-tech/env-accessor` component provides easy type-safe access to enviroment variables. It supports multiple sources (sync/async), schema validation, and convenient access patterns. ## EnvAccessor class[​](#envaccessor-class "Direct link to EnvAccessor class") ### Initial configuration[​](#initial-configuration "Direct link to Initial configuration") ./samples/env-accessor-initial-config.ts ``` import { EnvAccessor } from "eridu-tech/env-accessor"; import { z } from "zod"; // Combine multiple sources as env variables // Note: The order matters—later sources override previous ones for overlapping keys. const sources = [ process.env, async () => { // Load secrets from a remote store (e.g. AWS Secrets Manager) and return // them as a plain object, mirroring what a secret-store SDK call would do. return { PORT: "3000", }; }, ]; // Define a schema for your environment variables const schema = z.object({ NODE_ENV: z.string().optional(), PORT: z.string().default("3000").pipe(z.coerce.number()), }); // Initialize the accessor export const accessor = new EnvAccessor({ schema, sources }); await accessor.init(); ``` ### Accessing enviroment variables[​](#accessing-enviroment-variables "Direct link to Accessing enviroment variables") #### get[​](#get "Direct link to get") Will return null if PORT enviroment field is missing: ./samples/get.ts ``` import { accessor } from "./env-accessor-initial-config.js"; accessor.get("PORT"); ``` #### getOr[​](#getor "Direct link to getOr") Will return default value if NODE\_ENV enviroment field is missing: ./samples/get-or.ts ``` import { accessor } from "./env-accessor-initial-config.js"; accessor.getOr("NODE_ENV", "DEV"); ``` ## IEnvAccessor contract[​](#ienvaccessor-contract "Direct link to IEnvAccessor contract") The `IEnvAccessor` contract defines the contract for environment variable access. It provides type-safe methods for retrieving environment variables. ./samples/contract.ts ``` import type { UndefinedToNull, Lazyable } from "eridu-tech/utilities"; type BaseEnvConfig = Partial>; type IEnvAccessor = { get( field: TField, ): UndefinedToNull; getOr( field: TField, defaultValue: Lazyable>, ): NonNullable; }; ``` ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/env-accessor`](https://eridu-tech.github.io/eridu-tech-core/modules/EnvAccessor.html) API docs. --- # ExecutionContext The `eridu-tech/execution-context` module provides a type-safe, composable, and environment-agnostic way to store and propagate contextual data (such as request IDs, user info, or tracing metadata) across async boundaries and function calls. `IExecutionContext` uses symbols internally for reliable context isolation. ## Initial configuration[​](#initial-configuration "Direct link to Initial configuration") To begin using the execution-context, you'll need to create and configure an instance: ./samples/execution-context-initial-config.ts ``` import { ExecutionContext } from "eridu-tech/execution-context"; import { AlsExecutionContextAdapter } from "eridu-tech/execution-context/als-execution-context-adapter"; // Create an execution-context instance with an adapter export const executionContext = new ExecutionContext( new AlsExecutionContextAdapter(), ); ``` ## ExecutionContext basics[​](#executioncontext-basics "Direct link to ExecutionContext basics") ### Running code with context[​](#running-code-with-context "Direct link to Running code with context") You can run code within a context boundary, and all context values will be accessible throughout the call chain: ./samples/run.ts ``` import { executionContext } from "./execution-context-initial-config.js"; import { contextToken } from "eridu-tech/execution-context/contracts"; // Define context tokens with type-safe identifiers type User = { id: string; name: string }; const userToken = contextToken("user"); const requestIdToken = contextToken("requestId"); function logData(): void { // Access context values later in the call chain // { id: "123", name: "Alice" } const user = executionContext.get(userToken); // "req-456" const reqId = executionContext.get(requestIdToken); console.log("user:", user); console.log("reqId:", reqId); } executionContext.run(() => { executionContext.put(userToken, { id: "123", name: "Alice" }); executionContext.put(requestIdToken, "req-456"); logData(); }); ``` ### Binding functions to context[​](#binding-functions-to-context "Direct link to Binding functions to context") You can bind a function to the current context, so it always executes with the captured context values: ./samples/bind.ts ``` import { executionContext } from "./execution-context-initial-config.js"; import { contextToken } from "eridu-tech/execution-context/contracts"; type User = { id: string; name: string }; const userToken = contextToken("user"); const requestIdToken = contextToken("requestId"); executionContext.run(() => { executionContext.put(userToken, { id: "123", name: "Alice" }); executionContext.put(requestIdToken, "req-456"); const logData = executionContext.bind((msg: string): void => { // Access context values later in the call chain const user = executionContext.get(userToken); // { id: "123", name: "Alice" } const reqId = executionContext.get(requestIdToken); // "req-456" console.log("message:", msg); console.log("user:", user); console.log("reqId:", reqId); }); logData("hello"); }); ``` ## Patterns[​](#patterns "Direct link to Patterns") ### Type safety with context tokens[​](#type-safety-with-context-tokens "Direct link to Type safety with context tokens") You can enforce compile-time type safety by defining context tokens with specific types: ./samples/type-safety.ts ``` import { executionContext } from "./execution-context-initial-config.js"; import { contextToken } from "eridu-tech/execution-context/contracts"; const userToken = contextToken<{ id: string; name: string }>("user"); executionContext.put(userToken, { id: "123", name: "Alice" }); // TypeScript will error if you try to put a value of the wrong type. ``` ### Immutable and chainable context operations[​](#immutable-and-chainable-context-operations "Direct link to Immutable and chainable context operations") All context mutation methods return the context instance, allowing for method chaining: ./samples/chainable.ts ``` import { executionContext } from "./execution-context-initial-config.js"; import { contextToken } from "eridu-tech/execution-context/contracts"; type User = { id: string; name: string }; const userToken = contextToken("user"); const requestIdToken = contextToken("requestId"); executionContext .put(userToken, { id: "123", name: "Alice" }) .put(requestIdToken, "req-456"); ``` ### Conditional context updates[​](#conditional-context-updates "Direct link to Conditional context updates") You can conditionally update the context: ./samples/conditional.ts ``` import { executionContext } from "./execution-context-initial-config.js"; import { contextToken } from "eridu-tech/execution-context/contracts"; const userToken = contextToken<{ id: string; name: string }>("user"); executionContext.when(true, (ctx) => ctx.put(userToken, { id: "conditional", name: "Bob" }), ); ``` ### Adapters[​](#adapters "Direct link to Adapters") * **`AlsExecutionContextAdapter`**: Uses Node.js AsyncLocalStorage for async context propagation. * **`NoOpExecutionContextAdapter`**: No-op adapter for testing or environments without async context support. ### Separating reading, updating, and execution concerns[​](#separating-reading-updating-and-execution-concerns "Direct link to Separating reading, updating, and execution concerns") The library includes several contracts that separate concerns for different use cases: * `IReadableContext`: Read-only access to context values (safe for consumers that should not mutate context). * `IContext`: Adds all mutation methods (put, update, remove, etc.) for full context management. * `IExecutionContextBase`: Adds execution boundary methods (run, bind) for context propagation and isolation. * `IExecutionContext`: Combines all of the above for complete context management and execution. #### `IExecutionContextBase`[​](#iexecutioncontextbase "Direct link to iexecutioncontextbase") * `run(invocable)` — Runs a function within the current execution-context. All context values are accessible during execution. * `bind(fn)` — Returns a new function that, when called, executes the original function within the captured context. #### `IContext`[​](#icontext "Direct link to icontext") * `add(token, value)` — Adds a value only if it doesn't already exist. No-op if the key exists. * `put(token, value)` — Sets or overwrites a value for the token. * `putIncrement(token, settings?)` — Initializes (if missing) and increments a numeric value. Optional max cap. * `putDecrement(token, settings?)` — Initializes (if missing) and decrements a numeric value. Optional min floor. * `putPush(token, ...values)` — Initializes (if missing) and pushes values to an array. * `update(token, value)` — Updates a value only if it exists. No-op if missing. * `updateIncrement(token, settings?)` — Increments a numeric value only if it exists. Optional max cap. * `updateDecrement(token, settings?)` — Decrements a numeric value only if it exists. Optional min floor. * `updatePush(token, ...values)` — Pushes values to an array only if it exists. No-op if missing. * `remove(token)` — Removes a value from the context. * `when(condition, ...invocables)` — Conditionally applies operations if the condition is true. #### `IReadableContext`[​](#ireadablecontext "Direct link to ireadablecontext") * `contains(token, matchValue)` — Checks if an array context value contains a specific item or matches a predicate. * `exists(token)` — Checks if a value exists for the token. * `missing(token)` — Checks if a value is missing for the token. * `get(token)` — Retrieves a value or null if not found. * `getOr(token, defaultValue)` — Retrieves a value or returns the provided default if not found. * `getOrFail(token)` — Retrieves a value or throws if not found. ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/execution-context`](https://eridu-tech.github.io/eridu-tech-core/modules/ExecutionContext.html) API docs. --- # Middleware The `eridu-tech/middleware` module provides a flexible middleware system for intercepting and composing function calls. It enables you to wrap functions with pre-processing and post-processing logic, similar to middleware patterns found in web frameworks like Express.js. ## Middleware basics[​](#middleware-basics "Direct link to Middleware basics") ### Creating a simple middleware[​](#creating-a-simple-middleware "Direct link to Creating a simple middleware") A middleware is a function that receives middleware arguments (containing the original arguments, a next function, and the name of the function) and returns the result: ./samples/simple-middleware.ts ``` import { type MiddlewareArgs, type MiddlewareFn, } from "eridu-tech/middleware/contracts"; const createLoggingMiddleware = , TReturn>( prefix: string = "LOG", ): MiddlewareFn => { return ({ args, next }: MiddlewareArgs) => { console.log(`${prefix} - Before invocation with args:`, args); const result = next(args); console.log(`${prefix} - After invocation, result:`, result); return result; }; }; const loggingMiddleware = createLoggingMiddleware(); ``` ### Applying middleware to a function[​](#applying-middleware-to-a-function "Direct link to Applying middleware to a function") Use the `use` function to apply one or more middlewares to a function: ./samples/use-function.ts ``` import { use } from "eridu-tech/middleware"; import { type MiddlewareArgs, type MiddlewareFn, } from "eridu-tech/middleware/contracts"; const originalFn = (name: string, age: number): string => { return `${name} is ${age} years old`; }; const createLoggingMiddleware = (): MiddlewareFn<[string, number], string> => { return ({ args, next }: MiddlewareArgs<[string, number], string>) => { console.log("Before invocation with args:", args); const result = next(args); console.log("After invocation, result:", result); return result; }; }; const loggingMiddleware = createLoggingMiddleware(); const wrappedFn = use(originalFn, loggingMiddleware); // Call the wrapped function const result = wrappedFn("Alice", 30); // Logs: "Before invocation with args: ["Alice", 30]" // Logs: "After invocation, result: Alice is 30 years old" ``` ### Applying multiple middlewares[​](#applying-multiple-middlewares "Direct link to Applying multiple middlewares") You can apply multiple middlewares, which are executed in order of their priority: ./samples/multiple-middlewares.ts ``` import { use } from "eridu-tech/middleware"; import { type MiddlewareArgs, type MiddlewareFn, } from "eridu-tech/middleware/contracts"; const originalFn = (name: string, age: number): string => { return `${name} is ${age} years old`; }; const createLoggingMiddleware = (): MiddlewareFn<[string, number], string> => { return ({ args, next }: MiddlewareArgs<[string, number], string>) => { console.log("Before invocation with args:", args); const result = next(args); console.log("After invocation, result:", result); return result; }; }; const loggingMiddleware = createLoggingMiddleware(); const createValidationMiddleware = (): MiddlewareFn< [string, number], string > => { return ({ args, next }: MiddlewareArgs<[string, number], string>) => { const [name, age] = args; if (age < 0) throw new Error("Age cannot be negative"); return next(args); }; }; const createAuthMiddleware = (): MiddlewareFn<[string, number], string> => { return ({ args, next }: MiddlewareArgs<[string, number], string>) => { console.log("Checking authorization..."); return next(args); }; }; const validationMiddleware = createValidationMiddleware(); const authMiddleware = createAuthMiddleware(); const wrappedFn = use(originalFn, [ loggingMiddleware, validationMiddleware, authMiddleware, ]); ``` ## Middleware types[​](#middleware-types "Direct link to Middleware types") ### MiddlewareFn[​](#middlewarefn "Direct link to MiddlewareFn") A function that receives middleware arguments and returns a result: ./samples/fn-type.ts ``` import type { MiddlewareArgs } from "eridu-tech/middleware/contracts"; type MiddlewareFn, TReturn> = ( args: MiddlewareArgs, ) => TReturn; ``` ### IMiddlewareObject[​](#imiddlewareobject "Direct link to IMiddlewareObject") A middleware object with an optional priority property: ./samples/object-type.ts ``` import { use } from "eridu-tech/middleware"; import { type IMiddlewareObject, type MiddlewareArgs, } from "eridu-tech/middleware/contracts"; const originalFn = (name: string, age: number): string => { return `${name} is ${age} years old`; }; class AuthMiddleware implements IMiddlewareObject<[string, number], string> { constructor(public readonly priority: number = 100) {} invoke({ args, next }: MiddlewareArgs<[string, number], string>): string { // Authentication logic return next(args); } } const authMiddleware = new AuthMiddleware(100); const wrappedFn = use(originalFn, authMiddleware); ``` ### MiddlewareArgs[​](#middlewareargs "Direct link to MiddlewareArgs") The argument passed to each middleware: ./samples/args-type.ts ``` import type { NextFn } from "eridu-tech/middleware/contracts"; type MiddlewareArgs, TReturn> = { // Original function arguments args: TParameters; // Function to invoke next middleware or original function next: NextFn; // Name of the function/method name: string; }; ``` ### defineMiddleware[​](#definemiddleware "Direct link to defineMiddleware") A helper function for defining middleware with accurate type inference. It ensures the provided handler conforms to the `MiddlewareFn` signature while preserving exact parameter and return types, without needing explicit generic annotations: ./samples/define-middleware.ts ``` import { defineMiddleware } from "eridu-tech/middleware/contracts"; import type { MiddlewareArgs } from "eridu-tech/middleware/contracts"; const loggingMiddleware = defineMiddleware( ({ args, next }: MiddlewareArgs): R => { console.log("Before:", args); const result = next(args); console.log("After:", result); return result; }, ); ``` ## Patterns[​](#patterns "Direct link to Patterns") ### Priority-based ordering[​](#priority-based-ordering "Direct link to Priority-based ordering") Set priority on middleware objects to control execution order (lower numbers execute first): ./samples/priority-ordering.ts ``` import { use } from "eridu-tech/middleware"; import { type IMiddlewareObject, type MiddlewareArgs, } from "eridu-tech/middleware/contracts"; const createPriorityMiddleware = ( name: string, priority: number, ): IMiddlewareObject<[string], string> => ({ priority, invoke: ({ args, next }: MiddlewareArgs<[string], string>): string => { console.log(`${priority}. ${name}`); return next(args); }, }); const authMiddleware = createPriorityMiddleware("Auth", 10); const validationMiddleware = createPriorityMiddleware("Validation", 20); const loggingMiddleware = createPriorityMiddleware("Logging", 30); const wrappedFn = use( (value: string): string => value.toUpperCase(), [loggingMiddleware, validationMiddleware, authMiddleware], ); // Executes in order: Auth -> Validation -> Logging -> Original function ``` ### Async middleware[​](#async-middleware "Direct link to Async middleware") Middleware can be asynchronous: ./samples/async-middleware.ts ``` import { use } from "eridu-tech/middleware"; import { type MiddlewareArgs, type MiddlewareFn, } from "eridu-tech/middleware/contracts"; // Async validator used by the middleware const validateAsync = async (args: [string, number]): Promise => { const [, age] = args; return age >= 0; }; const createAsyncValidationMiddleware = ( validator: (args: [string, number]) => Promise, ): MiddlewareFn<[string, number], Promise> => { return async ({ args, next, }: MiddlewareArgs<[string, number], Promise>): Promise => { // Perform async validation const isValid = await validator(args); if (!isValid) throw new Error("Validation failed"); return await next(args); }; }; // Async function being wrapped const originalFn = async (name: string, age: number): Promise => { return `${name} is ${age} years old`; }; const asyncValidationMiddleware = createAsyncValidationMiddleware(validateAsync); const wrappedFn = use(originalFn, asyncValidationMiddleware); ``` ### Short-circuiting middleware[​](#short-circuiting-middleware "Direct link to Short-circuiting middleware") Skip calling `next()` to bypass subsequent middleware and the original function: ./samples/caching-middleware.ts ``` import { type MiddlewareArgs, type MiddlewareFn, } from "eridu-tech/middleware/contracts"; const createCachingMiddleware = ( cacheStore: Map = new Map(), ): MiddlewareFn => { return ({ args, next }: MiddlewareArgs) => { const cacheKey: string = JSON.stringify(args); if (cacheStore.has(cacheKey)) { console.log("Cache hit!"); return cacheStore.get(cacheKey); // Skip next() } const result = next(args); cacheStore.set(cacheKey, result); return result; }; }; const cache = new Map(); const cachingMiddleware = createCachingMiddleware(cache); ``` ### Error handling middleware[​](#error-handling-middleware "Direct link to Error handling middleware") Catch and handle errors in middleware: ./samples/error-handling-middleware.ts ``` import { type MiddlewareArgs, type MiddlewareFn, } from "eridu-tech/middleware/contracts"; const createErrorHandlingMiddleware = ( errorHandler?: (error: unknown) => void, ): MiddlewareFn<[string, number], Promise> => { return async ({ args, next, }: MiddlewareArgs<[string, number], Promise>): Promise => { try { return await next(args); } catch (error) { const message = error instanceof Error ? error.message : String(error); console.error("Error occurred:", message); if (errorHandler) errorHandler(error); throw error; } }; }; const errorHandlingMiddleware = createErrorHandlingMiddleware((error) => console.log("Error handled gracefully"), ); ``` ## Enhancing Methods with `enhance`[​](#enhancing-methods-with-enhance "Direct link to enhancing-methods-with-enhance") The `enhance` function provides a convenient way to apply middleware to methods of class instances, enabling interception and augmentation of method calls without manually wrapping each function. ### Usage Example[​](#usage-example "Direct link to Usage Example") ./samples/enhance-greeter.ts ``` import { enhance } from "eridu-tech/middleware"; import type { MiddlewareFn } from "eridu-tech/middleware/contracts"; class Greeter { greet(name: string): string { return `Hello, ${name}!`; } } const greeter = new Greeter(); // Example middleware that logs calls export function loggingMiddleware< TParameters extends Array, TReturn, >(): MiddlewareFn { return ({ args, next }) => { console.log("Calling greet with:", args); const result = next(args); console.log("Result:", result); return result; }; } // Enhance the 'greet' method with middleware enhance(greeter, "greet", loggingMiddleware()); greeter.greet("Alice"); // Logs: // Calling greet with: ["Alice"] // Result: Hello, Alice! ``` ### Enhancing Object Literal Methods[​](#enhancing-object-literal-methods "Direct link to Enhancing Object Literal Methods") You can enhance methods on plain object literals as well: ./samples/enhance-object-literal.ts ``` import { enhance } from "eridu-tech/middleware"; import { loggingMiddleware } from "./enhance-greeter.js"; const obj = { add(a: number, b: number) { return a + b; }, }; enhance(obj, "add", loggingMiddleware()); obj.add(2, 3); // Logs: // Calling greet with: [2, 3] // Result: 5 ``` ### Enhancing Static Methods[​](#enhancing-static-methods "Direct link to Enhancing Static Methods") Static methods on classes can also be enhanced: ./samples/enhance-static.ts ``` import { enhance } from "eridu-tech/middleware"; import { loggingMiddleware } from "./enhance-greeter.js"; class MathUtils { static multiply(a: number, b: number) { return a * b; } } enhance(MathUtils, "multiply", loggingMiddleware()); MathUtils.multiply(4, 5); // Logs: // Calling greet with: [4, 5] // Result: 20 ``` ### Enhancing Class Prototype Methods[​](#enhancing-class-prototype-methods "Direct link to Enhancing Class Prototype Methods") You can enhance all instances of a class by enhancing its prototype: ./samples/enhance-prototype.ts ``` import { enhance } from "eridu-tech/middleware"; import { loggingMiddleware } from "./enhance-greeter.js"; class Person { say(message: string) { return `Person says: ${message}`; } } enhance(Person.prototype, "say", loggingMiddleware()); const alice = new Person(); alice.say("Hello!"); // Logs: // Calling greet with: ["Hello!"] // Result: Person says: Hello! ``` ### How it Works[​](#how-it-works "Direct link to How it Works") * The `enhance` function replaces the specified method on the object with a wrapped version that runs the provided middleware pipeline. * If the target property is not a function, a `TypeError` is thrown. * Multiple middlewares can be provided (as an array or single value). danger Because `enhance` mutates the object **in-place**, when one enhanced method internally calls another enhanced method via `this`, the internal call goes through the already-enhanced wrapper again, causing the middleware to apply **twice**. Be mindful of inter-method calls when using `enhance` on multiple methods of the same instance. This pattern is useful for adding cross-cutting concerns (logging, validation, authorization, etc.) to class methods in a reusable and declarative way. ## Applying Plugins with `withPlugin`[​](#plugin "Direct link to plugin") The `withPlugin` function provides a way to apply one or more plugins to a class instance or object literal, where each plugin can use the `enhance` function to wrap methods with middleware. This is useful for encapsulating cross-cutting concerns into reusable plugin units. ### Usage with Class Instances[​](#usage-with-class-instances "Direct link to Usage with Class Instances") ./samples/with-plugin-class.ts ``` import { withPlugin } from "eridu-tech/middleware"; import { type MiddlewareFn, type PluginFn, } from "eridu-tech/middleware/contracts"; export class UserService { async getUser(id: string): Promise<{ name: string }> { return { name: "Alice" }; } async deleteUser(id: string): Promise { // Deletion logic } } function withPerformanceLogging< TParameters extends Array, TReturn, >(): MiddlewareFn> { return async ({ args, next, name }) => { const start = performance.now(); const returnValue = await next(args); const end = performance.now(); const timeInMs = end - start; console.log(`function/method ${name} took ${timeInMs}ms`); return returnValue; }; } // Define a logging plugin export const loggingPlugin: PluginFn = (service, enhance) => { enhance(service, "getUser", withPerformanceLogging()); enhance(service, "deleteUser", withPerformanceLogging()); }; // Apply the plugin to a class instance const service = new UserService(); const enhancedService = withPlugin(service, loggingPlugin); await enhancedService.getUser("123"); // Logs: // getUser called with: ["123"] // getUser returned: { name: "Alice" } // The original service is NOT modified — a copy is returned instead ``` ### Usage with Object Literals[​](#usage-with-object-literals "Direct link to Usage with Object Literals") `withPlugin` also works with plain object literals: ./samples/with-plugin-object-literal.ts ``` import { withPlugin } from "eridu-tech/middleware"; import { type MiddlewareFn, type PluginFn, } from "eridu-tech/middleware/contracts"; // Timing middleware for synchronous methods function withPerformanceLogging< TParameters extends Array, TReturn, >(): MiddlewareFn { return ({ args, next, name }) => { const start = performance.now(); const returnValue = next(args); const timeInMs = performance.now() - start; console.log(`function/method ${name} took ${timeInMs}ms`); return returnValue; }; } const calculator = { add(a: number, b: number): number { return a + b; }, subtract(a: number, b: number): number { return a - b; }, }; const loggingPlugin: PluginFn = (obj, enhance) => { enhance(obj, "add", withPerformanceLogging()); enhance(obj, "subtract", withPerformanceLogging()); }; const enhancedCalc = withPlugin(calculator, loggingPlugin); enhancedCalc.add(2, 3); // Logs: add called with: [2, 3] // The original calculator object is NOT modified — a copy is returned instead ``` ### Applying Multiple Plugins[​](#applying-multiple-plugins "Direct link to Applying Multiple Plugins") You can apply multiple plugins at once by passing an array: ./samples/with-plugin-multiple.ts ``` import { withPlugin } from "eridu-tech/middleware"; import type { PluginFn } from "eridu-tech/middleware/contracts"; import { loggingPlugin, UserService } from "./with-plugin-class.js"; const monitoringPlugin: PluginFn = (service, enhance) => { // Monitor methods... }; const validationPlugin: PluginFn = (service, enhance) => { // Validate methods... }; const service = new UserService(); const enhancedService = withPlugin(service, [ loggingPlugin, monitoringPlugin, validationPlugin, ]); ``` ### Object-based Plugins[​](#object-based-plugins "Direct link to Object-based Plugins") For plugins with state or configuration, use the object form: ./samples/with-plugin-object-based.ts ``` import { withPlugin } from "eridu-tech/middleware"; import { type Enhance, type IPluginObject, type MiddlewareFn, } from "eridu-tech/middleware/contracts"; import { UserService } from "./with-plugin-class.js"; class MetricsClient { record(method: string, durationMs: number): void { console.log(`${method} took ${durationMs}ms`); } } const client = new MetricsClient(); class MetricsPlugin implements IPluginObject { constructor(private readonly metricsClient: MetricsClient) {} invoke(service: UserService, enhance: Enhance): void { const metricsMiddleware: MiddlewareFn< [string], Promise<{ name: string }> > = async ({ args, next }) => { const start = performance.now(); const result = await next(args); const duration = performance.now() - start; this.metricsClient.record("getUser", duration); return result; }; enhance(service, "getUser", metricsMiddleware); } } const service = new UserService(); const enhancedService = withPlugin(service, new MetricsPlugin(client)); ``` ### How it Works[​](#how-it-works-1 "Direct link to How it Works") * `withPlugin` **always** creates a copy of the target (whether a class instance or object literal), preserving the original unchanged. * Each plugin is invoked in order, receiving the copied target and the `enhance` function. * The `enhance` function wraps the specified method with a middleware pipeline in-place on the copy. * The enhanced copy is returned, leaving the original untouched. danger Because `withPlugin` uses `enhance` under the hood, the same edge case applies: if one enhanced method internally calls another enhanced method via `this`, the middleware will apply **twice**. Be mindful of inter-method calls when applying plugins that enhance multiple methods on the same instance. info This pattern is ideal for building reusable feature packs (logging, monitoring) that can be composed and applied to any class instance or object literal. ## Hooks[​](#hooks "Direct link to Hooks") The `withBeforeHook`, `withAfterHook` and `withOnError` middlewares run a callback at the three points of interest of a function call — before it runs, after it resolves and when it throws — so that common logic does not have to be written as a full middleware. Each one accepts a callback (a function or an invocable object) and an optional `detach` flag that fires the callback without awaiting it. ### withBeforeHook[​](#withbeforehook "Direct link to withBeforeHook") Runs a `BeforeHook` before the wrapped function and lets it replace the arguments by returning a new argument tuple, while returning nothing keeps the original arguments. The hook may be async and is awaited, unless `detach` is set to `true`, in which case its return value is ignored. The synchronous counterpart is `withBeforeHookSync`, which takes a hook that must be synchronous, has no `detach` option and always applies the tuple it returns. ./samples/with-before-hook.ts ``` import { use, withBeforeHook } from "eridu-tech/middleware"; interface User { id: string; name: string; } const saveUser = async (name: string): Promise => { return { id: "1", name }; }; const auditName = async (name: string): Promise => { console.log(`Saving user with name: ${name}`); }; // The hook may be async and can replace the arguments of the wrapped function. const createUser = use( saveUser, withBeforeHook<[name: string], User>(async ([name]) => { await auditName(name); return [name.trim()]; }), ); // With `detach` set to `true` the hook is fired without being awaited and its // return value is ignored, so the wrapped function keeps the original arguments. const createUserDetached = use( saveUser, withBeforeHook<[name: string], User>(async ([name]) => { await auditName(name); }, true), ); ``` ### withAfterHook[​](#withafterhook "Direct link to withAfterHook") Runs an `AfterHook` after the wrapped function resolves successfully, receiving the arguments and the result, and can replace the result by returning a value, while returning nothing keeps the original result. The hook may be async and is awaited, unless `detach` is set to `true`, in which case it is fired without being awaited and cannot change the result. The synchronous counterpart is `withAfterHookSync`, which takes a hook that must be synchronous, has no `detach` option and can still replace the result. ./samples/with-after-hook.ts ``` import { use, withAfterHook } from "eridu-tech/middleware"; interface User { id: string; name: string; } const saveUser = async (name: string): Promise => { return { id: "1", name }; }; const trackUser = async (user: User): Promise => { console.log(`Tracking user: ${user.id}`); }; // The hook may be async and can replace the result by returning a value. const createUser = use( saveUser, withAfterHook<[name: string], User>(async ([name], user) => { await trackUser(user); return { ...user, name: name.trim() }; }), ); // Returning nothing keeps the original result, which pairs with `detach` for // fire-and-forget side effects that should not delay the caller. const createUserWithSideEffect = use( saveUser, withAfterHook<[name: string], User>((_args, user) => { console.log(`Saved user: ${user.id}`); }, true), ); ``` ### withOnError[​](#withonerror "Direct link to withOnError") Runs an `OnErrorHook` when the wrapped function throws, receiving the arguments and the error for side effects only, because the error is always re-thrown unchanged. The hook may be async and is awaited before the error propagates, unless `detach` is set to `true`. The synchronous counterpart is `withOnErrorSync`, which takes a hook that must be synchronous, has no `detach` option and is always invoked before the error is re-thrown. ./samples/with-on-error-hook.ts ``` import { use, withOnError } from "eridu-tech/middleware"; interface User { id: string; name: string; } const reportFailure = async (name: string, error: unknown): Promise => { console.error(`Failed to save user with name: ${name}`, error); }; const saveUser = async (name: string): Promise => { return { id: "1", name }; }; // The hook may be async and is awaited before the error is re-thrown. const createUser = use( saveUser, withOnError<[name: string], User>(async ([name], error) => { await reportFailure(name, error); }), ); // With `detach` set to `true` the hook is fired without being awaited. const createUserDetached = use( saveUser, withOnError<[name: string], User>(([name], error) => { console.error(`Failed to save user with name: ${name}`, error); }, true), ); ``` ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/middleware`](https://eridu-tech.github.io/eridu-tech-core/modules/Middleware.html) API docs. --- # Foundation `eridu-tech` is a modular, adapter-first TypeScript library for building production-ready backend applications. Every component ships with in-memory adapters for fast, deterministic testing — and pluggable adapters to swap in real infrastructure without touching your business logic. The building blocks that power the rest of the ecosystem. * [Execution Context](/docs/foundation/execution_context/.md) — Type-safe, composable context propagation for request IDs, user info, and tracing metadata across async boundaries, without manual context passing. * [Transaction Context](/docs/foundation/transaction_context/transaction_context_usage/.md) — Coordinate database transactions across components with support for the after-commit pattern, joining existing transactions, and pluggable adapters (Kysely, MongoDB). * [Middleware and AOP](/docs/foundation/middleware/.md) — Composable middleware pipeline with before/after hooks and error handling, the foundation for every component's plugin system. * [Serde](/docs/foundation/serde/.md) — Serialize and deserialize data with a built-in SuperJSON adapter and custom serializers, the backbone for all data interchange across the ecosystem. * [Codec](/docs/foundation/codec/.md) — Encode and decode data with a unified, type-safe interface that includes a built-in Base64 codec and lets you build custom codecs for any protocol. * [Typed Config Access](/docs/foundation/config_accessor/.md) — Standardized type-safe access to domain configuration variables, with optional schema validation and full TypeScript inference. * [Typed Env Access](/docs/foundation/env_accessor/.md) — Type-safe environment variable access from multiple sync/async sources with parsing, defaults, and validation. * [DI Container](/docs/foundation/di/.md) — A lightweight, type-safe dependency injection container for wiring application components without tight coupling. --- # Serde The `eridu-tech/serde` component provides seamless way to serialize/deserialize data and adding custom serialization/deserialization logic for custom data types. ## Initial configuration[​](#initial-configuration "Direct link to Initial configuration") ./samples/serde-initial-config.ts ``` import { SuperJsonSerde } from "eridu-tech/serde/super-json-serde"; export const serde = new SuperJsonSerde(); ``` ## Serde basics[​](#serde-basics "Direct link to Serde basics") ### Serializing and deserializing values[​](#serializing-and-deserializing-values "Direct link to Serializing and deserializing values") Here is an example of serializing and deserializing a value. ./samples/serialize.ts ``` import { serde } from "./serde-initial-config.js"; const serializedValue = await serde.serialize({ name: "abra", age: 20, }); const deserializedValue = await serde.deserialize(serializedValue); ``` ### Custom serialization and deserialization logic[​](#custom-serialization-and-deserialization-logic "Direct link to Custom serialization and deserialization logic") The `registerCustom` method offers control over serialization and deserialization behavior. ./samples/register-custom.ts ``` import type { ISerdeTransformer } from "eridu-tech/serde/contracts"; import { serde } from "./serde-initial-config.js"; type ISerializedUser = { version: "1"; name: string; age: number; }; class User { static readonly serdeTransformer: ISerdeTransformer = { name: "User", isApplicable(value: unknown): value is User { return value instanceof User; }, deserialize(serializedValue: ISerializedUser): User { return new User(serializedValue.name, serializedValue.age); }, serialize(deserializedValue: User): ISerializedUser { return { version: "1", name: deserializedValue.name, age: deserializedValue.age, }; }, }; constructor( readonly name: string, readonly age: number, ) {} } serde.registerCustom(User.serdeTransformer); ``` info Note the `ISerdeTranformer` object can be dynamically created. ## Patterns[​](#patterns "Direct link to Patterns") ### Usage with other components[​](#usage-with-other-components "Direct link to Usage with other components") When using `Serde` class instance there is no need to call `serialize` and `deserialize` manually. Because components like `Cache` handle it automatically through their adapter. ./samples/with-cache.ts ``` import { SuperJsonSerde } from "eridu-tech/serde/super-json-serde"; import { RedisCacheAdapter } from "eridu-tech/cache/redis-cache-adapter"; import { Cache } from "eridu-tech/cache"; import { ListCollection } from "eridu-tech/collection"; import type { ICollection } from "eridu-tech/collection/contracts"; import { Redis } from "ioredis"; const serde = new SuperJsonSerde(); serde.registerCustom(ListCollection.serdeTransformer); const cache = new Cache>({ adapter: new RedisCacheAdapter({ database: new Redis("YOUR_REDIS_CONNECTION_STRING"), serde, }), }); const listCollection = new ListCollection(["a", "b", "c", "d", "e"]); await cache.add("list", listCollection); const deserializedListCollection = await cache.get("list"); if (deserializedListCollection) { // Logs "c" console.log(deserializedListCollection.getOrFail(2)); } ``` info Note you should use one `Serde` class instance accross all components and register all serializable objects before component usage. ## Separating serialization, deserialization and registering custom serialization/deserialization logic[​](#separating-serialization-deserialization-and-registering-custom-serializationdeserialization-logic "Direct link to Separating serialization, deserialization and registering custom serialization/deserialization logic") The library includes 4 additional contracts: * `ISerializer` - Allows only for serialization. * `IDeserializer` - Allows only for deserialization. * `ISerde` - Allows for both serialization and deserialization. * `ISerderRegister` - Allows only for registering custom serialization/deserialization logic. * `IFlexibleSerde` – Allows for both serialization, deserialization and for registering custom serialization/deserialization and deserialization logic. This separation makes it easy to visually distinguish the 4 contracts, making it immediately obvious that they serve different purposes. ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/serde`](https://eridu-tech.github.io/eridu-tech-core/modules/Serde.html) API docs. --- # Configuring TransactionContext adapters ## KyselyTransactionAdapter[​](#kysely_transaction_adapter "Direct link to KyselyTransactionAdapter") To use the `KyselyTransactionAdapter`, you'll need to install the required dependency: [`kysely`](https://www.npmjs.com/package/kysely) package. ### Usage with Sqlite[​](#usage-with-sqlite "Direct link to Usage with Sqlite") You will need to install [`better-sqlite3`](https://www.npmjs.com/package/better-sqlite3) package: ./samples/kysely-transaction-sqlite.ts ``` import { KyselyTransactionAdapter } from "eridu-tech/transaction-context/kysely-transaction-adapter"; import Sqlite from "better-sqlite3"; import { Kysely, SqliteDialect } from "kysely"; const database = new Kysely({ dialect: new SqliteDialect({ database: new Sqlite(":memory:"), }), }); export const kyselyTransactionAdapter = new KyselyTransactionAdapter({ database, }); ``` ### Usage with Postgres[​](#usage-with-postgres "Direct link to Usage with Postgres") You will need to install [`pg`](https://www.npmjs.com/package/pg) package: ./samples/kysely-transaction-postgres.ts ``` import { KyselyTransactionAdapter } from "eridu-tech/transaction-context/kysely-transaction-adapter"; import { Kysely, PostgresDialect } from "kysely"; import { Pool } from "pg"; export const database = new Kysely({ dialect: new PostgresDialect({ pool: new Pool({ database: "DATABASE_NAME", host: "DATABASE_HOST", user: "DATABASE_USER", // DATABASE port port: 5432, password: "DATABASE_PASSWORD", max: 10, }), }), }); export const kyselyTransactionAdapter = new KyselyTransactionAdapter({ database, }); ``` ### Usage with other databases[​](#usage-with-other-databases "Direct link to Usage with other databases") Note [`kysely`](https://www.npmjs.com/package/kysely) has support for multiple [databases](https://github.com/kysely-org/awesome-kysely?tab=readme-ov-file#dialects). danger Before choosing a database, ensure it supports transactions. Without transaction support, starting a transaction fails and a `StartTransactionError` is thrown. ### Settings[​](#settings "Direct link to Settings") ./samples/kysely-transaction-adapter-settings.ts ``` import { KyselyTransactionAdapter } from "eridu-tech/transaction-context/kysely-transaction-adapter"; import { database } from "./kysely-transaction-postgres.js"; export const kyselyTransactionAdapter = new KyselyTransactionAdapter({ database, // Applied to every new transaction // This is the default value accessMode: "read write", // Applied to every new transaction // This is the default value isolationLevel: "serializable", }); ``` info Kysely's SQLite driver ignores the configured access mode and isolation level, because SQLite does not support them. Other dialects apply both settings to every new transaction. ### Usage with TransactionContext[​](#usage-with-transactioncontext "Direct link to Usage with TransactionContext") Pass the adapter to the `TransactionContext` class, then read `current` to get the client of the current scope. Inside `run()` it is the transaction-scoped client, outside of it the base client: ./samples/kysely-transaction-context.ts ``` import { ExecutionContext } from "eridu-tech/execution-context"; import { AlsExecutionContextAdapter } from "eridu-tech/execution-context/als-execution-context-adapter"; import { contextToken } from "eridu-tech/execution-context/contracts"; import { TransactionContext } from "eridu-tech/transaction-context"; import { kyselyTransactionAdapter } from "./kysely-transaction-sqlite.js"; import type { Kysely } from "kysely"; const executionContext = new ExecutionContext(new AlsExecutionContextAdapter()); export const transactionContext = new TransactionContext>({ token: contextToken("kysely-transaction"), adapter: kyselyTransactionAdapter, executionContext, }); async function createUser(userId: string): Promise { // `current` is the transaction-scoped client when a transaction is active, // otherwise the base client await transactionContext.current .insertInto("users") .values({ id: userId }) .execute(); } // No transaction is active, so `current` is the base client await createUser("1"); await transactionContext.run(async () => { // A transaction is active, so the same call runs inside it await createUser("2"); }); ``` ## MongodbTransactionAdapter[​](#mongodb_transaction_adapter "Direct link to MongodbTransactionAdapter") To use the `MongodbTransactionAdapter`, you'll need to install the required dependency: [`mongodb`](https://www.npmjs.com/package/mongodb) package. The `client` setting is the `MongoClient` used to start sessions and transactions, while `database` is the `Db` instance exposed as the base (non-transactional) client: ./samples/mongodb-transaction-adapter.ts ``` import { MongodbTransactionAdapter } from "eridu-tech/transaction-context/mongodb-transaction-adapter"; import { MongoClient } from "mongodb"; // The client used to start sessions and transactions const mongoClient = new MongoClient("mongodb://localhost:27017"); // The database exposed as the base (non-transactional) client const database = mongoClient.db("DATABASE_NAME"); export const mongodbTransactionAdapter = new MongodbTransactionAdapter({ client: mongoClient, database, }); ``` danger MongoDB only supports transactions on replica sets and sharded clusters. Standalone deployments will fail as soon as an operation runs inside a transaction. ### Settings[​](#settings-1 "Direct link to Settings") ./samples/mongodb-transaction-adapter-settings.ts ``` import { TimeSpan } from "eridu-tech/time-span"; import { MongodbTransactionAdapter } from "eridu-tech/transaction-context/mongodb-transaction-adapter"; import { MongoClient } from "mongodb"; const mongoClient = new MongoClient("mongodb://localhost:27017"); export const mongodbTransactionAdapter = new MongodbTransactionAdapter({ client: mongoClient, database: mongoClient.db("DATABASE_NAME"), // Applied when committing a transaction commitTimeout: TimeSpan.fromSeconds(10), // Applied when aborting a transaction abortTimeout: TimeSpan.fromSeconds(10), // Passed to `startTransaction` for each new transaction startTransactionSettings: { readConcern: { level: "snapshot" }, readPreference: "primary", writeConcern: { w: "majority" }, }, // Passed to `startSession` when a new session is created startSessionSettings: { causalConsistency: true, }, // Passed to `endSession` after a transaction is committed or aborted endSessionSettings: {}, }); ``` info Every transaction commits or aborts on its own session, and the session is always ended afterwards — even when committing or aborting fails. ### Usage with TransactionContext[​](#usage-with-transactioncontext-1 "Direct link to Usage with TransactionContext") MongoDB scopes a transaction to a `ClientSession`, so read `transaction` to get the session of the active transaction and pass it to the operations that must join it. `client` always stays the base database, which must not be used for transactional work: ./samples/mongodb-transaction-context.ts ``` import { ExecutionContext } from "eridu-tech/execution-context"; import { AlsExecutionContextAdapter } from "eridu-tech/execution-context/als-execution-context-adapter"; import { contextToken } from "eridu-tech/execution-context/contracts"; import { TransactionContext } from "eridu-tech/transaction-context"; import { mongodbTransactionAdapter } from "./mongodb-transaction-adapter-settings.js"; import type { ClientSession, Db } from "mongodb"; const executionContext = new ExecutionContext(new AlsExecutionContextAdapter()); export const transactionContext = new TransactionContext({ token: contextToken("mongodb-transaction"), adapter: mongodbTransactionAdapter, executionContext, }); // `client` is always the base database, it never becomes transaction-scoped const users = transactionContext.client.collection("users"); async function createUser(name: string): Promise { // Every operation must be given the session to join the active transaction await users.insertOne( { name, }, { // The session of the active transaction, or `undefined` outside one session: transactionContext.transaction ?? undefined, }, ); } // No transaction is active, so the insert runs on the base database await createUser("Jose"); await transactionContext.run(async () => { // A transaction is active, so the insert joins it await createUser("Jose"); }); ``` ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/transaction-context`](https://eridu-tech.github.io/eridu-tech-core/modules/TransactionContext.html) API docs. --- # Creating TransactionContext adapters ## Implementing your custom ITransactionAdapter[​](#implementing-your-custom-itransactionadapter "Direct link to Implementing your custom ITransactionAdapter") In order to create an adapter you need to implement the [`ITransactionAdapter`](https://eridu-tech.github.io/eridu-tech-core/types/TransactionContext.ITransactionAdapter.html) contract. ## Implementing your custom ITransactionContext class[​](#implementing-your-custom-itransactioncontext-class "Direct link to Implementing your custom ITransactionContext class") In some cases, you may need to implement a custom [`TransactionContext`](https://eridu-tech.github.io/eridu-tech-core/classes/TransactionContext.TransactionContext.html) class to optimize performance for your specific technology stack. You can then directly implement the [`ITransactionContext`](https://eridu-tech.github.io/eridu-tech-core/types/TransactionContext.ITransactionContext.html) contract. ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/transaction-context`](https://eridu-tech.github.io/eridu-tech-core/modules/TransactionContext.html) API docs. --- # TransactionContext middlewares Transaction-context middlewares wrap functions to run them within a transaction or defer them until a transaction commits. They let you apply transaction behavior without adding it to the function implementation. ## Initial configuration[​](#initial-configuration "Direct link to Initial configuration") Set up the MongoDB and SQLite databases and their transaction adapters, then register them with the resolver and call `init()`. MongoDB is registered as the primary (default) adapter, while SQLite is registered as the secondary adapter: #### 1. Set up the MongoDB database[​](#1-set-up-the-mongodb-database "Direct link to 1. Set up the MongoDB database") ./samples/mongodb-database.ts ``` import { MongoClient } from "mongodb"; export const mongoClient = new MongoClient("mongodb://localhost:27017"); export const mongoDatabase = mongoClient.db("DATABASE_NAME"); ``` #### 2. Create the MongoDB transaction adapter[​](#2-create-the-mongodb-transaction-adapter "Direct link to 2. Create the MongoDB transaction adapter") ./samples/mongodb-transaction-adapter.ts ``` import { MongodbTransactionAdapter } from "eridu-tech/transaction-context/mongodb-transaction-adapter"; import { mongoClient, mongoDatabase } from "./mongodb-database.js"; export const mongodbTransactionAdapter = new MongodbTransactionAdapter({ client: mongoClient, database: mongoDatabase, }); ``` #### 3. Set up the SQLite database[​](#3-set-up-the-sqlite-database "Direct link to 3. Set up the SQLite database") ./samples/sqlite-database.ts ``` import Sqlite from "better-sqlite3"; import { Kysely, SqliteDialect } from "kysely"; export const sqliteDatabase = new Kysely({ dialect: new SqliteDialect({ database: new Sqlite(":memory:") }), }); ``` #### 4. Create the SQLite transaction adapter[​](#4-create-the-sqlite-transaction-adapter "Direct link to 4. Create the SQLite transaction adapter") ./samples/sqlite-transaction-adapter.ts ``` import { KyselyTransactionAdapter } from "eridu-tech/transaction-context/kysely-transaction-adapter"; import { sqliteDatabase } from "./sqlite-database.js"; export const sqliteTransactionAdapter = new KyselyTransactionAdapter({ database: sqliteDatabase, }); ``` #### 5. Configure the transaction context resolver[​](#5-configure-the-transaction-context-resolver "Direct link to 5. Configure the transaction context resolver") ./samples/transaction-context.ts ``` import { ExecutionContext } from "eridu-tech/execution-context"; import { AlsExecutionContextAdapter } from "eridu-tech/execution-context/als-execution-context-adapter"; import { TransactionContextResolver } from "eridu-tech/transaction-context"; import { mongodbTransactionAdapter } from "./mongodb-transaction-adapter.js"; import { sqliteTransactionAdapter } from "./sqlite-transaction-adapter.js"; type Adapters = "primary" | "secondary"; const executionContext = new ExecutionContext(new AlsExecutionContextAdapter()); export const transactionContextResolver = new TransactionContextResolver({ adapters: { primary: mongodbTransactionAdapter, secondary: sqliteTransactionAdapter, }, defaultAdapter: "primary", executionContext, }); await transactionContextResolver.init(); ``` ## withTransactionFactory middleware[​](#with_transaction_factory_middleware "Direct link to withTransactionFactory middleware") The transaction middleware intercepts function calls and runs them inside a transaction. When the wrapped function is invoked, the middleware delegates to the selected transaction context's `run()` method with the requested propagation mode, so the wrapped function executes within a transaction scope — joining, starting, or forbidding one depending on the mode. The propagation mode defaults to `REQUIRED`: the wrapped function joins an existing transaction when one is active and starts a new one otherwise. ### Usage[​](#usage "Direct link to Usage") ./samples/with-transaction.ts ``` import { use } from "eridu-tech/middleware"; import { withTransactionFactory } from "eridu-tech/transaction-context/middlewares"; import { transactionContextResolver } from "./transaction-context.js"; const withTransaction = withTransactionFactory(transactionContextResolver); const createUser = async (userId: string, name: string): Promise => { // ... return "user-id"; }; // Uses the default adapter (`primary`) and REQUIRED propagation const createUserInTransaction = use(createUser, withTransaction()); // Uses the `secondary` adapter const createUserOnSecondary = use( createUser, withTransaction.use("secondary")(), ); await createUserInTransaction("1", "Jose"); await createUserOnSecondary("2", "Mina"); ``` Calling `withTransaction()` uses the transaction resolver's default adapter (`primary` in this example). Use `withTransaction.use("secondary")()` to run the wrapped function through a specific registered adapter. info Here is a complete list of settings for the [`withTransactionFactory`](https://eridu-tech.github.io/eridu-tech-core/functions/TransactionContext.withTransactionFactory.html) function. ## withAfterCommitFactory middleware[​](#with_after_commit_factory_middleware "Direct link to withAfterCommitFactory middleware") The after-commit middleware intercepts function calls and registers them to run once the active transaction commits. When the wrapped function is invoked, the middleware delegates to `transactionContext.afterCommit()`, so the wrapped function is registered as an after-commit invocable instead of running immediately. This is useful for side effects that must not happen when the transaction is rolled back, such as sending emails or publishing events. When no transaction is active, the wrapped function runs immediately, unless `runIfNoTransaction` is `false`, in which case it is discarded. When using a `TransactionContextResolver`, `afterCommit` can be called within a transaction started by any configured adapter, for example `resolver.use("primary").run(callback)` or `resolver.use("secondary").run(callback)`. The hook runs after the active transaction commits. ### Usage[​](#usage-1 "Direct link to Usage") ./samples/with-after-commit.ts ``` import { use } from "eridu-tech/middleware"; import { withAfterCommitFactory } from "eridu-tech/transaction-context/middlewares"; import { transactionContextResolver } from "./transaction-context.js"; const withAfterCommit = withAfterCommitFactory(transactionContextResolver); const sendWelcomeEmail = async (userId: string): Promise => { // ... }; // Registers the wrapped function to run once the active transaction commits const sendWelcomeEmailAfterCommit = use(sendWelcomeEmail, withAfterCommit()); const sendWelcomeEmailAfterCommitOrSkip = use( sendWelcomeEmail, // Discards the wrapped function when no transaction is active withAfterCommit({ runIfNoTransaction: false }), ); // Runs once the transaction commits await transactionContextResolver .use("primary") .run(() => sendWelcomeEmailAfterCommit("1")); // Does nothing, because no transaction is active await sendWelcomeEmailAfterCommitOrSkip("2"); ``` info Here is a complete list of settings for the [`withAfterCommitFactory`](https://eridu-tech.github.io/eridu-tech-core/functions/TransactionContext.withAfterCommitFactory.html) function. ### Settings[​](#settings "Direct link to Settings") | Option | Type | Default | Description | | -------------------- | --------- | ------- | ---------------------------------------------------------------------------------------------------------------- | | `runIfNoTransaction` | `boolean` | `true` | Whether to invoke the wrapped function immediately when there is no active transaction, instead of discarding it | ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/transaction-context`](https://eridu-tech.github.io/eridu-tech-core/modules/TransactionContext.html) API docs. --- # TransactionContextResolver The `TransactionContextResolver` creates a transaction context for each configured adapter. Use it to run work with a default transaction adapter or select a specific adapter at runtime. ## Initial configuration[​](#initial-configuration "Direct link to Initial configuration") Set up the SQLite and MongoDB databases and their transaction adapters, then register them with the resolver and call `init()`. Initialization creates a transaction context for each configured adapter. #### 1. Set up the SQLite database[​](#1-set-up-the-sqlite-database "Direct link to 1. Set up the SQLite database") ./samples/sqlite-database.ts ``` import Sqlite from "better-sqlite3"; import { Kysely, SqliteDialect } from "kysely"; export const sqliteDatabase = new Kysely({ dialect: new SqliteDialect({ database: new Sqlite(":memory:") }), }); ``` #### 2. Create the SQLite transaction adapter[​](#2-create-the-sqlite-transaction-adapter "Direct link to 2. Create the SQLite transaction adapter") ./samples/sqlite-transaction-adapter.ts ``` import { KyselyTransactionAdapter } from "eridu-tech/transaction-context/kysely-transaction-adapter"; import { sqliteDatabase } from "./sqlite-database.js"; export const sqliteTransactionAdapter = new KyselyTransactionAdapter({ database: sqliteDatabase, }); ``` #### 3. Set up the MongoDB database[​](#3-set-up-the-mongodb-database "Direct link to 3. Set up the MongoDB database") ./samples/mongodb-database.ts ``` import { MongoClient } from "mongodb"; export const mongoClient = new MongoClient("mongodb://localhost:27017"); export const mongoDatabase = mongoClient.db("DATABASE_NAME"); ``` #### 4. Create the MongoDB transaction adapter[​](#4-create-the-mongodb-transaction-adapter "Direct link to 4. Create the MongoDB transaction adapter") ./samples/mongodb-transaction-adapter.ts ``` import { MongodbTransactionAdapter } from "eridu-tech/transaction-context/mongodb-transaction-adapter"; import { mongoClient, mongoDatabase } from "./mongodb-database.js"; export const mongodbTransactionAdapter = new MongodbTransactionAdapter({ client: mongoClient, database: mongoDatabase, }); ``` #### 5. Configure the transaction context resolver[​](#5-configure-the-transaction-context-resolver "Direct link to 5. Configure the transaction context resolver") ./samples/transaction-context-resolver-initial-config.ts ``` import { ExecutionContext } from "eridu-tech/execution-context"; import { AlsExecutionContextAdapter } from "eridu-tech/execution-context/als-execution-context-adapter"; import { TransactionContextResolver } from "eridu-tech/transaction-context"; import { mongodbTransactionAdapter } from "./mongodb-transaction-adapter.js"; import { sqliteTransactionAdapter } from "./sqlite-transaction-adapter.js"; type Adapters = "mongodb" | "sqlite"; const executionContext = new ExecutionContext(new AlsExecutionContextAdapter()); export const transactionContextResolver = new TransactionContextResolver({ adapters: { mongodb: mongodbTransactionAdapter, sqlite: sqliteTransactionAdapter, }, defaultAdapter: "mongodb", executionContext, }); await transactionContextResolver.init(); ``` ## Usage[​](#usage "Direct link to Usage") ### 1. Using the default adapter[​](#1-using-the-default-adapter "Direct link to 1. Using the default adapter") ./samples/transaction-context-resolver-default-adapter.ts ``` import { transactionContextResolver } from "./transaction-context-resolver-initial-config.js"; await transactionContextResolver.use().run(async () => { // Perform work using the default (mongodb) adapter's transaction. await transactionContextResolver.afterCommit(async () => { console.log("Transaction committed"); }); }); ``` danger If no default adapter is configured and none is specified in `use()`, an error is thrown. ### 2. Specifying an adapter explicitly[​](#2-specifying-an-adapter-explicitly "Direct link to 2. Specifying an adapter explicitly") ./samples/transaction-context-resolver-specific-adapter.ts ``` import { transactionContextResolver } from "./transaction-context-resolver-initial-config.js"; await transactionContextResolver.use("sqlite").run(async () => { // Perform work using the SQLite adapter's transaction. }); ``` danger If you specify an adapter that is not configured, an error is thrown. Call `init()` before `use()`; otherwise, no configured adapter is available yet. ### Registering after-commit hooks[​](#registering-after-commit-hooks "Direct link to Registering after-commit hooks") The resolver's `afterCommit()` registers a callback with the configured transaction contexts. If none has an active transaction, the callback runs immediately by default. Otherwise, it is scheduled with each active transaction context and runs after that context commits. Use `runIfNoTransaction: false` to discard the callback when no transaction is active. ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/transaction-context`](https://eridu-tech.github.io/eridu-tech-core/modules/TransactionContext.html) API docs. --- # TransactionContext usage The `eridu-tech/transaction-context` component lets you run code inside a database transaction and access the client that belongs to the current transaction scope, without passing it around manually. ## Initial configuration[​](#initial-configuration "Direct link to Initial configuration") To begin using the `TransactionContext` class, you'll need to create and configure an instance: First, set up the Kysely database: ./samples/kysely-database.ts ``` import Sqlite from "better-sqlite3"; import { Kysely, SqliteDialect } from "kysely"; export const database = new Kysely({ dialect: new SqliteDialect({ database: new Sqlite("DATABASE_NAME.db"), }), }); ``` Next, set up the execution context that tracks the active transaction across asynchronous scopes: ./samples/execution-context.ts ``` import { ExecutionContext } from "eridu-tech/execution-context"; import { AlsExecutionContextAdapter } from "eridu-tech/execution-context/als-execution-context-adapter"; export const executionContext = new ExecutionContext( new AlsExecutionContextAdapter(), ); ``` Then create a `TransactionContext` using the database and execution context: ./samples/transaction-context-initial-config.ts ``` import { contextToken } from "eridu-tech/execution-context/contracts"; import { TransactionContext } from "eridu-tech/transaction-context"; import { KyselyTransactionAdapter } from "eridu-tech/transaction-context/kysely-transaction-adapter"; import { executionContext } from "./execution-context.js"; import { database } from "./kysely-database.js"; import type { Kysely } from "kysely"; export const transactionContext = new TransactionContext>({ // The token the active transaction is stored under token: contextToken("sqlite-transaction"), // You can choose the adapter to use adapter: new KyselyTransactionAdapter({ database }), // The execution context that tracks the active transaction across scopes executionContext, }); ``` info Here is a complete list of settings for the [`TransactionContext`](https://eridu-tech.github.io/eridu-tech-core/types/TransactionContext.ITransactionContextSettings.html) class. ## Transaction basics[​](#transaction-basics "Direct link to Transaction basics") ### Running code in a transaction[​](#running-code-in-a-transaction "Direct link to Running code in a transaction") You can run an invocable inside a transaction scope with `run()`. When no transaction is active a new one is started, committed after the invocable succeeds, and the invocable's return value is returned: ./samples/transaction-context-run.ts ``` import { transactionContext } from "./transaction-context-initial-config.js"; // Starts a transaction, commits it once the invocable succeeds and returns its result const user = await transactionContext.run(async () => { return transactionContext.current .insertInto("users") .values({ id: "1", name: "Jose" }) .returningAll() .executeTakeFirst(); }); ``` ### Aborting a transaction[​](#aborting-a-transaction "Direct link to Aborting a transaction") When the invocable throws, the transaction is aborted and the original error is re-thrown: ./samples/transaction-context-abort.ts ``` import { transactionContext } from "./transaction-context-initial-config.js"; try { await transactionContext.run(async () => { await transactionContext.current .insertInto("users") .values({ id: "1", name: "Jose" }) .execute(); // Throwing inside the invocable aborts the transaction throw new Error("Something went wrong"); }); } catch (error: unknown) { // The original error is re-thrown after the transaction was aborted console.error(error); } ``` ### Inspecting the connection state[​](#inspecting-the-connection-state "Direct link to Inspecting the connection state") A transaction context exposes four read-only members describing the current scope, plus a fail-fast accessor for the transaction client. None of them performs I/O, so they are safe to read anywhere. #### `client`[​](#client "Direct link to client") The base client that operates outside of any transaction. It comes straight from the adapter and never becomes transaction-scoped, so it is the same instance inside and outside a transaction. Use it only for work that must stay **outside** the ambient transaction: * schema and migration work, * a repository that opts out of the caller's transaction. ./samples/transaction-context-client.ts ``` import { transactionContext } from "./transaction-context-initial-config.js"; async function writeAuditLog(message: string): Promise { // The base client never joins a transaction await transactionContext.client .insertInto("audit_logs") .values({ message }) .execute(); } // No transaction is active, so the base client is the client in use here console.log(transactionContext.client); // The base client await writeAuditLog("Started"); ``` warning Never use the base client for work that belongs to the transaction. It runs on a different connection than the open transaction, which means the work silently escapes the transaction and the two connections can deadlock against each other. #### `transaction`[​](#transaction "Direct link to transaction") The active transaction-scoped client, or `null` when there is none. Use it to get the transaction client explicitly and to handle the absence yourself. It never throws, so a `null` check is required; for the opposite behaviour use `getTransactionOrFail()`. The value is scoped to the current call chain and to this context's `token`, so another `TransactionContext` with a different token sees `null` even while a transaction is active here. ./samples/transaction-context-transaction.ts ``` import { transactionContext } from "./transaction-context-initial-config.js"; // The base client is used here, because no transaction is active console.log(transactionContext.transaction); // null await transactionContext.run(async () => { // The transaction-scoped client console.log(transactionContext.transaction); // `transaction` is nullable, so check it before using it const trx = transactionContext.transaction; if (trx !== null) { await trx.insertInto("users").values({ id: "1" }).execute(); } }); ``` #### `current`[​](#current "Direct link to current") The client for the current scope: the transaction-scoped client when a transaction is active, otherwise the base client. It is never `null`. Prefer it in application and repository code, so that code takes part in the caller's transaction when there is one and falls back to the base client when there is none. ./samples/transaction-context-current.ts ``` import { transactionContext } from "./transaction-context-initial-config.js"; async function createUser(userId: string): Promise { // Joins the active transaction when there is one, otherwise uses the base client await transactionContext.current .insertInto("users") .values({ id: userId }) .execute(); } // No transaction is active, so the base client is used console.log(transactionContext.current); // The base client await createUser("1"); await transactionContext.run(async () => { // A transaction is active, so the transaction-scoped client is used console.log(transactionContext.current); // The transaction-scoped client // The very same function now takes part in the transaction await createUser("2"); }); ``` info `current` is typed as `TClient | TTransactionClient`, so what it can do depends on the two type parameters. When they are the same, as with `KyselyTransactionAdapter`, `current` is fully usable. When they differ, as with `MongodbTransactionAdapter` (an `ITransactionAdapter`), only the members shared by both types are callable, so a MongoDB driver call cannot be made through `current`. In that case, read the `transaction` getter to obtain the `ClientSession` and pass it to the collection settings as `{ session }`. #### `isInTransaction`[​](#isintransaction "Direct link to isintransaction") Whether a transaction is active in this context. Derived from `transaction`, so it is cheap to call anywhere. Use it to decide, not to do: defer a side effect through `afterCommit()` or run it right away, guard transactional-only code, or narrow `current` when the two client types differ. It is always `false` for `TransactionContext.noOp()`. ./samples/transaction-context-is-in-transaction.ts ``` import { transactionContext } from "./transaction-context-initial-config.js"; async function createUser(userId: string): Promise { await transactionContext.current .insertInto("users") .values({ id: userId }) .execute(); } async function notify(userId: string): Promise { console.log(`Created user ${userId}`); } async function createUserAndNotify(userId: string): Promise { if (transactionContext.isInTransaction) { // Only notify once the active transaction commits await transactionContext.afterCommit(() => notify(userId)); } await createUser(userId); if (!transactionContext.isInTransaction) { // There is no transaction, so notify right away await notify(userId); } } console.log(transactionContext.isInTransaction); // false await transactionContext.run(() => createUserAndNotify("1")); ``` #### `getTransactionOrFail()`[​](#gettransactionorfail "Direct link to gettransactionorfail") Returns the active transaction-scoped client, and throws a `MandatoryPropagationError` when no transaction is active. Calling it opts the surrounding code into `MANDATORY` propagation, which is the fail-fast counterpart of checking `transaction` for `null` yourself. ./samples/transaction-context-get-transaction-or-fail.ts ``` import { transactionContext } from "./transaction-context-initial-config.js"; await transactionContext.run(async () => { // Returns the transaction-scoped client, or throws when no transaction is active const trx = transactionContext.getTransactionOrFail(); await trx.insertInto("users").values({ id: "1", name: "Jose" }).execute(); }); // Throws a MandatoryPropagationError, because no transaction is active here transactionContext.getTransactionOrFail(); ``` In short: `client` opts out of the transaction, `current` opts in, `transaction` and `getTransactionOrFail()` require it, and `isInTransaction` asks whether there is one. ### Nesting runs[​](#nesting-runs "Direct link to Nesting runs") Nested `run()` calls reuse the transaction that is already active instead of starting a new one, so a single transaction spans the whole call chain: ./samples/transaction-context-nested-run.ts ``` import { transactionContext } from "./transaction-context-initial-config.js"; // Only one transaction is started: nested runs join the transaction of the outer run await transactionContext.run(async () => { await transactionContext.run(async () => { // Same transaction as the outer run }); }); ``` ## Patterns[​](#patterns "Direct link to Patterns") ### Propagation modes[​](#propagation-modes "Direct link to Propagation modes") `run()` accepts a propagation mode that decides how it behaves in relation to an existing transaction: | Mode | Transaction is active | No transaction is active | | ----------- | ------------------------------ | ---------------------------------- | | `REQUIRED` | Joins the existing transaction | Starts a new transaction | | `SUPPORTS` | Joins the existing transaction | Runs without a transaction | | `MANDATORY` | Joins the existing transaction | Throws `MandatoryPropagationError` | | `NEVER` | Throws `NeverPropagationError` | Runs without a transaction | `REQUIRED` is the default when no propagation mode is provided: ./samples/transaction-context-propagation.ts ``` import { TRANSACTION_PROPAGATION } from "eridu-tech/transaction-context/contracts"; import { transactionContext } from "./transaction-context-initial-config.js"; async function createUser(userId: string): Promise { await transactionContext.current .insertInto("users") .values({ id: userId }) .execute(); } // Joins the active transaction, or starts a new one when there is none (default) await transactionContext.run(TRANSACTION_PROPAGATION.REQUIRED, () => createUser("1"), ); // Joins the active transaction, or runs without a transaction when there is none await transactionContext.run(TRANSACTION_PROPAGATION.SUPPORTS, () => createUser("2"), ); // Requires an active transaction, otherwise a MandatoryPropagationError is thrown await transactionContext.run(TRANSACTION_PROPAGATION.MANDATORY, () => createUser("3"), ); // Must run outside of a transaction, otherwise a NeverPropagationError is thrown await transactionContext.run(TRANSACTION_PROPAGATION.NEVER, () => createUser("4"), ); ``` info Here is a complete list of propagation modes for the [`TRANSACTION_PROPAGATION`](https://eridu-tech.github.io/eridu-tech-core/variables/TransactionContext.TRANSACTION_PROPAGATION.html) constant. ### After commit hooks[​](#after-commit-hooks "Direct link to After commit hooks") You can register an invocable that only runs once the active transaction commits. This is useful for side effects that must not happen when the transaction is rolled back, such as sending emails or publishing events: ./samples/transaction-context-after-commit.ts ``` import { transactionContext } from "./transaction-context-initial-config.js"; async function sendWelcomeEmail(userId: string): Promise { // ... } await transactionContext.run(async () => { await transactionContext.current .insertInto("users") .values({ id: "1", name: "Jose" }) .execute(); // Registered as an after-commit hook, so it only runs once the transaction commits await transactionContext.afterCommit(() => sendWelcomeEmail("1")); }); ``` Hooks are attached to the transaction scope they were registered in, so they are discarded when that transaction aborts. When several hooks are registered, they run in registration order. ### Hooks without a transaction[​](#hooks-without-a-transaction "Direct link to Hooks without a transaction") When no transaction is active, `afterCommit()` decides what to do based on the `runIfNoTransaction` setting: ./samples/transaction-context-after-commit-no-transaction.ts ``` import { transactionContext } from "./transaction-context-initial-config.js"; async function sendWelcomeEmail(userId: string): Promise { // ... } // No transaction is active, so the hook runs immediately await transactionContext.afterCommit(() => sendWelcomeEmail("1")); // No transaction is active, so the hook is discarded await transactionContext.afterCommit(() => sendWelcomeEmail("2"), { runIfNoTransaction: false, }); ``` ### Non-transactional contexts[​](#non-transactional-contexts "Direct link to Non-transactional contexts") You can create a context that never uses transactions with `TransactionContext.noOp()`. This is useful when a component expects a transaction context, but the underlying storage cannot support transactions: ./samples/transaction-context-no-op.ts ``` import { TransactionContext } from "eridu-tech/transaction-context"; import Sqlite from "better-sqlite3"; import { Kysely, SqliteDialect } from "kysely"; const database = new Kysely({ dialect: new SqliteDialect({ database: new Sqlite("DATABASE_NAME.db"), }), }); // A context that never uses transactions: // `run()` invokes its invocable directly and `afterCommit()` hooks run immediately export const noOpTransactionContext = TransactionContext.noOp(database); ``` info With a no-op context, `run()` invokes its invocable directly, `isInTransaction` is always `false`, `transaction` is always `null`, and `getTransactionOrFail()` always throws a `MandatoryPropagationError`. ### Multiple databases[​](#multiple-databases "Direct link to Multiple databases") You can fan out after-commit hooks to several transaction contexts with `MultiTransactionHooks`, so a single consumer stays transaction-aware across every database a project uses, for example PostgreSQL and SQLite at the same time: ./samples/multi-transaction-hooks.ts ``` import { ExecutionContext } from "eridu-tech/execution-context"; import { AlsExecutionContextAdapter } from "eridu-tech/execution-context/als-execution-context-adapter"; import { TransactionContextResolver } from "eridu-tech/transaction-context"; import { KyselyTransactionAdapter } from "eridu-tech/transaction-context/kysely-transaction-adapter"; import { Kysely, PostgresDialect } from "kysely"; import { Pool } from "pg"; import Sqlite from "better-sqlite3"; import { SqliteDialect } from "kysely"; const postgresDatabase = new Kysely({ dialect: new PostgresDialect({ pool: new Pool({ database: "DATABASE_NAME", host: "DATABASE_HOST", user: "DATABASE_USER", // DATABASE port port: 5432, password: "DATABASE_PASSWORD", }), }), }); const executionContext = new ExecutionContext(new AlsExecutionContextAdapter()); const sqliteDatabase = new Kysely({ dialect: new SqliteDialect({ database: new Sqlite("local.db"), }), }); const transactionContextResolver = new TransactionContextResolver({ adapters: { postgres: new KyselyTransactionAdapter({ database: postgresDatabase }), sqlite: new KyselyTransactionAdapter({ database: sqliteDatabase }), }, executionContext, }); await transactionContextResolver.init(); async function publishUserCreatedEvent(userId: string): Promise { // ... } // No wrapped context is in a transaction, so the hook runs immediately await transactionContextResolver.afterCommit(() => publishUserCreatedEvent("1"), ); // Only the postgres context is in a transaction, so the hook is registered on // it and runs once that transaction commits await transactionContextResolver.use("postgres").run(async () => { await transactionContextResolver.afterCommit(() => publishUserCreatedEvent("1"), ); }); // Only the sqlite context is in a transaction, so the hook is registered on it // and runs once that transaction commits await transactionContextResolver.use("sqlite").run(async () => { await transactionContextResolver.afterCommit(() => publishUserCreatedEvent("1"), ); }); ``` info The hook is registered on every wrapped context that currently has an active transaction, and therefore runs once per transaction that commits. When none of the wrapped contexts is in a transaction, the hook runs immediately unless `runIfNoTransaction` is `false`. ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/transaction-context`](https://eridu-tech.github.io/eridu-tech-core/modules/TransactionContext.html) API docs. --- # DI Integration The `ProxyTransactionContextResolver` class provides an [`ITransactionContextResolver`](/docs/foundation/transaction_context/transaction_context_resolver/.md) whose underlying resolver is resolved from a [DI container](/docs/foundation/di/.md). The proxy supports the `ITransactionContextResolver` operations: * `use(adapterName?)` returns a transaction runner that resolves the resolver from the container for each operation. * `run(...)` delegates to the default adapter's transaction runner. * `afterCommit(asyncInvocable, settings?)` resolves the resolver from the container and delegates to it. The proxy resolves the resolver lazily, so it supports the container's configured lifetimes. Scoped registrations must be used inside `container.run()`. Initialize the resolver from the container before using it. ## Initial configuration[​](#initial-configuration "Direct link to Initial configuration") Set up the MongoDB and SQLite databases and transaction adapters. Create the container, register the MongoDB adapter as primary (default) and the SQLite adapter as secondary, initialize the resolver from its `onInit` hook, and wrap it with `ProxyTransactionContextResolver`: #### 1. Set up the MongoDB database[​](#1-set-up-the-mongodb-database "Direct link to 1. Set up the MongoDB database") ./samples/mongodb-database.ts ``` import { MongoClient } from "mongodb"; export const mongoClient = new MongoClient("mongodb://localhost:27017"); export const mongoDatabase = mongoClient.db("DATABASE_NAME"); ``` #### 2. Create the MongoDB transaction adapter[​](#2-create-the-mongodb-transaction-adapter "Direct link to 2. Create the MongoDB transaction adapter") ./samples/mongodb-transaction-adapter.ts ``` import { MongodbTransactionAdapter } from "eridu-tech/transaction-context/mongodb-transaction-adapter"; import { mongoClient, mongoDatabase } from "./mongodb-database.js"; export const mongodbTransactionAdapter = new MongodbTransactionAdapter({ client: mongoClient, database: mongoDatabase, }); ``` #### 3. Set up the SQLite database[​](#3-set-up-the-sqlite-database "Direct link to 3. Set up the SQLite database") ./samples/sqlite-database.ts ``` import Sqlite from "better-sqlite3"; import { Kysely, SqliteDialect } from "kysely"; export const sqliteDatabase = new Kysely({ dialect: new SqliteDialect({ database: new Sqlite(":memory:") }), }); ``` #### 4. Create the SQLite transaction adapter[​](#4-create-the-sqlite-transaction-adapter "Direct link to 4. Create the SQLite transaction adapter") ./samples/sqlite-transaction-adapter.ts ``` import { KyselyTransactionAdapter } from "eridu-tech/transaction-context/kysely-transaction-adapter"; import { sqliteDatabase } from "./sqlite-database.js"; export const sqliteTransactionAdapter = new KyselyTransactionAdapter({ database: sqliteDatabase, }); ``` #### 5. Create the dependency injection container[​](#5-create-the-dependency-injection-container "Direct link to 5. Create the dependency injection container") ./samples/container.ts ``` import { Container } from "eridu-tech/di"; import { ExecutionContext } from "eridu-tech/execution-context"; import { AlsExecutionContextAdapter } from "eridu-tech/execution-context/als-execution-context-adapter"; export const executionContext = new ExecutionContext( new AlsExecutionContextAdapter(), ); export const container = new Container({ executionContext }); ``` #### 6. Register and initialize the transaction context resolver[​](#6-register-and-initialize-the-transaction-context-resolver "Direct link to 6. Register and initialize the transaction context resolver") ./samples/transaction-context-resolver-di.ts ``` import { genericToken, LIFETIME } from "eridu-tech/di/contracts"; import { ProxyTransactionContextResolver } from "eridu-tech/transaction-context/di"; import { TransactionContextResolver } from "eridu-tech/transaction-context"; import { container, executionContext } from "./container.js"; import { mongodbTransactionAdapter } from "./mongodb-transaction-adapter.js"; import { sqliteTransactionAdapter } from "./sqlite-transaction-adapter.js"; type Adapters = "primary" | "secondary"; const resolverToken = genericToken>( "transactionContextResolver", ); container.registerFactory({ token: resolverToken, factory: () => new TransactionContextResolver({ adapters: { primary: mongodbTransactionAdapter, secondary: sqliteTransactionAdapter, }, defaultAdapter: "primary", executionContext, }), onInit: async (resolver) => { await resolver.init(); }, deps: {}, lifetime: LIFETIME.SINGLETON, }); // Create the proxy before container.init() export const transactionContext = new ProxyTransactionContextResolver( { container, resolverToken, }, ); await container.init(); ``` ## Usage[​](#usage "Direct link to Usage") ### 1. Using the default adapter[​](#1-using-the-default-adapter "Direct link to 1. Using the default adapter") ./samples/transaction-context-resolver-di-default-adapter.ts ``` import { transactionContext } from "./transaction-context-resolver-di.js"; // Runs inside a transaction using the default (primary) adapter await transactionContext.run(async () => { // Perform transactional work here await transactionContext.afterCommit(async () => { console.log("Transaction committed"); }); }); ``` ### 2. Specifying an adapter explicitly[​](#2-specifying-an-adapter-explicitly "Direct link to 2. Specifying an adapter explicitly") ./samples/transaction-context-resolver-di-specific-adapter.ts ``` import { transactionContext } from "./transaction-context-resolver-di.js"; // Runs inside a transaction using the secondary adapter await transactionContext.use("secondary").run(async () => { // Perform transactional work here }); ``` ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/transaction-context`](https://eridu-tech.github.io/eridu-tech-core/modules/TransactionContext.html) API docs. --- # Getting started **Write business logic once. Replace infrastructure anytime.** eridu-tech is an adapter-first backend foundation toolkit for TypeScript, purpose-built for developing majestic modular monoliths. It provides a comprehensive ecosystem of officially maintained components like caching, locking, rate limiting, event buses, HTTP routing, resilience, and more that integrate seamlessly with popular frameworks like Next.js, Nuxt, Express, and NestJS. This guide covers the basics: installation and configuration. ## Prerequisites[​](#prerequisites "Direct link to Prerequisites") * Node.js installed (version 24 and up) * npm/yarn/pnpm package manager ## Installation[​](#installation "Direct link to Installation") Run the following command to install the library: ``` npm install eridu-tech ``` ## Configuration[​](#configuration "Direct link to Configuration") #### Set Module Type:[​](#set-module-type "Direct link to Set Module Type:") `eridu-tech` exclusively uses ESM (ECMAScript Modules). To configure your project: 1. Open your `package.json` 2. Add or update the `type` field: ``` { "type": "module" // ... your existing configurations } ``` info This is only required when running in Node.js. Frameworks like `Next.js`, `SvelteKit.js` and `Nuxt.js` use bundlers that support ESM modules automatically. --- # Overview Welcome to the **Guides** section. These guides walk you through practical, step-by-step scenarios for building with `eridu-tech`. info Guides are coming soon — as of now there are no guides published yet. ## What you'll find here[​](#what-youll-find-here "Direct link to What you'll find here") * **Guided workflows** — end-to-end recipes that combine multiple components to solve real-world problems. * **Best practices** — recommended patterns for configuration, testing, and production usage. ## Getting started[​](#getting-started "Direct link to Getting started") If you haven't already, start with the **[Getting started](/docs/getting_started.md)** page to get `eridu-tech` set up in your project, then come back here to explore the guides. --- # LLM This page is the gateway for AI assistants, LLM tooling, and anyone who wants to consume the Eridu-tech documentation programmatically. ## Table of Contents[​](#table-of-contents "Direct link to Table of Contents") * [Overview](#overview) * [Machine-Readable Documentation](#machine_readable_documentation) * [llms.txt](#llms_txt) * [llms-full.txt](#llms_full_txt) * [Agent Skill Package](#agent_skill_package) * [How to Use These Files](#how_to_use_these_files) ## Overview[​](#overview "Direct link to Overview") `eridu-tech` ships machine-readable versions of its documentation that are optimized for large language models and AI-assisted tooling. These plain-text files let an LLM quickly discover the library's components and usage patterns — no need to crawl the entire site. ## Machine-Readable Documentation[​](#machine_readable_documentation "Direct link to Machine-Readable Documentation") Two plain-text files are published, each tuned for a different context budget: ### llms.txt[​](#llms_txt "Direct link to llms.txt") **[llms.txt](https://www.eridu-tech.dev/llms.txt)** is a concise, structured index of the documentation. It gives AI assistants a compact map of the ecosystem — ideal for quick context retrieval and navigation. ### llms-full.txt[​](#llms_full_txt "Direct link to llms-full.txt") **[llms-full.txt](https://www.eridu-tech.dev/llms-full.txt)** contains the complete documentation in a single plain-text file. Use it when you need the full context for deep understanding and comprehensive answers. ## Agent Skill Package[​](#agent_skill_package "Direct link to Agent Skill Package") **[Download the skill package](/assets/files/eridu-tech-skills-722960a411b738d61fa23c04ab0567ce.zip)** — a zip archive that bundles the entire API surface of `eridu-tech` as an AI agent skill. The archive contains: * **`eridu-tech-skills/SKILL.md`** — the main skill file in the [SKILL.md](https://agentskills.io) format, with a quick reference to the full API. * **`eridu-tech-skills/references/`** — on-demand reference files covering every export, grouped by kind: functions, classes, types, variables, and configuration options. The skill is generated automatically from the library's TypeScript source — every export with its signatures, parameters, return types, and usage examples. It's designed to be loaded by AI agents and assistants: import it into your agent of choice to give it accurate, up-to-date knowledge of the `eridu-tech` API without crawling the site. ## How to Use These Files[​](#how_to_use_these_files "Direct link to How to Use These Files") * **Quick lookups** — start with `llms.txt` to locate the right component or section before diving deeper. * **Full-context tasks** — feed `llms-full.txt` to your tool when you want the entire documentation available in a single pass. * **Agent integration** — download `eridu-tech-skills.zip` to give your AI agent a ready-made, token-budgeted skill describing the whole API. --- # Configuring EventBus adapters ## MemoryEventBusAdapter[​](#memoryeventbusadapter "Direct link to MemoryEventBusAdapter") To use the `MemoryEventBusAdapter` you only need to create instance of it. ./samples/memory-event-bus-adapter.ts ``` import { MemoryEventBusAdapter } from "eridu-tech/event-bus/memory-event-bus-adapter"; const eventBusAdapter = new MemoryEventBusAdapter(); ``` You can also provide an `EventEmitter` that will be used for dispatching the events in memory: ./samples/memory-event-bus-adapter-with-emitter.ts ``` import { MemoryEventBusAdapter } from "eridu-tech/event-bus/memory-event-bus-adapter"; import { EventEmitter } from "node:events"; const eventEmitter = new EventEmitter(); const eventBusAdapter = new MemoryEventBusAdapter({ eventEmitter, }); ``` Pass any [`ITransactionHooks`](https://eridu-tech.github.io/eridu-tech-core/types/TransactionContext.ITransactionHooks.html) (like a [`TransactionContext`](/docs/foundation/transaction_context/transaction_context_usage/.md), [`MultiTransactionHooks`](/docs/foundation/transaction_context/transaction_context_usage/.md)) as the `transactionHooks` setting to make the adapter transaction aware. Events dispatch immediately outside a transaction, and inside transaction events dispatch after commit: ./samples/transaction-context.ts ``` import { ExecutionContext } from "eridu-tech/execution-context"; import { AlsExecutionContextAdapter } from "eridu-tech/execution-context/als-execution-context-adapter"; import { contextToken } from "eridu-tech/execution-context/contracts"; import { TransactionContext } from "eridu-tech/transaction-context"; import { KyselyTransactionAdapter } from "eridu-tech/transaction-context/kysely-transaction-adapter"; import Sqlite from "better-sqlite3"; import { Kysely, SqliteDialect } from "kysely"; const database = new Kysely({ dialect: new SqliteDialect({ database: new Sqlite("DATABASE_NAME.db"), }), }); export const transactionContext = new TransactionContext>({ // The token the active transaction is stored under token: contextToken("sqlite-transaction"), // You can choose the adapter to use adapter: new KyselyTransactionAdapter({ database }), // The execution context that tracks the active transaction across scopes executionContext: new ExecutionContext(new AlsExecutionContextAdapter()), }); ``` ./samples/memory-event-bus-adapter-transaction-aware.ts ``` import { MemoryEventBusAdapter } from "eridu-tech/event-bus/memory-event-bus-adapter"; import { transactionContext } from "./transaction-context.js"; const eventBusAdapter = new MemoryEventBusAdapter({ // Immediately outside a transaction, after commit inside transaction transactionHooks: transactionContext, }); ``` info `MemoryEventBusAdapter` lets you test your app without external dependencies like `Redis`, ideal for local development, unit tests, integration tests and fast E2E test for the backend application. ## RedisPubSubEventBusAdapter[​](#redispubsubeventbusadapter "Direct link to RedisPubSubEventBusAdapter") To use the `RedisPubSubEventBusAdapter`, you'll need to: 1. Install the required dependency: [`ioredis`](https://www.npmjs.com/package/ioredis) package: 2. Provide a string serializer ([`ISerde`](/docs/foundation/serde/.md)): * We recommend using `SuperJsonSerdeAdapter` for this purpose ./samples/redis-pub-sub-event-bus-adapter.ts ``` import { RedisPubSubEventBusAdapter } from "eridu-tech/event-bus/redis-pub-sub-event-bus-adapter"; import { SuperJsonSerde } from "eridu-tech/serde/super-json-serde"; import { Redis } from "ioredis"; const client = new Redis("YOUR_REDIS_CONNECTION_STRING"); const serde = new SuperJsonSerde(); const eventBusAdapter = new RedisPubSubEventBusAdapter({ client, serde, }); ``` Pass any [`ITransactionHooks`](https://eridu-tech.github.io/eridu-tech-core/types/TransactionContext.ITransactionHooks.html) (like a [`TransactionContext`](/docs/foundation/transaction_context/transaction_context_usage/.md), [`MultiTransactionHooks`](/docs/foundation/transaction_context/transaction_context_usage/.md)) as the `transactionHooks` setting to make the adapter transaction aware. Events dispatch immediately outside a transaction, and inside transaction events dispatch after commit: ./samples/redis-pub-sub-event-bus-adapter-transaction-aware.ts ``` import { RedisPubSubEventBusAdapter } from "eridu-tech/event-bus/redis-pub-sub-event-bus-adapter"; import { SuperJsonSerde } from "eridu-tech/serde/super-json-serde"; import { Redis } from "ioredis"; import { transactionContext } from "./transaction-context.js"; const client = new Redis("YOUR_REDIS_CONNECTION_STRING"); const serde = new SuperJsonSerde(); const eventBusAdapter = new RedisPubSubEventBusAdapter({ client, serde, // Immediately outside a transaction, after commit inside transaction transactionHooks: transactionContext, }); ``` ## NoOpEventBusAdapter[​](#noopeventbusadapter "Direct link to NoOpEventBusAdapter") The `NoOpEventBusAdapter` is a no-operation implementation, it performs no actions when called. ./samples/no-op-event-bus-adapter.ts ``` import { NoOpEventBusAdapter } from "eridu-tech/event-bus/no-op-event-bus-adapter"; const noEventBusAdapter = new NoOpEventBusAdapter(); ``` info The `NoOpEventBusAdapter` is useful when you want to mock out or disable your [`EventBus`](https://eridu-tech.github.io/eridu-tech-core/classes/EventBus.EventBus.html) class. ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/event-bus`](https://eridu-tech.github.io/eridu-tech-core/modules/EventBus.html) API docs. --- # Creating EventBus adapters ## Implementing your custom IEventBusAdapter[​](#implementing-your-custom-ieventbusadapter "Direct link to Implementing your custom IEventBusAdapter") In order to create an adapter you need to implement the [`IEventBusAdapter`](https://eridu-tech.github.io/eridu-tech-core/types/EventBus.IEventBusAdapter.html) contract. ## Testing your custom IEventBusAdapter[​](#testing-your-custom-ieventbusadapter "Direct link to Testing your custom IEventBusAdapter") We provide a complete test suite to verify your event-bus adapter implementation. Simply use the [`eventBusAdapterTestSuite`](https://eridu-tech.github.io/eridu-tech-core/functions/EventBus.eventBusAdapterTestSuite.html) function: The suite provides preconfigured Vitest test cases with common edge case coverage and standardized event-bus adapter contract conformance testing. Usage example: ./samples/event-bus-adapter-test-suite.ts ``` import { describe, test, beforeEach, expect } from "vitest"; import { eventBusAdapterTestSuite } from "eridu-tech/event-bus/test-utilities"; import { MemoryEventBusAdapter } from "eridu-tech/event-bus/memory-event-bus-adapter"; describe("class: MyEventBusAdapter", () => { eventBusAdapterTestSuite({ createAdapter: () => new MemoryEventBusAdapter(), test, beforeEach, expect, describe, }); }); ``` ## Implementing your custom IEventBus class[​](#implementing-your-custom-ieventbus-class "Direct link to Implementing your custom IEventBus class") In some cases, you may need to implement a custom [`EventBus`](https://eridu-tech.github.io/eridu-tech-core/modules/EventBus.html) class to optimize performance for your specific technology stack. You can then directly implement the [`IEventBus`](https://eridu-tech.github.io/eridu-tech-core/types/EventBus.IEventBus.html) contract. ## Testing your custom IEventBus class[​](#testing-your-custom-ieventbus-class "Direct link to Testing your custom IEventBus class") We provide a complete test suite to verify your custom event-bus class implementation. Simply use the [`eventBusTestSuite`](https://eridu-tech.github.io/eridu-tech-core/functions/EventBus.eventBusTestSuite.html) function: The suite provides preconfigured Vitest test cases with common edge case coverage and standardized event-bus contract conformance testing. Usage example: ./samples/event-bus-test-suite.ts ``` import { describe, test, beforeEach, expect } from "vitest"; import { eventBusTestSuite } from "eridu-tech/event-bus/test-utilities"; import { EventBus } from "eridu-tech/event-bus"; import { MemoryEventBusAdapter } from "eridu-tech/event-bus/memory-event-bus-adapter"; describe("class: EventBus", () => { eventBusTestSuite({ createEventBus: () => new EventBus({ adapter: new MemoryEventBusAdapter(), }), test, beforeEach, expect, describe, }); }); ``` ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/event-bus`](https://eridu-tech.github.io/eridu-tech-core/modules/EventBus.html) API docs. --- # EventBus middlewares EventBus middlewares let you dispatch events around a wrapped function without coupling the function itself to the event bus. Each factory takes an `IEventBusResolver` and returns a middleware configured with an event `type` and a `payload` invocable. The payload is resolved from the wrapped function's arguments (and, where applicable, its return value or caught error) and dispatched on the selected event bus, while the wrapped function's result (or thrown error) passes through unchanged. If the payload resolves to `null` (i.e. it doesn't return a value), no event is dispatched. ## Initial configuration[​](#initial-configuration "Direct link to Initial configuration") To begin using the EventBus middlewares, create and configure an `EventBusResolver`: ./samples/event-bus.ts ``` import { EventBusResolver } from "eridu-tech/event-bus"; import { RedisPubSubEventBusAdapter } from "eridu-tech/event-bus/redis-pub-sub-event-bus-adapter"; import { MemoryEventBusAdapter } from "eridu-tech/event-bus/memory-event-bus-adapter"; import { SuperJsonSerde } from "eridu-tech/serde/super-json-serde"; import { Redis } from "ioredis"; const serde = new SuperJsonSerde(); export const eventBusResolver = new EventBusResolver({ adapters: { memory: new MemoryEventBusAdapter(), redis: new RedisPubSubEventBusAdapter({ client: new Redis("YOUR_REDIS_CONNECTION_STRING"), serde, }), }, defaultAdapter: "memory", }); await eventBusResolver.init(); ``` ## withDispatchBeforeFactory middleware[​](#with_dispatch_before_factory_middleware "Direct link to withDispatchBeforeFactory middleware") The before-dispatch middleware emits an event **before** the wrapped function is invoked. When the wrapped function is called, the middleware resolves the event payload from the function's arguments, dispatches the configured event, and then invokes the wrapped function and returns its result. If the payload resolves to `null`, no event is dispatched and the wrapped function is invoked directly. This is useful for emitting lifecycle events such as "about to create a user" or for tracing and auditing when a function starts. ### Usage[​](#usage "Direct link to Usage") ./samples/with-dispatch-before.ts ``` import { withDispatchBeforeFactory } from "eridu-tech/event-bus/middlewares"; import { use } from "eridu-tech/middleware"; import { eventBusResolver } from "./event-bus.js"; const withDispatchBefore = withDispatchBeforeFactory(eventBusResolver); const createUser = async (userId: string): Promise => { // ... create the user return `user-${userId}`; }; // Wrap with a "before" dispatch using the default adapter (`memory`) const wrappedCreateUser = use( createUser, withDispatchBefore({ type: "user.before.create", payload: ({ args }) => ({ userId: args[0] }), }), ); // Wrap with a "before" dispatch using a specific adapter (`redis`) const wrappedCreateUserOnRedis = use( createUser, withDispatchBefore.use("redis")({ type: "user.before.create", payload: ({ args }) => ({ userId: args[0] }), }), ); await wrappedCreateUser("123"); // The "user.before.create" event is dispatched before createUser runs ``` Use `withDispatchBefore()` to dispatch through the resolver's default adapter (`memory` in this example), or `withDispatchBefore.use("redis")({ ... })` to select a specific adapter. info Here is a complete list of settings for the [`withDispatchBefore`](https://eridu-tech.github.io/eridu-tech-core/types/EventBus.WithDispatchBeforeSettings.html) function. ### Settings[​](#settings "Direct link to Settings") | Option | Type | Description | | --------- | ------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------- | | `type` | `TEventName` | The event name to dispatch when the wrapped function is invoked | | `payload` | `Invocable<[settings: WithDispatchBeforePayloadSettings], TEventMap[TEventName] \| void>` | An invocable that produces the event payload from the wrapped function's arguments. Returning `null` skips the dispatch | ## withDispatchAfterFactory middleware[​](#with_dispatch_after_factory_middleware "Direct link to withDispatchAfterFactory middleware") The after-dispatch middleware emits an event **after** the wrapped function resolves. When the wrapped function completes, the middleware resolves the event payload from the function's arguments and its return value, dispatches the configured event, and then returns the wrapped function's result unchanged. If the payload resolves to `null`, no event is dispatched and the wrapped function's result is returned unchanged. This is useful for emitting completion events such as "user created" or for recording the outcome of a function. ### Usage[​](#usage-1 "Direct link to Usage") ./samples/with-dispatch-after.ts ``` import { withDispatchAfterFactory } from "eridu-tech/event-bus/middlewares"; import { use } from "eridu-tech/middleware"; import { eventBusResolver } from "./event-bus.js"; const withDispatchAfter = withDispatchAfterFactory(eventBusResolver); const createUser = async (userId: string): Promise => { // ... create the user return `user-${userId}`; }; // Wrap with an "after" dispatch using the default adapter (`memory`) const wrappedCreateUser = use( createUser, withDispatchAfter({ type: "user.after.create", payload: ({ args, returnValue }) => ({ userId: args[0], name: returnValue, }), }), ); // Wrap with an "after" dispatch using a specific adapter (`redis`) const wrappedCreateUserOnRedis = use( createUser, withDispatchAfter.use("redis")({ type: "user.after.create", payload: ({ args, returnValue }) => ({ userId: args[0], name: returnValue, }), }), ); const name = await wrappedCreateUser("123"); // The "user.after.create" event is dispatched after createUser resolves, // with the return value included in the payload ``` Use `withDispatchAfter()` to dispatch through the resolver's default adapter (`memory` in this example), or `withDispatchAfter.use("redis")({ ... })` to select a specific adapter. info Here is a complete list of settings for the [`withDispatchAfter`](https://eridu-tech.github.io/eridu-tech-core/types/EventBus.WithDispatchAfterSettings.html) function. ### Settings[​](#settings-1 "Direct link to Settings") | Option | Type | Description | | --------- | -------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | `type` | `TEventName` | The event name to dispatch after the wrapped function resolves | | `payload` | `Invocable<[settings: WithDispatchAfterPayloadSettings], TEventMap[TEventName] \| void>` | An invocable that produces the event payload from the wrapped function's arguments and its return value. Returning `null` skips the dispatch | ## withDispatchOnErrorFactory middleware[​](#with_dispatch_on_error_factory_middleware "Direct link to withDispatchOnErrorFactory middleware") The on-error middleware emits an event when the wrapped function **throws**. When the wrapped function fails, the middleware resolves the event payload from the function's arguments and the caught error, dispatches the configured event, and then re-throws the original error so the failure still propagates to the caller. If the payload resolves to `null`, no event is dispatched and the original error is re-thrown as-is. This is useful for emitting failure events such as "user creation failed" or for feeding an error-monitoring pipeline without swallowing the exception. ### Usage[​](#usage-2 "Direct link to Usage") ./samples/with-dispatch-on-error.ts ``` import { withDispatchOnErrorFactory } from "eridu-tech/event-bus/middlewares"; import { use } from "eridu-tech/middleware"; import { eventBusResolver } from "./event-bus.js"; const withDispatchOnError = withDispatchOnErrorFactory(eventBusResolver); const createUser = async (userId: string): Promise => { // ... create the user throw new Error("boom"); }; // Wrap with an error dispatch using the default adapter (`memory`) const wrappedCreateUser = use( createUser, withDispatchOnError({ type: "user.error", payload: ({ args, error }) => ({ userId: args[0], error, }), }), ); // Wrap with an error dispatch using a specific adapter (`redis`) const wrappedCreateUserOnRedis = use( createUser, withDispatchOnError.use("redis")({ type: "user.error", payload: ({ args, error }) => ({ userId: args[0], error, }), }), ); try { await wrappedCreateUser("123"); } catch (error) { // The "user.error" event is dispatched with the caught error, // then the original error is re-thrown } ``` Use `withDispatchOnError()` to dispatch through the resolver's default adapter (`memory` in this example), or `withDispatchOnError.use("redis")({ ... })` to select a specific adapter. info Here is a complete list of settings for the [`withDispatchOnError`](https://eridu-tech.github.io/eridu-tech-core/types/EventBus.WithDispatchOnErrorSettings.html) function. ### Settings[​](#settings-2 "Direct link to Settings") | Option | Type | Description | | --------- | ------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | `type` | `TEventName` | The event name to dispatch when the wrapped function throws | | `payload` | `Invocable<[settings: WithDispatchOnErrorPayloadSettings], TEventMap[TEventName] \| void>` | An invocable that produces the event payload from the wrapped function's arguments and the caught error. Returning `null` skips the dispatch | ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/event-bus`](https://eridu-tech.github.io/eridu-tech-core/modules/EventBus.html) API docs. --- # EventBus Plugins ## withEventBusPrefix plugin[​](#witheventbusprefix-plugin "Direct link to withEventBusPrefix plugin") The EventBus prefix plugin intercepts calls to an event bus adapter and transparently prefixes all event names with a configurable string. This enables logical event namespace isolation without modifying the adapter implementation. ### Use cases[​](#use-cases "Direct link to Use cases") * **Multi-tenant systems** — Prefix event names with a tenant identifier to isolate events between tenants * **Environment isolation** — Separate development, staging, and production event streams * **Module scoping** — Organize events by feature or module to avoid naming collisions ### How it works[​](#how-it-works "Direct link to How it works") The `withEventBusPrefix` function returns a [`PluginFn`](/docs/foundation/middleware/.md) that calls `enhance` on each adapter method that accepts an event name. When an enhanced method is invoked, the plugin intercepts the call, prepends the configured prefix to the event name argument, and forwards the modified arguments to the original method. The plugin prefixes event names for the following methods: | Method | Event name argument | Pattern | | ---------------- | ------------------- | -------------- | | `dispatch` | First argument | `prefix + key` | | `addListener` | First argument | `prefix + key` | | `removeListener` | First argument | `prefix + key` | ### Usage[​](#usage "Direct link to Usage") ./samples/with-event-bus-prefix.ts ``` import { withPlugin } from "eridu-tech/middleware"; import { MemoryEventBusAdapter } from "eridu-tech/event-bus/memory-event-bus-adapter"; import { withEventBusPrefix } from "eridu-tech/event-bus/plugins"; const adapter = new MemoryEventBusAdapter(); // Apply the prefix plugin to the adapter const prefixedAdapter = withPlugin(adapter, withEventBusPrefix("tenant-42:")); ``` ### Before/after behavior[​](#beforeafter-behavior "Direct link to Before/after behavior") **Before** — Event names are used as-is: ./samples/unprefixed-dispatch.ts ``` import type { BaseEvent } from "eridu-tech/event-bus/contracts"; import { MemoryEventBusAdapter } from "eridu-tech/event-bus/memory-event-bus-adapter"; const adapter = new MemoryEventBusAdapter(); // Event data to dispatch and listener to register const data = { userId: "123" }; const listener = (event: BaseEvent): void => { console.log("Received event:", event); }; await adapter.dispatch("user.created", data); // -> dispatches "user.created" await adapter.addListener("user.created", listener); // -> listens to "user.created" ``` **After** — Event names are automatically prefixed: ./samples/prefixed-dispatch.ts ``` import { withPlugin } from "eridu-tech/middleware"; import { MemoryEventBusAdapter } from "eridu-tech/event-bus/memory-event-bus-adapter"; import { withEventBusPrefix } from "eridu-tech/event-bus/plugins"; import type { BaseEvent } from "eridu-tech/event-bus/contracts"; const adapter = new MemoryEventBusAdapter(); // Apply the prefix plugin to the adapter const prefixedAdapter = withPlugin(adapter, withEventBusPrefix("tenant-42:")); // Event data to dispatch and listener to register const data = { userId: "123" }; const listener = (event: BaseEvent): void => { console.log("Received event:", event); }; await prefixedAdapter.dispatch("user.created", data); // -> dispatches "tenant-42:user.created" await prefixedAdapter.addListener("user.created", listener); // -> listens to "tenant-42:user.created" ``` danger Because `withPlugin` uses `enhance` under the hood, the same edge case applies: if one enhanced method internally calls another enhanced method via `this`, the middleware will apply **twice**. Be mindful of inter-method calls when applying plugins that enhance multiple methods on the same instance. info For more information about the `withPlugin` function and applying plugins to adapters, see the [Middleware plugin](/docs/foundation/middleware/.md#plugin) documentation. ## withListenerTracking plugin[​](#withlistenertracking-plugin "Direct link to withListenerTracking plugin") The `withListenerTracking` plugin wraps another plugin with automatic listener-reference tracking. When a middleware plugin intercepts `addListener` and wraps the listener function, the adapter stores the wrapped reference. If the caller later invokes `removeListener` with the original listener, the adapter cannot find it — the reference has changed. This plugin solves that problem by ensuring that `removeListener` with the original listener correctly resolves through the chain. ### Use cases[​](#use-cases-1 "Direct link to Use cases") * **Listener reference transparency** — Callers can use the original listener function with `removeListener` even when a plugin wraps the listener in `addListener` * **Plugin safety** — Wrap plugins that transform listeners in `addListener` (for example a plugin that wraps the listener to add logging or validation) to ensure `removeListener` still resolves correctly * **Per-plugin tracking** — Apply `withListenerTracking` to each plugin that wraps listeners; it does not automatically handle wrapping from other plugins in the chain info This plugin is only needed if you call `removeListener` at runtime. If you only register listeners during startup and never remove them, listener-reference tracking is unnecessary. ### How it works[​](#how-it-works-1 "Direct link to How it works") `withListenerTracking` wraps the provided plugin and adds tracking layers around `addListener` and `removeListener` on the adapter. When `addListener` is called, the original listener is wrapped with a tracking wrapper and the mapping from original to wrapper is stored in a `ListenerStore` keyed by event name. On `removeListener`, the original listener is resolved back to the tracking wrapper through the store before forwarding the call down the chain. The plugin execution order is: 1. The user plugin's enhancements are applied first (inner layer) 2. The tracking enhancements are applied second (outermost layer) ### Usage[​](#usage-1 "Direct link to Usage") ./samples/with-listener-tracking.ts ``` import { withPlugin } from "eridu-tech/middleware"; import { MemoryEventBusAdapter } from "eridu-tech/event-bus/memory-event-bus-adapter"; import { withListenerTracking } from "eridu-tech/event-bus/plugins"; import type { PluginFn } from "eridu-tech/middleware/contracts"; import type { IEventBusAdapter } from "eridu-tech/event-bus/contracts"; const adapter = new MemoryEventBusAdapter(); // A plugin that wraps listeners, e.g. to add logging or validation const loggingPlugin: PluginFn = (instance, enhance) => { enhance( instance, "addListener", ({ args: [eventName, listener], next }) => { return next([ eventName, (event) => { console.log(`Received "${eventName}"`); return listener(event); }, ]); }, ); }; // Apply listener tracking around a plugin that wraps listeners const enhancedAdapter = withPlugin( adapter, withListenerTracking(loggingPlugin), ); ``` #### Chaining multiple tracking calls[​](#chaining-multiple-tracking-calls "Direct link to Chaining multiple tracking calls") Multiple `withListenerTracking` calls can be composed together: ./samples/listener-tracking-chaining.ts ``` import { withPlugin } from "eridu-tech/middleware"; import { MemoryEventBusAdapter } from "eridu-tech/event-bus/memory-event-bus-adapter"; import { withListenerTracking } from "eridu-tech/event-bus/plugins"; import type { PluginFn } from "eridu-tech/middleware/contracts"; import type { IEventBusAdapter } from "eridu-tech/event-bus/contracts"; const adapter = new MemoryEventBusAdapter(); // Plugin A: wraps listeners, e.g. to add logging const pluginA: PluginFn = (instance, enhance) => { enhance( instance, "addListener", ({ args: [eventName, listener], next }) => { return next([ eventName, (event) => { console.log(`[A] Received "${eventName}"`); return listener(event); }, ]); }, ); }; // Plugin B: another plugin that wraps listeners, e.g. to add validation const pluginB: PluginFn = (instance, enhance) => { enhance( instance, "addListener", ({ args: [eventName, listener], next }) => { return next([ eventName, (event) => { console.log(`[B] Received "${eventName}"`); return listener(event); }, ]); }, ); }; // Compose multiple tracking-wrapped plugins const enhancedAdapter = withPlugin(adapter, [ withListenerTracking(pluginA), withListenerTracking(pluginB), ]); ``` danger Because `withPlugin` uses `enhance` under the hood, the same edge case applies: if one enhanced method internally calls another enhanced method via `this`, the middleware will apply **twice**. Be mindful of inter-method calls when applying plugins that enhance multiple methods on the same instance. info For more information about the `withPlugin` function and applying plugins to adapters, see the [Middleware plugin](/docs/foundation/middleware/.md#plugin) documentation. --- # EventBusResolver The `EventBusResolver` class creates an event bus for each configured adapter. Use it to dispatch through a default adapter or select a specific adapter at runtime. ## Initial configuration[​](#initial-configuration "Direct link to Initial configuration") Configure the named adapters in the constructor, then call `init()` before registering listeners or selecting an event bus. Initialization creates an event bus for each configured adapter. ./samples/event-bus-resolver-initial-config.ts ``` import { EventBusResolver } from "eridu-tech/event-bus"; import { RedisPubSubEventBusAdapter } from "eridu-tech/event-bus/redis-pub-sub-event-bus-adapter"; import { MemoryEventBusAdapter } from "eridu-tech/event-bus/memory-event-bus-adapter"; import { SuperJsonSerde } from "eridu-tech/serde/super-json-serde"; import { Redis } from "ioredis"; import { z } from "zod"; type EventMap = { add: { a: 1; b: 2; }; sub: { c: number; d: number; }; }; const serde = new SuperJsonSerde(); export const eventBusResolver = new EventBusResolver< "memory" | "redis", EventMap >({ eventMapSchema: { add: z.object({ a: z.literal(1), b: z.literal(2), }), sub: z.object({ c: z.number(), d: z.number(), }), }, adapters: { memory: new MemoryEventBusAdapter(), redis: new RedisPubSubEventBusAdapter({ client: new Redis("YOUR_REDIS_CONNECTION_STRING"), serde, }), }, // You can set an optional default adapter defaultAdapter: "memory", }); await eventBusResolver.init(); ``` ## Usage[​](#usage "Direct link to Usage") ### Registering listeners[​](#registering-listeners "Direct link to Registering listeners") Register listeners during application setup. The resolver adds each listener to every configured adapter, so it can handle events regardless of which adapter is used to dispatch them. ### 1. Using the default adapter[​](#1-using-the-default-adapter "Direct link to 1. Using the default adapter") ./samples/event-bus-resolver-default-adapter.ts ``` import { eventBusResolver } from "./event-bus-resolver-initial-config.js"; await eventBusResolver.use().dispatch("add", { a: 1, b: 2 }); ``` danger If no default adapter is configured and none is specified in `use()`, an error is thrown. ### 2. Specifying an adapter explicitly[​](#2-specifying-an-adapter-explicitly "Direct link to 2. Specifying an adapter explicitly") ./samples/event-bus-resolver-specific-adapter.ts ``` import { eventBusResolver } from "./event-bus-resolver-initial-config.js"; await eventBusResolver.use("redis").dispatch("add", { a: 1, b: 2 }); ``` danger If you specify an adapter that is not configured, an error is thrown. Call `init()` before `use()`; otherwise, no configured adapter is available yet. ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/event-bus`](https://eridu-tech.github.io/eridu-tech-core/modules/EventBus.html) API docs. --- # EventBus usage The `eridu-tech/event-bus` component provides a way for dispatching and listening to events independent of underlying technology. ## Initial configuration[​](#initial-configuration "Direct link to Initial configuration") To begin using the `EventBus` class, you'll need to create and configure an instance: ./samples/event-bus-initial-config.ts ``` import { MemoryEventBusAdapter } from "eridu-tech/event-bus/memory-event-bus-adapter"; import type { IEventBus } from "eridu-tech/event-bus/contracts"; import { EventBus } from "eridu-tech/event-bus"; export const eventBus: IEventBus = new EventBus({ // You can choose the adapter to use adapter: new MemoryEventBusAdapter(), }); ``` info Here is a complete list of settings for the [`EventBus`](https://eridu-tech.github.io/eridu-tech-core/types/EventBus.EventBusSettingsBase.html) class. ## Event handling basics[​](#event-handling-basics "Direct link to Event handling basics") ### Registering Listeners and Dispatching Events[​](#registering-listeners-and-dispatching-events "Direct link to Registering Listeners and Dispatching Events") Event listeners can be added to respond to specific events: ./samples/event-bus-listeners.ts ``` import { eventBus } from "./event-bus-initial-config.js"; await eventBus.addListener("add", (event) => { console.log(event); }); await eventBus.dispatch("add", { a: 5, b: 5, }); ``` ### Listener management[​](#listener-management "Direct link to Listener management") To properly remove a listener, you must use a named function: ./samples/event-bus-listener-management.ts ``` import { eventBus } from "./event-bus-initial-config.js"; import type { BaseEvent } from "eridu-tech/event-bus/contracts"; const listener = (event: BaseEvent) => { console.log(event); }; await eventBus.addListener("add", listener); await eventBus.removeListener("add", listener); // The listener is removed before dispatch and won't be triggered. await eventBus.dispatch("add", { a: 5, b: 5, }); ``` ## Patterns[​](#patterns "Direct link to Patterns") ### Compile time type safety[​](#compile-time-type-safety "Direct link to Compile time type safety") An event map can be used to strictly type the events: ./samples/event-bus-type-safety.ts ``` import { MemoryEventBusAdapter } from "eridu-tech/event-bus/memory-event-bus-adapter"; import { EventBus } from "eridu-tech/event-bus"; type AddEvent = { a: number; b: number; }; type EventMap = { add: AddEvent; }; const eventBus = new EventBus({ adapter: new MemoryEventBusAdapter(), }); // A typescript error will show up because the event name doesnt exist. await eventBus.dispatch("addd", { a: 2, b: 2, }); // A typescript error will show up because the event fields doesnt match await eventBus.dispatch("add", { nbr1: 1, nbr2: 2, }); // A typescript error will show up because the event name doesnt exist. await eventBus.addListener("addd", (event) => { console.log(event); }); ``` ### Runtime type safety[​](#runtime-type-safety "Direct link to Runtime type safety") You can validate event data against standard-schema-compliant schemas by providing the `eventMapSchema` setting on the `EventBus`. This works with any library that implements the `StandardSchemaV1` specification, such as Zod, ArkType and Valibot. When a schema map is provided, event data is validated: * **On dispatch** — event data is validated against the schema for the event name before it is dispatched. * **On listener delivery** — when `shouldValidateListeners` is `true` (the default), event data is validated before it is delivered to listeners. This ensures listeners only receive data that conforms to the schema. If no schema is defined for a particular event name, that event is passed through without validation. If validation fails, a `ValidationError` is thrown. ./samples/event-bus-runtime-validation.ts ``` import { MemoryEventBusAdapter } from "eridu-tech/event-bus/memory-event-bus-adapter"; import { EventBus } from "eridu-tech/event-bus"; import { z } from "zod"; type UserCreatedEvent = { userId: string; name: string; }; // The value type stays permissive so schema validation is what enforces the // full shape at runtime (compile-time safety is shown in event_bus_type_safety). type EventMap = { "user.created": UserCreatedEvent; }; const eventBus = new EventBus({ adapter: new MemoryEventBusAdapter(), eventMapSchema: { "user.created": z.object({ userId: z.string(), name: z.string(), }), }, }); await eventBus.dispatch("user.created", { userId: "123", name: "John", }); // Throws a ValidationError because userId is missing await eventBus.dispatch("user.created", { name: "Jane", }); ``` #### Disabling listener validation[​](#disabling-listener-validation "Direct link to Disabling listener validation") If you only want to validate event data on dispatch and skip validation when delivering to listeners, set `shouldValidateListeners` to `false`: ./samples/event-bus-disable-listener-validation.ts ``` import { EventBus } from "eridu-tech/event-bus"; import { MemoryEventBusAdapter } from "eridu-tech/event-bus/memory-event-bus-adapter"; import { z } from "zod"; type UserCreatedEvent = { userId: string; name: string; }; type EventMap = { "user.created": UserCreatedEvent; }; const eventBus = new EventBus({ adapter: new MemoryEventBusAdapter(), eventMapSchema: { "user.created": z.object({ userId: z.string(), name: z.string(), }), }, shouldValidateListeners: false, }); ``` ### Subscribe method[​](#subscribe-method "Direct link to Subscribe method") The subscription pattern provides automatic cleanup through an unsubscribe function: ./samples/event-bus-subscribe.ts ``` import { eventBus } from "./event-bus-initial-config.js"; const unsubscribe = await eventBus.subscribe("add", (event) => { console.log(event); }); await eventBus.dispatch("add", { a: 20, b: 5, }); await unsubscribe(); ``` ### One-Time event handling[​](#one-time-event-handling "Direct link to One-Time event handling") For listeners that should only trigger once: ./samples/event-bus-listen-once.ts ``` import { eventBus } from "./event-bus-initial-config.js"; await eventBus.listenOnce("add", (event) => { console.log(event); }); // Listener will be only triggered here await eventBus.dispatch("add", { a: 5, b: 5, }); // Listener will not be triggered because it removed after the first dispatch. await eventBus.dispatch("add", { a: 3, b: 3, }); ``` You can also cancel one-time listeners before they trigger: ./samples/event-bus-cancel-listen-once.ts ``` import { eventBus } from "./event-bus-initial-config.js"; import type { BaseEvent } from "eridu-tech/event-bus/contracts"; const listener = (event: BaseEvent) => { console.log(event); }; await eventBus.listenOnce("add", listener); await eventBus.removeListener("add", listener); // The listener is removed before dispatch and won't be triggered. await eventBus.dispatch("add", { a: 5, b: 5, }); ``` The `subscribeOnce` method creates a one-time listener and returns an unsubscribe function: ./samples/event-bus-subscribe-once.ts ``` import { eventBus } from "./event-bus-initial-config.js"; const unsubscribe = await eventBus.subscribeOnce("add", (event) => { console.log(event); }); await unsubscribe(); await eventBus.dispatch("add", { a: 5, b: 5, }); ``` ### Promise-based event handling[​](#promise-based-event-handling "Direct link to Promise-based event handling") Wait for events using promises: ./samples/event-bus-as-promise.ts ``` import { eventBus } from "./event-bus-initial-config.js"; import { delay } from "eridu-tech/utilities"; import { TimeSpan } from "eridu-tech/time-span"; // Register the promise before dispatching the event. const eventPromise = eventBus.asPromise("add"); await delay(TimeSpan.fromSeconds(1)); await eventBus.dispatch("add", { a: 30, b: 20, }); const event = await eventPromise; ``` ### Listening to multiple events[​](#listening-to-multiple-events "Direct link to Listening to multiple events") The `addListener`, `removeListener`, and `subscribe` methods all accept either a single event name or an array of event names, allowing you to register one listener for multiple events at once: ./samples/event-bus-multi-events.ts ``` import { EventBus } from "eridu-tech/event-bus"; import { MemoryEventBusAdapter } from "eridu-tech/event-bus/memory-event-bus-adapter"; type AddEvent = { a: number; b: number; }; type RemoveEvent = { id: number; }; type EventMap = { add: AddEvent; remove: RemoveEvent; }; const eventBus = new EventBus({ adapter: new MemoryEventBusAdapter(), }); // The same listener handles both "add" and "remove" events await eventBus.addListener(["add", "remove"], (event) => { console.log("EVENT:", event); // event.type will be "add" or "remove" depending on which was dispatched }); await eventBus.dispatch("add", { a: 1, b: 2 }); await eventBus.dispatch("remove", { id: 42 }); ``` You can also use `subscribe` to get a single cleanup function that unsubscribes from all listed events at once: ./samples/event-bus-subscribe-multi.ts ``` import { eventBus } from "./event-bus-initial-config.js"; const unsubscribe = await eventBus.subscribe(["add", "remove"], (event) => { console.log("EVENT:", event); }); await eventBus.dispatch("add", { a: 1, b: 2 }); await eventBus.dispatch("remove", { id: 42 }); // Unsubscribes from both "add" and "remove" in one call await unsubscribe(); ``` ### Separating dispatching and listening[​](#separating-dispatching-and-listening "Direct link to Separating dispatching and listening") The library includes two additional contracts: * [`IEventDispatcher`](https://eridu-tech.github.io/eridu-tech-core/types/EventBus.IEventDispatcher.html) - Allows only for event dispatching. * [`IEventListenable`](https://eridu-tech.github.io/eridu-tech-core/types/EventBus.IEventListenable.html) - Allows only for event listening. This separation makes it easy to visually distinguish the two contracts, making it immediately obvious that they serve different purposes. ./samples/event-bus-contracts.ts ``` import type { IEventListenable, IEventDispatcher, } from "eridu-tech/event-bus/contracts"; import { MemoryEventBusAdapter } from "eridu-tech/event-bus/memory-event-bus-adapter"; import { EventBus } from "eridu-tech/event-bus"; type AddEvent = { a: number; b: number; }; type EventMap = { add: AddEvent; }; async function listenerFunc( eventListenable: IEventListenable, ): Promise { // You cannot access the dispatch method // You will get typescript error if you try await eventListenable.addListener("add", (event) => { console.log("EVENT:", event); }); } async function dispatchingFunc( eventDispatcher: IEventDispatcher, ): Promise { // You cannot access the listener methods // You will get typescript error if you try await eventDispatcher.dispatch("add", { a: 20, b: 5, }); } const eventBus = new EventBus({ // You can choose the adapter to use adapter: new MemoryEventBusAdapter(), }); await listenerFunc(eventBus); await dispatchingFunc(eventBus); ``` ### Invocable listeners[​](#invocable-listeners "Direct link to Invocable listeners") An event listener is `Invocable` meaning you can also pass in an object (class instance or object literal) as listener: info For further information refer the [`Invocable`](/docs/utilities/invocable/.md) docs. ./samples/event-bus-invocable-listener.ts ``` import { EventBus } from "eridu-tech/event-bus"; import { MemoryEventBusAdapter } from "eridu-tech/event-bus/memory-event-bus-adapter"; import type { IEventListenerObject } from "eridu-tech/event-bus/contracts"; type AddEvent = { a: number; b: number; }; type EventMap = { add: AddEvent; }; class Listener implements IEventListenerObject { private count = 0; invoke(event: AddEvent): void { console.log("EVENT:", event); console.log("COUNT:", this.count); this.count++; } } const eventBus = new EventBus({ adapter: new MemoryEventBusAdapter(), }); await eventBus.addListener("add", new Listener()); await eventBus.dispatch("add", { a: 1, b: 2, }); await eventBus.dispatch("add", { a: 3, b: -1, }); ``` ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/event-bus`](https://eridu-tech.github.io/eridu-tech-core/modules/EventBus.html) API docs. --- # DI Integration The `ProxyEventBusResolver` class provides an [`IEventBusResolver`](/docs/messaging/event_bus/event_bus_resolver/.md) whose underlying resolver is resolved from a [DI container](/docs/foundation/di/.md). The proxy supports the `IEventBusResolver` operations: * `use(adapterName?)` returns a dispatcher that resolves the resolver from the container for each dispatch. This supports the resolver's configured lifetime; use scoped registrations inside `container.run()`. * `addListener(eventNames, listener)` registers the listener as a container initialization hook. Listeners must be registered before `container.init()`; you cannot add listeners dynamically after initialization. Create the proxy before calling `container.init()`. The resolver is initialized by the container, and listeners registered through the proxy are added during container initialization. In a multi-server deployment, register listeners during startup on each server so each server is configured to receive events. ## Initial configuration[​](#initial-configuration "Direct link to Initial configuration") Set up the memory and Redis adapters, then create the container and register the `EventBusResolver`. Wrap it with `ProxyEventBusResolver` and register listeners before initializing the container: #### 1. Set up the memory event-bus adapter[​](#1-set-up-the-memory-event-bus-adapter "Direct link to 1. Set up the memory event-bus adapter") ./samples/memory-event-bus-adapter.ts ``` import { MemoryEventBusAdapter } from "eridu-tech/event-bus/memory-event-bus-adapter"; export const memoryEventBusAdapter = new MemoryEventBusAdapter(); ``` #### 2. Set up the Redis event-bus adapter[​](#2-set-up-the-redis-event-bus-adapter "Direct link to 2. Set up the Redis event-bus adapter") ./samples/redis-event-bus-adapter.ts ``` import { RedisPubSubEventBusAdapter } from "eridu-tech/event-bus/redis-pub-sub-event-bus-adapter"; import { SuperJsonSerde } from "eridu-tech/serde/super-json-serde"; import { Redis } from "ioredis"; export const redisEventBusAdapter = new RedisPubSubEventBusAdapter({ client: new Redis("YOUR_REDIS_CONNECTION_STRING"), serde: new SuperJsonSerde(), }); ``` #### 3. Create the dependency injection container[​](#3-create-the-dependency-injection-container "Direct link to 3. Create the dependency injection container") ./samples/container.ts ``` import { Container } from "eridu-tech/di"; import { ExecutionContext } from "eridu-tech/execution-context"; import { AlsExecutionContextAdapter } from "eridu-tech/execution-context/als-execution-context-adapter"; export const executionContext = new ExecutionContext( new AlsExecutionContextAdapter(), ); export const container = new Container({ executionContext }); ``` #### 4. Register and initialize the event-bus resolver[​](#4-register-and-initialize-the-event-bus-resolver "Direct link to 4. Register and initialize the event-bus resolver") ./samples/event-bus-resolver-di.ts ``` import { genericToken, LIFETIME } from "eridu-tech/di/contracts"; import { ProxyEventBusResolver } from "eridu-tech/event-bus/di"; import { EventBusResolver } from "eridu-tech/event-bus"; import { container } from "./container.js"; import { memoryEventBusAdapter } from "./memory-event-bus-adapter.js"; import { redisEventBusAdapter } from "./redis-event-bus-adapter.js"; type Adapters = "memory" | "redis"; type EventMap = { userCreated: { userId: string; }; }; const resolverToken = genericToken>("eventBusResolver"); container.registerFactory({ token: resolverToken, factory: () => new EventBusResolver({ adapters: { memory: memoryEventBusAdapter, redis: redisEventBusAdapter, }, defaultAdapter: "memory", }), onInit: async (resolver) => { await resolver.init(); }, deps: {}, lifetime: LIFETIME.SINGLETON, }); // Create the proxy before container.init() export const eventBus = new ProxyEventBusResolver({ container, resolverToken, }); // Register listeners during application setup, before container.init() await eventBus.addListener("userCreated", (event) => { console.log(event.userId); }); await container.init(); ``` ## Usage[​](#usage "Direct link to Usage") ### 1. Using the default adapter[​](#1-using-the-default-adapter "Direct link to 1. Using the default adapter") ./samples/event-bus-resolver-di-default-adapter.ts ``` import { eventBus } from "./event-bus-resolver-di.js"; // Uses the adapter configured as the default (memory) await eventBus.use().dispatch("userCreated", { userId: "user-1" }); ``` ### 2. Specifying an adapter explicitly[​](#2-specifying-an-adapter-explicitly "Direct link to 2. Specifying an adapter explicitly") ./samples/event-bus-resolver-di-specific-adapter.ts ``` import { eventBus } from "./event-bus-resolver-di.js"; await eventBus.use("redis").dispatch("userCreated", { userId: "user-1" }); ``` ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/event-bus`](https://eridu-tech.github.io/eridu-tech-core/modules/EventBus.html) API docs. --- # Messaging Decouple your services with reliable, asynchronous communication. * [Event Bus](/docs/messaging/event_bus/event_bus_usage/.md) — Pub/sub event bus for dispatching and listening to events with pluggable transport backends, independent of underlying technology. ## Existing middlewares[​](#existing-middlewares "Direct link to Existing middlewares") Beyond the components themselves, every component provides middleware factories for composing component behavior around your own functions. Wrap a function with use() or method with enhance() and apply any of the following middlewares. * [withDispatchBeforeFactory](/docs/messaging/event_bus/event_bus_middlewares/.md#with_dispatch_before_factory_middleware) — Dispatches an event before the wrapped function runs. * [withDispatchAfterFactory](/docs/messaging/event_bus/event_bus_middlewares/.md#with_dispatch_after_factory_middleware) — Dispatches an event after the wrapped function resolves. * [withDispatchOnErrorFactory](/docs/messaging/event_bus/event_bus_middlewares/.md#with_dispatch_on_error_factory_middleware) — Dispatches an event when the wrapped function throws. --- # CircuitBreaker provider resolver classes ## pro[​](#pro "Direct link to pro") The `CircuitBreakerFactoryResolver` class provides a flexible way to configure and switch between different circuit-breaker adapters at runtime. ### Initial configuration[​](#initial-configuration "Direct link to Initial configuration") To begin using the `CircuitBreakerFactoryResolver`, You will need to register all required adapters during initialization. ./samples/circuit-breaker-factory-resolver-initial-config.ts ``` import { CircuitBreakerFactoryResolver } from "eridu-tech/circuit-breaker"; import { MemoryCircuitBreakerStorageAdapter } from "eridu-tech/circuit-breaker/memory-circuit-breaker-storage-adapter"; import { DatabaseCircuitBreakerAdapter } from "eridu-tech/circuit-breaker/database-circuit-breaker-adapter"; import { RedisCircuitBreakerAdapter } from "eridu-tech/circuit-breaker/redis-circuit-breaker-adapter"; import { SuperJsonSerde } from "eridu-tech/serde/super-json-serde"; import { Redis } from "ioredis"; const serde = new SuperJsonSerde(); export const circuitBreakerFactoryResolver = new CircuitBreakerFactoryResolver({ serde, adapters: { memory: new DatabaseCircuitBreakerAdapter({ adapter: new MemoryCircuitBreakerStorageAdapter(), }), redis: new RedisCircuitBreakerAdapter({ database: new Redis("YOUR_REDIS_CONNECTION"), }), }, defaultAdapter: "memory", }); await circuitBreakerFactoryResolver.init(); ``` ### Usage[​](#usage "Direct link to Usage") #### 1. Using the default adapter[​](#1-using-the-default-adapter "Direct link to 1. Using the default adapter") ./samples/circuit-breaker-factory-resolver-default-adapter.ts ``` import { circuitBreakerFactoryResolver } from "./circuit-breaker-factory-resolver-initial-config.js"; // Will apply circuit-breaker logic the default adapter which is MemoryCircuitBreakerStorageAdapter await circuitBreakerFactoryResolver .use() .create("a") .runOrFail(async () => { // ... code to apply circuit-breaker logic }); ``` danger Note that if you dont set a default adapter, an error will be thrown. #### 2. Specifying an adapter explicitly[​](#2-specifying-an-adapter-explicitly "Direct link to 2. Specifying an adapter explicitly") ./samples/circuit-breaker-factory-resolver-specific-adapter.ts ``` import { circuitBreakerFactoryResolver } from "./circuit-breaker-factory-resolver-initial-config.js"; // Will apply circuit-breaker logic using the redis adapter await circuitBreakerFactoryResolver .use("redis") .create("a") .runOrFail(async () => { // ... code to apply circuit-breaker logic }); ``` danger Note that if you specify a non-existent adapter, an error will be thrown. ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/circuit-breaker`](https://eridu-tech.github.io/eridu-tech-core/modules/CircuitBreaker.html) API docs. --- # CircuitBreaker middlewares ## Initial configuration[​](#initial-configuration "Direct link to Initial configuration") To begin using the circuit-breaker middlewares, you'll need to create and configure a `CircuitBreakerFactoryResolver` instance: ./samples/circuit-breaker.ts ``` import { CircuitBreakerFactoryResolver } from "eridu-tech/circuit-breaker"; import { DatabaseCircuitBreakerAdapter } from "eridu-tech/circuit-breaker/database-circuit-breaker-adapter"; import { MemoryCircuitBreakerStorageAdapter } from "eridu-tech/circuit-breaker/memory-circuit-breaker-storage-adapter"; import { RedisCircuitBreakerAdapter } from "eridu-tech/circuit-breaker/redis-circuit-breaker-adapter"; import { SuperJsonSerde } from "eridu-tech/serde/super-json-serde"; import { Redis } from "ioredis"; const serde = new SuperJsonSerde(); export const circuitBreakerFactoryResolver = new CircuitBreakerFactoryResolver({ serde, adapters: { memory: new DatabaseCircuitBreakerAdapter({ adapter: new MemoryCircuitBreakerStorageAdapter(), }), redis: new RedisCircuitBreakerAdapter({ database: new Redis("YOUR_REDIS_CONNECTION_STRING"), }), }, defaultAdapter: "memory", }); await circuitBreakerFactoryResolver.init(); ``` ## withCircuitBreakerFactory middleware[​](#with_circuit_breaker_factory_middleware "Direct link to withCircuitBreakerFactory middleware") The CircuitBreaker middleware wraps function calls with a circuit-breaker, providing fault tolerance for distributed systems. Each unique key (derived from the function's arguments) gets its own circuit instance. When the circuit is **open**, the wrapped function is not called and an error is thrown instead, preventing cascading failures. ### Usage[​](#usage "Direct link to Usage") ./samples/with-circuit-breaker.ts ``` import { withCircuitBreakerFactory } from "eridu-tech/circuit-breaker/middlewares"; import { use } from "eridu-tech/middleware"; import { circuitBreakerFactoryResolver } from "./circuit-breaker.js"; const withCircuitBreaker = withCircuitBreakerFactory( circuitBreakerFactoryResolver, ); const callExternalApi = async (endpoint: string): Promise => { const response = await fetch(`https://api.example.com/${endpoint}`); return response.json(); }; // Wrap with circuit-breaker using the default adapter (`memory`) const protectedCall = use( callExternalApi, withCircuitBreaker({ key: ([endpoint]) => `api:${endpoint}`, }), ); // Wrap with circuit-breaker using a specific adapter (`redis`) const protectedCallOnRedis = use( callExternalApi, withCircuitBreaker.use("redis")({ key: ([endpoint]) => `api:${endpoint}`, }), ); await protectedCall("users"); // Succeeds or opens the circuit on repeated failures await protectedCallOnRedis("users"); // Uses the Redis adapter ``` Calling `withCircuitBreaker()` directly uses the resolver's default adapter (`memory`). Use `withCircuitBreaker.use("redis")` to run the wrapped function through a specific registered adapter. info Here is a complete list of settings for the [`withCircuitBreaker`](https://eridu-tech.github.io/eridu-tech-core/types/CircuitBreaker.WithCircuitBreakerSettings.html) function. ### Settings[​](#settings "Direct link to Settings") | Option | Type | Description | | -------------- | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | `key` | `Invocable` | A function that produces a unique identifier for the circuit from the wrapped function's arguments. Each unique key gets its own circuit state | | `errorPolicy` | `ErrorPolicy` | Determines which errors count toward opening the circuit. Defaults to treating all errors as failures | | `trigger` | `CircuitBreakerTrigger` | Optional custom trigger that determines when the circuit should open. Defaults to the built-in trigger | | `slowCallTime` | `ITimeSpan` | Optional duration above which a call is considered slow. Exceeding it counts toward opening the circuit (if configured in the trigger) | ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/circuit-breaker`](https://eridu-tech.github.io/eridu-tech-core/modules/CircuitBreaker.html) API docs. --- # CircuitBreaker Plugins ## withCircuitBreakerPrefix plugin[​](#withcircuitbreakerprefix-plugin "Direct link to withCircuitBreakerPrefix plugin") The CircuitBreaker prefix plugin intercepts calls to a circuit-breaker adapter and transparently prefixes all circuit keys with a configurable string. This enables logical key namespacing without modifying the adapter implementation. ### Use cases[​](#use-cases "Direct link to Use cases") * **Multi-tenant systems** — Prefix circuit keys with a tenant identifier to isolate circuit state between tenants * **Service versioning** — Separate circuit state for different API versions * **Environment isolation** — Keep development, staging, and production circuit state separate * **Region scoping** — Prefix keys with a region identifier in multi-region deployments ### How it works[​](#how-it-works "Direct link to How it works") The `withCircuitBreakerPrefix` function returns a [`PluginFn`](/docs/foundation/middleware/.md) that calls `enhance` on each adapter method that accepts a circuit key. When an enhanced method is invoked, the plugin intercepts the call, prepends the configured prefix to the key argument, and forwards the modified arguments to the original method. The plugin prefixes keys for the following methods: | Method | Key argument | Pattern | | -------------- | ---------------------- | -------------- | | `getState` | First argument (`key`) | `prefix + key` | | `isolate` | First argument (`key`) | `prefix + key` | | `reset` | First argument (`key`) | `prefix + key` | | `trackFailure` | First argument (`key`) | `prefix + key` | | `trackSuccess` | First argument (`key`) | `prefix + key` | | `updateState` | First argument (`key`) | `prefix + key` | ### Usage[​](#usage "Direct link to Usage") ./samples/with-circuit-breaker-prefix.ts ``` import { withPlugin } from "eridu-tech/middleware"; import { MemoryCircuitBreakerStorageAdapter } from "eridu-tech/circuit-breaker/memory-circuit-breaker-storage-adapter"; import { DatabaseCircuitBreakerAdapter } from "eridu-tech/circuit-breaker/database-circuit-breaker-adapter"; import { withCircuitBreakerPrefix } from "eridu-tech/circuit-breaker/plugins"; const adapter = new DatabaseCircuitBreakerAdapter({ adapter: new MemoryCircuitBreakerStorageAdapter(), }); // Apply the prefix plugin to the adapter const prefixedAdapter = withPlugin( adapter, withCircuitBreakerPrefix("service-a:"), ); ``` ### Before/after behavior[​](#beforeafter-behavior "Direct link to Before/after behavior") **Before** — Circuit keys are used as-is: ./samples/unprefixed-circuit-lookup.ts ``` import { MemoryCircuitBreakerStorageAdapter } from "eridu-tech/circuit-breaker/memory-circuit-breaker-storage-adapter"; import { DatabaseCircuitBreakerAdapter } from "eridu-tech/circuit-breaker/database-circuit-breaker-adapter"; const adapter = new DatabaseCircuitBreakerAdapter({ adapter: new MemoryCircuitBreakerStorageAdapter(), }); await adapter.getState("api:users"); // -> looks up circuit "api:users" ``` **After** — Circuit keys are automatically prefixed: ./samples/prefixed-circuit-lookup.ts ``` import { withPlugin } from "eridu-tech/middleware"; import { MemoryCircuitBreakerStorageAdapter } from "eridu-tech/circuit-breaker/memory-circuit-breaker-storage-adapter"; import { DatabaseCircuitBreakerAdapter } from "eridu-tech/circuit-breaker/database-circuit-breaker-adapter"; import { withCircuitBreakerPrefix } from "eridu-tech/circuit-breaker/plugins"; const adapter = new DatabaseCircuitBreakerAdapter({ adapter: new MemoryCircuitBreakerStorageAdapter(), }); // Apply the prefix plugin to the adapter const prefixedAdapter = withPlugin( adapter, withCircuitBreakerPrefix("service-a:"), ); await prefixedAdapter.getState("api:users"); // -> looks up circuit "service-a:api:users" ``` danger Because `withPlugin` uses `enhance` under the hood, the same edge case applies: if one enhanced method internally calls another enhanced method via `this`, the middleware will apply **twice**. Be mindful of inter-method calls when applying plugins that enhance multiple methods on the same instance. info For more information about the `withPlugin` function and applying plugins to adapters, see the [Middleware plugin](/docs/foundation/middleware/.md#plugin) documentation. --- # CircuitBreaker usage The `eridu-tech/circuit-breaker` component provides a way for managing circuit-breaker independent of underlying platform or storage. ## Initial configuration[​](#initial-configuration "Direct link to Initial configuration") To begin using the `CircuitBreakerFactory` class, you'll need to create and configure an instance: ./samples/circuit-breaker-initial-config.ts ``` import { MemoryCircuitBreakerStorageAdapter } from "eridu-tech/circuit-breaker/memory-circuit-breaker-storage-adapter"; import { DatabaseCircuitBreakerAdapter } from "eridu-tech/circuit-breaker/database-circuit-breaker-adapter"; import { CircuitBreakerFactory } from "eridu-tech/circuit-breaker"; export const circuitBreakerFactory = new CircuitBreakerFactory({ // You can provide default settings // You can choose the adapter to use adapter: new DatabaseCircuitBreakerAdapter({ adapter: new MemoryCircuitBreakerStorageAdapter(), }), }); await circuitBreakerFactory.init(); ``` info Here is a complete list of settings for the [`CircuitBreakerFactory`](https://eridu-tech.github.io/eridu-tech-core/types/CircuitBreaker.CircuitBreakerFactorySettingsBase.html) class. ## CircuitBreaker basics[​](#circuitbreaker-basics "Direct link to CircuitBreaker basics") ### Creating a circuit-breaker[​](#creating-a-circuit-breaker "Direct link to Creating a circuit-breaker") ./samples/circuit-breaker-create.ts ``` import { circuitBreakerFactory } from "./circuit-breaker-initial-config.js"; export const circuitBreaker = circuitBreakerFactory.create("resource"); ``` ### Using the circuit-breaker[​](#using-the-circuit-breaker "Direct link to Using the circuit-breaker") ./samples/circuit-breaker-run-or-fail.ts ``` import { circuitBreaker } from "./circuit-breaker-create.js"; // The function will only be called when the circuit-breaker is in closed state or half open state. await circuitBreaker.runOrFail(async () => { // Call the external service }); ``` info Note the method throws an error when the circuit-breaker is in open state or isolated state. info You can provide synchronous or asynchronous [`Invocable<[], TValue | Promise>`](/docs/utilities/invocable/.md) as values for the `runOrFail` method. ### Applying circuit-breaker on certiain errors[​](#applying-circuit-breaker-on-certiain-errors "Direct link to Applying circuit-breaker on certiain errors") ./samples/circuit-breaker-error-policy.ts ``` import { circuitBreakerFactory } from "./circuit-breaker-initial-config.js"; class ErrorA extends Error {} const circuitBreaker = circuitBreakerFactory.create("resource", { errorPolicy: ErrorA, }); await circuitBreaker.runOrFail(async () => { // Call the external service }); ``` ### Setting circuit-breaker triggers[​](#setting-circuit-breaker-triggers "Direct link to Setting circuit-breaker triggers") By default the the circuit-breaker will treat errors and slow calls as failures. You can explicitly set ths option. The `CIRCUIT_BREAKER_TRIGGER.BOTH` will treat error and slow calls as failures. ./samples/circuit-breaker-trigger-both.ts ``` import { circuitBreakerFactory } from "./circuit-breaker-initial-config.js"; import { CIRCUIT_BREAKER_TRIGGER } from "eridu-tech/circuit-breaker/contracts"; const circuitBreaker = circuitBreakerFactory.create("resource", { trigger: CIRCUIT_BREAKER_TRIGGER.BOTH, }); await circuitBreaker.runOrFail(async () => { // Call the external service }); ``` The `CIRCUIT_BREAKER_TRIGGER.ONLY_ERROR` will treat only errors as failures. ./samples/circuit-breaker-trigger-only-error.ts ``` import { circuitBreakerFactory } from "./circuit-breaker-initial-config.js"; import { CIRCUIT_BREAKER_TRIGGER } from "eridu-tech/circuit-breaker/contracts"; const circuitBreaker = circuitBreakerFactory.create("resource", { trigger: CIRCUIT_BREAKER_TRIGGER.ONLY_ERROR, }); await circuitBreaker.runOrFail(async () => { // Call the external service }); ``` The `CIRCUIT_BREAKER_TRIGGER.ONLY_SLOW_CALL` will treat slow calls as failures. ./samples/circuit-breaker-trigger-only-slow-call.ts ``` import { circuitBreakerFactory } from "./circuit-breaker-initial-config.js"; import { CIRCUIT_BREAKER_TRIGGER } from "eridu-tech/circuit-breaker/contracts"; const circuitBreaker = circuitBreakerFactory.create("resource", { trigger: CIRCUIT_BREAKER_TRIGGER.ONLY_SLOW_CALL, }); await circuitBreaker.runOrFail(async () => { // Call the external service }); ``` ### Setting the slow call threshold[​](#setting-the-slow-call-threshold "Direct link to Setting the slow call threshold") You can set custom slow call threshold that will be used when treating slow calls as failures. ./samples/circuit-breaker-slow-call-threshold.ts ``` import { circuitBreakerFactory } from "./circuit-breaker-initial-config.js"; import { TimeSpan } from "eridu-tech/time-span"; const circuitBreaker = circuitBreakerFactory.create("resource", { slowCallTime: TimeSpan.fromSeconds(1), }); await circuitBreaker.runOrFail(async () => { // Call the external service }); ``` ### Tracking circuit-breaker metrics in the background[​](#tracking-circuit-breaker-metrics-in-the-background "Direct link to Tracking circuit-breaker metrics in the background") By default tracking is awaited, so `runOrFail` only resolves after the metric is recorded. Set `enableAsyncTracking` to `true` to record metrics in the background instead, without blocking the wrapped function. ./samples/circuit-breaker-enable-async-tracking.ts ``` import { MemoryCircuitBreakerStorageAdapter } from "eridu-tech/circuit-breaker/memory-circuit-breaker-storage-adapter"; import { DatabaseCircuitBreakerAdapter } from "eridu-tech/circuit-breaker/database-circuit-breaker-adapter"; import { CircuitBreakerFactory } from "eridu-tech/circuit-breaker"; const circuitBreakerFactory = new CircuitBreakerFactory({ adapter: new DatabaseCircuitBreakerAdapter({ adapter: new MemoryCircuitBreakerStorageAdapter(), }), // Metric tracking will run in the background enableAsyncTracking: true, }); ``` info The trade-off: the circuit-breaker may open slightly later, since a failure may not be recorded before the next call. Keep it disabled when you need deterministic state transitions, for example in tests. warning On serverless platforms (Cloudflare Workers, Vercel Functions) background promises are cancelled after the response. Pass the platform's `waitUntil` to `CircuitBreakerFactory` to keep them alive. ### Reseting the circuit-breaker[​](#reseting-the-circuit-breaker "Direct link to Reseting the circuit-breaker") You can reset circuit-breaker state to the closed state manually. ./samples/circuit-breaker-reset.ts ``` import { circuitBreaker } from "./circuit-breaker-create.js"; await circuitBreaker.reset(); ``` ### Isolating the circuit-breaker[​](#isolating-the-circuit-breaker "Direct link to Isolating the circuit-breaker") You can manually hold circuit-breaker in open state until reseted. ./samples/circuit-breaker-isolate.ts ``` import { circuitBreaker } from "./circuit-breaker-create.js"; await circuitBreaker.isolate(); ``` ### Checking circuit-breaker state[​](#checking-circuit-breaker-state "Direct link to Checking circuit-breaker state") You can get the circuit-breaker state by using the `getState` method, it returns [`CircuitBreakerState`](https://eridu-tech.github.io/eridu-tech-core/types/CircuitBreaker.CircuitBreakerState.html). ./samples/circuit-breaker-get-state.ts ``` import { circuitBreaker } from "./circuit-breaker-create.js"; import { CIRCUIT_BREAKER_STATE } from "eridu-tech/circuit-breaker/contracts"; const state = await circuitBreaker.getState(); if (state === CIRCUIT_BREAKER_STATE.CLOSED) { console.log("The service is up and running without problems"); } if (state === CIRCUIT_BREAKER_STATE.OPEN) { console.log("The service is down or degraded and you need to wait"); } if (state === CIRCUIT_BREAKER_STATE.HALF_OPEN) { console.log( "Proping to check if the server is up and running or down / degraded", ); } if (state === CIRCUIT_BREAKER_STATE.ISOLATED) { console.log("The service is held in open state manually until reseted"); } ``` ### CircuitBreaker instance variables[​](#circuitbreaker-instance-variables "Direct link to CircuitBreaker instance variables") The `CircuitBreaker` class exposes instance variables such as: ./samples/circuit-breaker-instance-variables.ts ``` import { circuitBreakerFactory } from "./circuit-breaker-initial-config.js"; const circuitBreaker = circuitBreakerFactory.create("resource"); // Will return the key of the circuit-breaker which is "resource" console.log(circuitBreaker); ``` ## Patterns[​](#patterns "Direct link to Patterns") ### Serialization and deserialization of circuit-breakers[​](#serialization-and-deserialization-of-circuit-breakers "Direct link to Serialization and deserialization of circuit-breakers") circuit-breakers can be serialized, allowing them to be transmitted over the network to another server and later deserialized for reuse. This means you can, for example, acquire the circuit-breaker on the main server, transfer it to a queue worker server, and release it there. In order to serialize or deserialize a circuit-breaker you need pass an object that implements [`ISerderRegister`](/docs/foundation/serde/.md) contract like the [`Serde`](/docs/foundation/serde/.md) class to `CircuitBreakerFactory`. Manually serializing and deserializing the circuit-breaker: ./samples/circuit-breaker-manual-serialization.ts ``` import { Redis } from "ioredis"; import { RedisCircuitBreakerAdapter } from "eridu-tech/circuit-breaker/redis-circuit-breaker-adapter"; import { CircuitBreakerFactory } from "eridu-tech/circuit-breaker"; import { SuperJsonSerde } from "eridu-tech/serde/super-json-serde"; const serde = new SuperJsonSerde(); const redisClient = new Redis("YOUR_REDIS_CONNECTION"); const circuitBreakerFactory = new CircuitBreakerFactory({ // You can laso pass in an array of Serde class instances serde, adapter: new RedisCircuitBreakerAdapter({ database: redisClient }), }); const circuitBreaker = circuitBreakerFactory.create("resource"); const serializedCircuitBreaker = await serde.serialize(circuitBreaker); const deserializedCircuitBreaker = await serde.deserialize( serializedCircuitBreaker, ); ``` danger When serializing or deserializing a circuit-breaker, you must use the same `Serde` instances that were provided to the `CircuitBreakerFactory`. This is required because the `CircuitBreakerFactory` injects custom serialization logic for `ICircuitBreaker` instance into `Serde` instances. info Note you only need manuall serialization and deserialization when integrating with external libraries. As long you pass the same `Serde` instances with all other components you dont need to serialize and deserialize the circuit-breaker manually. ./samples/circuit-breaker-event-bus-serialization.ts ``` import { Redis } from "ioredis"; import { RedisCircuitBreakerAdapter } from "eridu-tech/circuit-breaker/redis-circuit-breaker-adapter"; import type { ICircuitBreaker } from "eridu-tech/circuit-breaker/contracts"; import { CircuitBreakerFactory } from "eridu-tech/circuit-breaker"; import { RedisPubSubEventBusAdapter } from "eridu-tech/event-bus/redis-pub-sub-event-bus-adapter"; import { EventBus } from "eridu-tech/event-bus"; import { SuperJsonSerde } from "eridu-tech/serde/super-json-serde"; const serde = new SuperJsonSerde(); const redis = new Redis("YOUR_REDIS_CONNECTION"); type EventMap = { "sending-circuit-breaker-over-network": { circuitBreaker: ICircuitBreaker; }; }; const eventBus = new EventBus({ adapter: new RedisPubSubEventBusAdapter({ client: redis, serde, }), }); const circuitBreakerFactory = new CircuitBreakerFactory({ serde, adapter: new RedisCircuitBreakerAdapter({ database: redis }), }); const circuitBreaker = circuitBreakerFactory.create("resource"); // We are sending the circuitBreaker over the network to other servers. await eventBus.dispatch("sending-circuit-breaker-over-network", { circuitBreaker, }); // The other servers will recieve the serialized circuitBreaker and automattically deserialize it. await eventBus.addListener( "sending-circuit-breaker-over-network", ({ circuitBreaker }) => { // The circuitBreaker is deserialized and can be used console.log("CIRCUIT_BREAKER:", circuitBreaker); }, ); ``` ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/circuit-breaker`](https://eridu-tech.github.io/eridu-tech-core/modules/CircuitBreaker.html) API docs. --- # DI Integration The `circuitBreakerFactoryResolverDiFactory` function creates an [`ICircuitBreakerFactoryResolver`](/docs/reliability/circuit_breaker/circuit_breaker_factory_resolver/.md) whose underlying resolver is resolved from a [DI container](/docs/foundation/di/.md). The returned proxy implements both `ICircuitBreakerFactoryResolver` and `ICircuitBreakerFactory`: * `use(adapterName?)` selects an adapter on the resolved `CircuitBreakerFactoryResolver`. * `create(key, settings?)` creates a circuit breaker through the resolver's default adapter. The proxy is wired to the container during `container.init()`. Create it **before** calling `init()`; calling `use()` or `create()` before the container is initialized throws an error. ## Initial configuration[​](#initial-configuration "Direct link to Initial configuration") Set up the memory and Redis circuit-breaker adapters, create the container, then register the `CircuitBreakerFactoryResolver` and wrap it with `circuitBreakerFactoryResolverDiFactory`: #### 1. Set up the memory circuit-breaker adapter[​](#1-set-up-the-memory-circuit-breaker-adapter "Direct link to 1. Set up the memory circuit-breaker adapter") ./samples/memory-circuit-breaker-adapter.ts ``` import { DatabaseCircuitBreakerAdapter } from "eridu-tech/circuit-breaker/database-circuit-breaker-adapter"; import { MemoryCircuitBreakerStorageAdapter } from "eridu-tech/circuit-breaker/memory-circuit-breaker-storage-adapter"; export const memoryCircuitBreakerAdapter = new DatabaseCircuitBreakerAdapter({ adapter: new MemoryCircuitBreakerStorageAdapter(), }); ``` #### 2. Set up the Redis circuit-breaker adapter[​](#2-set-up-the-redis-circuit-breaker-adapter "Direct link to 2. Set up the Redis circuit-breaker adapter") ./samples/redis-circuit-breaker-adapter.ts ``` import { RedisCircuitBreakerAdapter } from "eridu-tech/circuit-breaker/redis-circuit-breaker-adapter"; import { Redis } from "ioredis"; export const redisCircuitBreakerAdapter = new RedisCircuitBreakerAdapter({ database: new Redis("YOUR_REDIS_CONNECTION_STRING"), }); ``` #### 3. Create the dependency injection container[​](#3-create-the-dependency-injection-container "Direct link to 3. Create the dependency injection container") ./samples/container.ts ``` import { Container } from "eridu-tech/di"; import { ExecutionContext } from "eridu-tech/execution-context"; import { AlsExecutionContextAdapter } from "eridu-tech/execution-context/als-execution-context-adapter"; export const executionContext = new ExecutionContext( new AlsExecutionContextAdapter(), ); export const container = new Container({ executionContext }); ``` #### 4. Register and initialize the circuit-breaker factory resolver[​](#4-register-and-initialize-the-circuit-breaker-factory-resolver "Direct link to 4. Register and initialize the circuit-breaker factory resolver") ./samples/circuit-breaker-factory-resolver-di-factory.ts ``` import { CircuitBreakerFactoryResolver } from "eridu-tech/circuit-breaker"; import { ProxyCircuitBreakerFactoryResolver } from "eridu-tech/circuit-breaker/di"; import { container } from "./container.js"; import { memoryCircuitBreakerAdapter } from "./memory-circuit-breaker-adapter.js"; import { redisCircuitBreakerAdapter } from "./redis-circuit-breaker-adapter.js"; import { SuperJsonSerde } from "eridu-tech/serde/super-json-serde"; type Adapters = "memory" | "redis"; const serde = new SuperJsonSerde(); const circuitBreakerFactoryResolver = new CircuitBreakerFactoryResolver({ serde, adapters: { memory: memoryCircuitBreakerAdapter, redis: redisCircuitBreakerAdapter, }, defaultAdapter: "memory", }); await circuitBreakerFactoryResolver.init(); container.registerValue({ token: CircuitBreakerFactoryResolver, value: circuitBreakerFactoryResolver, }); // Create the proxy before container.init() export const circuitBreakerFactory = new ProxyCircuitBreakerFactoryResolver({ container, resolverToken: CircuitBreakerFactoryResolver, }); await container.init(); ``` ## Usage[​](#usage "Direct link to Usage") ### 1. Using the default adapter[​](#1-using-the-default-adapter "Direct link to 1. Using the default adapter") ./samples/circuit-breaker-factory-resolver-di-factory-default-adapter.ts ``` import { circuitBreakerFactory } from "./circuit-breaker-factory-resolver-di-factory.js"; // Uses the adapter configured as the default (memory) await circuitBreakerFactory.create("shared-resource").runOrFail(async () => { // code to run }); ``` ### 2. Specifying an adapter explicitly[​](#2-specifying-an-adapter-explicitly "Direct link to 2. Specifying an adapter explicitly") ./samples/circuit-breaker-factory-resolver-di-factory-specific-adapter.ts ``` import { circuitBreakerFactory } from "./circuit-breaker-factory-resolver-di-factory.js"; // Uses the Redis adapter await circuitBreakerFactory .use("redis") .create("shared-resource") .runOrFail(async () => { // code to run }); ``` ## Using it with the middlewares[​](#using-it-with-the-middlewares "Direct link to Using it with the middlewares") Because the proxy implements `ICircuitBreakerFactoryResolver`, it can be passed straight to the [circuit-breaker middleware factories](/docs/reliability/circuit_breaker/circuit_breaker_middlewares/.md): ./samples/circuit-breaker-factory-resolver-di-factory-with-middleware.ts ``` import { withCircuitBreakerFactory } from "eridu-tech/circuit-breaker/middlewares"; import { use } from "eridu-tech/middleware"; import { circuitBreakerFactory } from "./circuit-breaker-factory-resolver-di-factory.js"; const withCircuitBreaker = withCircuitBreakerFactory(circuitBreakerFactory); const callExternalApi = async (endpoint: string): Promise => { const response = await fetch(`https://api.example.com/${endpoint}`); return response.json(); }; const protectedCall = use( callExternalApi, withCircuitBreaker.use("redis")({ key: ([endpoint]) => `api:${endpoint}`, }), ); await protectedCall("users"); ``` ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/circuit-breaker`](https://eridu-tech.github.io/eridu-tech-core/modules/CircuitBreaker.html) API docs. --- # Configuring CircuitBreaker adapters ## RedisCircuitBreakerAdapter[​](#rediscircuitbreakeradapter "Direct link to RedisCircuitBreakerAdapter") To use the `RedisCircuitBreakerAdapter`, you'll need to: 1. Install the required dependency: [`ioredis`](https://www.npmjs.com/package/ioredis) package. ### Setup[​](#setup "Direct link to Setup") Connect to Redis: ./samples/redis-circuit-breaker-adapter-setup.ts ``` import { Redis } from "ioredis"; export const database = new Redis("YOUR_REDIS_CONNECTION_STRING"); ``` ### Usage[​](#usage "Direct link to Usage") Create the adapter: ./samples/redis-circuit-breaker-adapter.ts ``` import { RedisCircuitBreakerAdapter } from "eridu-tech/circuit-breaker/redis-circuit-breaker-adapter"; import { database } from "./redis-circuit-breaker-adapter-setup.js"; const redisCircuitBreakerAdapter = new RedisCircuitBreakerAdapter({ database, }); ``` ### Configuring backoff policy[​](#configuring-backoff-policy "Direct link to Configuring backoff policy") The `type` field is the only required field. All other fields are optional. ./samples/redis-circuit-breaker-backoff-policy.ts ``` import { BACKOFFS } from "eridu-tech/backoff-policies"; import { RedisCircuitBreakerAdapter } from "eridu-tech/circuit-breaker/redis-circuit-breaker-adapter"; import { Redis } from "ioredis"; import { TimeSpan } from "eridu-tech/time-span"; const database = new Redis("YOUR_REDIS_CONNECTION_STRING"); const redisCircuitBreakerAdapter = new RedisCircuitBreakerAdapter({ database, backoffPolicy: { type: BACKOFFS.CONSTANT, delay: TimeSpan.fromSeconds(1), jitter: 0.5, }, }); ``` The settings are the same as [backoff policies](/docs/utilities/backoff_policies/.md) settings. ### Configuring CircuitBreaker policy[​](#configuring-circuitbreaker-policy "Direct link to Configuring CircuitBreaker policy") The `type` field is the only required field. All other fields are optional. ./samples/redis-circuit-breaker-policy.ts ``` import { BREAKER_POLICIES } from "eridu-tech/circuit-breaker/policies"; import { RedisCircuitBreakerAdapter } from "eridu-tech/circuit-breaker/redis-circuit-breaker-adapter"; import { Redis } from "ioredis"; const database = new Redis("YOUR_REDIS_CONNECTION_STRING"); const redisCircuitBreakerAdapter = new RedisCircuitBreakerAdapter({ database, circuitBreakerPolicy: { type: BREAKER_POLICIES.CONSECUTIVE, failureThreshold: 5, successThreshold: 5, }, }); ``` The settings are the same as [circuit-breaker policies](/docs/reliability/circuit_breaker/configuring_circuit_breaker_policies/.md) settings. ## DatabaseCircuitBreakerAdapter[​](#database_circuit_breaker_adapter "Direct link to DatabaseCircuitBreakerAdapter") To use the `DatabaseCircuitBreakerAdapter`, you'll need to use `ICircuitBreakerStorageAdapter`: 1. Creating `ICircuitBreakerStorageAdapter`: ./samples/circuit-breaker-storage-adapter.ts ``` import { MemoryCircuitBreakerStorageAdapter } from "eridu-tech/circuit-breaker/memory-circuit-breaker-storage-adapter"; export const circuitBreakerStorageAdapter = new MemoryCircuitBreakerStorageAdapter(); ``` 2. Creating `DatabaseCircuitBreakerAdapter`: ./samples/database-circuit-breaker-adapter.ts ``` import { DatabaseCircuitBreakerAdapter } from "eridu-tech/circuit-breaker/database-circuit-breaker-adapter"; import { circuitBreakerStorageAdapter } from "./circuit-breaker-storage-adapter.js"; const circuitBreakerAdapter = new DatabaseCircuitBreakerAdapter({ adapter: circuitBreakerStorageAdapter, }); ``` ### Configuring backoff policy[​](#configuring-backoff-policy-1 "Direct link to Configuring backoff policy") You can use any of defined [backoff policies](/docs/utilities/backoff_policies/.md). ./samples/database-circuit-breaker-backoff-policy.ts ``` import { DatabaseCircuitBreakerAdapter } from "eridu-tech/circuit-breaker/database-circuit-breaker-adapter"; import { constantBackoff } from "eridu-tech/backoff-policies"; import { circuitBreakerStorageAdapter } from "./circuit-breaker-storage-adapter.js"; const circuitBreakerAdapter = new DatabaseCircuitBreakerAdapter({ adapter: circuitBreakerStorageAdapter, backoffPolicy: constantBackoff(), }); ``` ### Configuring CircuitBreaker policy[​](#configuring-circuitbreaker-policy-1 "Direct link to Configuring CircuitBreaker policy") You can use any of defined [circuit-breaker policies](/docs/reliability/circuit_breaker/configuring_circuit_breaker_policies/.md) or [create your own](/docs/reliability/circuit_breaker/creating_circuit_breaker_policies/.md). ./samples/database-circuit-breaker-policy.ts ``` import { DatabaseCircuitBreakerAdapter } from "eridu-tech/circuit-breaker/database-circuit-breaker-adapter"; import { SamplingBreaker } from "eridu-tech/circuit-breaker/policies"; import { circuitBreakerStorageAdapter } from "./circuit-breaker-storage-adapter.js"; const circuitBreakerAdapter = new DatabaseCircuitBreakerAdapter({ adapter: circuitBreakerStorageAdapter, circuitBreakerPolicy: new SamplingBreaker(), }); ``` ## NoOpCircuitBreakerAdapter[​](#noopcircuitbreakeradapter "Direct link to NoOpCircuitBreakerAdapter") The `NoOpCircuitBreakerAdapter` is a no-operation implementation, it performs no actions when called: ./samples/no-op-circuit-breaker-adapter.ts ``` import { NoOpCircuitBreakerAdapter } from "eridu-tech/circuit-breaker/no-op-circuit-breaker-adapter"; const noOpCircuitBreakerAdapter = new NoOpCircuitBreakerAdapter(); ``` info The `NoOpCircuitBreakerAdapter` is useful when you want to mock out or disable your [`CircuitBreakerProvider`](https://eridu-tech.github.io/eridu-tech-core/classes/CircuitBreaker.CircuitBreakerProvider.html) instance. ## KyselyCircuitBreakerStorageAdapter[​](#kyselycircuitbreakerstorageadapter "Direct link to KyselyCircuitBreakerStorageAdapter") To use the `KyselyCircuitBreakerStorageAdapter`, you'll need to: 1. Use database provider that has support for transactions. 2. Install the required dependency: [`kysely`](https://www.npmjs.com/package/kysely) package. 3. Provide a string serializer ([`ISerde`](/docs/foundation/serde/.md)). We recommend using `SuperJsonSerdeAdapter` for this purpose. ### Setup[​](#setup-1 "Direct link to Setup") Create the string serializer ([`ISerde`](/docs/foundation/serde/.md)) and a function that creates the [`TransactionContext`](/docs/foundation/transaction_context/transaction_context_usage/.md) by wrapping a `Kysely` instance in a [`KyselyTransactionAdapter`](/docs/foundation/transaction_context/configuring_transaction_context_adapters/.md#kysely_transaction_adapter): ./samples/kysely-circuit-breaker-storage-adapter-setup.ts ``` import { ExecutionContext } from "eridu-tech/execution-context"; import { AlsExecutionContextAdapter } from "eridu-tech/execution-context/als-execution-context-adapter"; import { contextToken } from "eridu-tech/execution-context/contracts"; import { SuperJsonSerde } from "eridu-tech/serde/super-json-serde"; import { TransactionContext } from "eridu-tech/transaction-context"; import { KyselyTransactionAdapter } from "eridu-tech/transaction-context/kysely-transaction-adapter"; import type { ITransactionContext } from "eridu-tech/transaction-context/contracts"; import type { Kysely } from "kysely"; export const serde = new SuperJsonSerde(); const executionContext = new ExecutionContext(new AlsExecutionContextAdapter()); // `KyselyCircuitBreakerStorageAdapter` is transaction aware: it runs every // circuit-breaker operation through the `current` client of this context, // which is the transaction-scoped client while a transaction is active and the // base client otherwise. export function createTransactionContext( kysely: Kysely, ): ITransactionContext> { return new TransactionContext>({ token: contextToken("kysely"), adapter: new KyselyTransactionAdapter({ database: kysely, }), executionContext, }); } ``` info The `transactionContext` setting makes the adapter transaction aware. Adapters given the same instance share the same transaction when available. ### With Sqlite[​](#with-sqlite "Direct link to With Sqlite") You will need to install [`better-sqlite3`](https://www.npmjs.com/package/better-sqlite3) package: ./samples/kysely-storage-sqlite.ts ``` import { KyselyCircuitBreakerStorageAdapter } from "eridu-tech/circuit-breaker/kysely-circuit-breaker-storage-adapter"; import Sqlite from "better-sqlite3"; import { Kysely, SqliteDialect } from "kysely"; import { createTransactionContext, serde, } from "./kysely-circuit-breaker-storage-adapter-setup.js"; const database = new Sqlite("DATABASE_NAME.db"); const kysely = new Kysely({ dialect: new SqliteDialect({ database, }), }); const transactionContext = createTransactionContext(kysely); export const kyselyCircuitBreakerStorageAdapter = new KyselyCircuitBreakerStorageAdapter({ transactionContext, serde, }); // You need initialize the adapter once before using it. // During the initialization the schema will be created await kyselyCircuitBreakerStorageAdapter.init(); ``` ### With Postgres[​](#with-postgres "Direct link to With Postgres") You will need to install [`pg`](https://www.npmjs.com/package/pg) package: ./samples/kysely-storage-postgres.ts ``` import { KyselyCircuitBreakerStorageAdapter } from "eridu-tech/circuit-breaker/kysely-circuit-breaker-storage-adapter"; import { Pool } from "pg"; import { Kysely, PostgresDialect } from "kysely"; import { createTransactionContext, serde, } from "./kysely-circuit-breaker-storage-adapter-setup.js"; const database = new Pool({ database: "DATABASE_NAME", host: "DATABASE_HOST", user: "DATABASE_USER", // DATABASE port port: 5432, password: "DATABASE_PASSWORD", max: 10, }); const kysely = new Kysely({ dialect: new PostgresDialect({ pool: database, }), }); const transactionContext = createTransactionContext(kysely); const kyselyCircuitBreakerStorageAdapter = new KyselyCircuitBreakerStorageAdapter({ transactionContext, serde, }); // You need initialize the adapter once before using it. // During the initialization the schema will be created await kyselyCircuitBreakerStorageAdapter.init(); ``` ## MemoryCircuitBreakerStorageAdapter[​](#memorycircuitbreakerstorageadapter "Direct link to MemoryCircuitBreakerStorageAdapter") To use the `MemoryCircuitBreakerStorageAdapter` you only need to create instance of it: ./samples/memory-circuit-breaker-storage-adapter.ts ``` import { MemoryCircuitBreakerStorageAdapter } from "eridu-tech/circuit-breaker/memory-circuit-breaker-storage-adapter"; const memoryCircuitBreakerStorageAdapter = new MemoryCircuitBreakerStorageAdapter(); ``` You can also provide an `Map` that will be used for storing the data in memory: ./samples/memory-circuit-breaker-storage-with-map.ts ``` import { MemoryCircuitBreakerStorageAdapter } from "eridu-tech/circuit-breaker/memory-circuit-breaker-storage-adapter"; const map = new Map(); const memoryCircuitBreakerStorageAdapter = new MemoryCircuitBreakerStorageAdapter(map); ``` info `MemoryCircuitBreakerStorageAdapter` lets you test your app without external dependencies like `Redis`, ideal for local development, unit tests, integration tests and fast E2E test for the backend application. ## MongodbCircuitBreakerStorageAdapter[​](#mongodbcircuitbreakerstorageadapter "Direct link to MongodbCircuitBreakerStorageAdapter") To use the `MongodbCircuitBreakerStorageAdapter`, you'll need to: 1. Use database provider that has support for transactions. 2. Install the required dependency: [`mongodb`](https://www.npmjs.com/package/mongodb) package. 3. Provide a string serializer ([`ISerde`](/docs/foundation/serde/.md)). We recommend using `SuperJsonSerdeAdapter` for this purpose. ### Setup[​](#setup-2 "Direct link to Setup") Connect to MongoDB, create the string serializer ([`ISerde`](/docs/foundation/serde/.md)) and the [`TransactionContext`](/docs/foundation/transaction_context/transaction_context_usage/.md), which wraps the `Db` instance in a [`MongodbTransactionAdapter`](/docs/foundation/transaction_context/configuring_transaction_context_adapters/.md#mongodb_transaction_adapter): ./samples/mongodb-circuit-breaker-storage-adapter-setup.ts ``` import { ExecutionContext } from "eridu-tech/execution-context"; import { AlsExecutionContextAdapter } from "eridu-tech/execution-context/als-execution-context-adapter"; import { contextToken } from "eridu-tech/execution-context/contracts"; import { SuperJsonSerde } from "eridu-tech/serde/super-json-serde"; import { TransactionContext } from "eridu-tech/transaction-context"; import { MongodbTransactionAdapter } from "eridu-tech/transaction-context/mongodb-transaction-adapter"; import { MongoClient } from "mongodb"; import type { ClientSession, Db } from "mongodb"; export const serde = new SuperJsonSerde(); const client = await MongoClient.connect("YOUR_MONGODB_CONNECTION_STRING"); const database = client.db("database"); // `MongodbCircuitBreakerStorageAdapter` is transaction aware: it runs every // circuit-breaker operation through the `current` client of this context. export const transactionContext = new TransactionContext({ token: contextToken("mongodb"), adapter: new MongodbTransactionAdapter({ client, database, }), executionContext: new ExecutionContext(new AlsExecutionContextAdapter()), }); ``` info The `transactionContext` setting makes the adapter transaction aware. Adapters given the same instance share the same transaction when available. ### Usage[​](#usage-1 "Direct link to Usage") Create the adapter: ./samples/mongodb-circuit-breaker-storage-adapter.ts ``` import { MongodbCircuitBreakerStorageAdapter } from "eridu-tech/circuit-breaker/mongodb-circuit-breaker-storage-adapter"; import { serde, transactionContext, } from "./mongodb-circuit-breaker-storage-adapter-setup.js"; const mongodbCircuitBreakerStorageAdapter = new MongodbCircuitBreakerStorageAdapter({ transactionContext, serde, }); // You need initialize the adapter once before using it. // During the initialization the indexes will be created await mongodbCircuitBreakerStorageAdapter.init(); ``` ## NoOpCircuitBreakerStorageAdapter[​](#noopcircuitbreakerstorageadapter "Direct link to NoOpCircuitBreakerStorageAdapter") The `NoOpCircuitBreakerStorageAdapter` is a no-operation implementation, it performs no actions when called: ./samples/no-op-circuit-breaker-storage-adapter.ts ``` import { NoOpCircuitBreakerStorageAdapter } from "eridu-tech/circuit-breaker/no-op-circuit-breaker-storage-adapter"; const noOpCircuitBreakerStorageAdapter = new NoOpCircuitBreakerStorageAdapter(); ``` info The `NoOpCircuitBreakerStorageAdapter` is useful when you want to mock out or disable your [`DatabaseCircuitBreakerAdapter`](https://eridu-tech.github.io/eridu-tech-core/classes/CircuitBreaker.DatabaseCircuitBreakerAdapter.html) instance. ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/circuit-breaker`](https://eridu-tech.github.io/eridu-tech-core/modules/CircuitBreaker.html) API docs. --- # Configuring CircuitBreaker policies ## ConsecutiveBreaker[​](#consecutivebreaker "Direct link to ConsecutiveBreaker") The `ConsecutiveBreaker` breaks after n requests in a row fail. ./samples/consecutive-breaker.ts ``` import { ConsecutiveBreaker } from "eridu-tech/circuit-breaker/policies"; new ConsecutiveBreaker({ /** * Amount of consecutive failures before going from closed -> open. * The field is optional. */ failureThreshold: 5, /** * Amount of consecutive success before going from half-open -> closed. * The field is optional. */ successThreshold: 5, }); ``` ## CountBreaker[​](#countbreaker "Direct link to CountBreaker") The `CountBreaker` breaks after a proportion of requests in a count based sliding window fail. ./samples/count-breaker.ts ``` import { CountBreaker } from "eridu-tech/circuit-breaker/policies"; new CountBreaker({ /** * Percentage (from 0 to 1) failures before going from closed -> open. * The field is optional. */ failureThreshold: 0.2, /** * Percentage (from 0 to 1) successes before going from half-open -> closed. * The field is optional. */ successThreshold: 0.8, /** * Size of the count based sliding window. * The field is optional. */ size: 20, /** * The minimum number of calls to go from closed -> open, half-opened -> closed or half-opened -> open. * The field is optional. */ minimumNumberOfCalls: 20, }); ``` ## SamplingBreaker[​](#samplingbreaker "Direct link to SamplingBreaker") The `SamplingBreaker` breaks after a proportion of requests over a time period fail. ./samples/sampling-breaker.ts ``` import { SamplingBreaker } from "eridu-tech/circuit-breaker/policies"; import { TimeSpan } from "eridu-tech/time-span"; new SamplingBreaker({ /** * Percentage (from 0 to 1) failures before going from closed -> open. * The field is optional. */ failureThreshold: 0.2, /** * Percentage (from 0 to 1) successes before going from half-open -> closed. * The field is optional. */ successThreshold: 0.8, /** * Length of time over which to sample. * The field is optional. */ timeSpan: TimeSpan.fromMinutes(1), /** * The sample length time. * The field is optional. */ sampleTimeSpan: TimeSpan.fromMinutes(1).divide(6), /** * The minimum number of calls per seconds to go from closed -> open, half-opened -> closed or half-opened -> open. * The field is optional. */ minimumRps: 5, }); ``` ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/circuit-breaker`](https://eridu-tech.github.io/eridu-tech-core/modules/CircuitBreaker.html) API docs. --- # Creating CircuitBreaker adapters ## Implementing your custom ICircuitBreakerAdapter[​](#implementing-your-custom-icircuitbreakeradapter "Direct link to Implementing your custom ICircuitBreakerAdapter") In order to create an adapter you need to implement the [`ICircuitBreakerAdapter`](https://eridu-tech.github.io/eridu-tech-core/types/CircuitBreaker.ICircuitBreakerAdapter.html) contract. ## Implementing your custom ICircuitBreakerStorageAdapter[​](#implementing-your-custom-icircuitbreakerstorageadapter "Direct link to Implementing your custom ICircuitBreakerStorageAdapter") We provide an additional contract [`ICircuitBreakerStorageAdapter`](https://eridu-tech.github.io/eridu-tech-core/types/CircuitBreaker.ICircuitBreakerStorageAdapter.html) for building custom circuit-breaker storage adapters tailored to [`DatabaseCircuitBreakerAdapter`](/docs/reliability/circuit_breaker/configuring_circuit_breaker_adapters/.md#database_circuit_breaker_adapter) and [`DatabaseCircuitBreakerProviderFactory`](/docs/reliability/circuit_breaker/circuit_breaker_factory_resolver/.md#database_circuit_breaker_factory_resolver). ## Testing your custom ICircuitBreakerStorageAdapter[​](#testing-your-custom-icircuitbreakerstorageadapter "Direct link to Testing your custom ICircuitBreakerStorageAdapter") We provide a complete test suite to test your circuit-breaker storage adapter implementation. Simply use the [`circuitBreakerStorageTestSuite`](https://eridu-tech.github.io/eridu-tech-core/functions/CircuitBreaker.circuitBreakerStorageTestSuite.html) function: * Preconfigured Vitest test cases * Common edge case coverage Usage example: ./samples/circuit-breaker-storage-test-suite.ts ``` import { beforeEach, describe, expect, test } from "vitest"; import { circuitBreakerStorageAdapterTestSuite } from "eridu-tech/circuit-breaker/test-utilities"; import { MemoryCircuitBreakerStorageAdapter } from "eridu-tech/circuit-breaker/memory-circuit-breaker-storage-adapter"; describe("class: MyCircuitBreakerStorageAdapter", () => { circuitBreakerStorageAdapterTestSuite({ createAdapter: () => new MemoryCircuitBreakerStorageAdapter(), test, beforeEach, expect, describe, }); }); ``` ## Implementing your custom ICircuitBreakerProvider class[​](#implementing-your-custom-icircuitbreakerprovider-class "Direct link to Implementing your custom ICircuitBreakerProvider class") In some cases, you may need to implement a custom [`CircuitBreakerProvider`](https://eridu-tech.github.io/eridu-tech-core/classes/CircuitBreaker.CircuitBreakerProvider.html) class to optimize performance for your specific technology stack. You can then directly implement the [`ICircuitBreakerProvider`](https://eridu-tech.github.io/eridu-tech-core/types/CircuitBreaker.ICircuitBreakerProvider.html) contract. ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/circuit-breaker`](https://eridu-tech.github.io/eridu-tech-core/modules/CircuitBreaker.html) API docs. --- # Creating CircuitBreaker policies ## Implementing your custom ICircuitBreakerPolicy[​](#implementing-your-custom-icircuitbreakerpolicy "Direct link to Implementing your custom ICircuitBreakerPolicy") In order to create custom circuit-breaker you need to implement the [`ICircuitBreakerPolicy`](https://eridu-tech.github.io/eridu-tech-core/types/CircuitBreaker.ICircuitBreakerPolicy.html) contract. Custom circuit-breaker policies can be used with [`DatabaseCircuitBreakerAdapter`](/docs/reliability/circuit_breaker/configuring_circuit_breaker_adapters/.md#database_circuit_breaker_adapter) and [`DatabaseCircuitBreakerProviderFactory`](/docs/reliability/circuit_breaker/circuit_breaker_factory_resolver/.md#database_circuit_breaker_factory_resolver). To understand how to implement a custom [`ICircuitBreakerPolicy`](https://eridu-tech.github.io/eridu-tech-core/types/CircuitBreaker.ICircuitBreakerPolicy.html), refer to the [`ConsecutiveBreaker`](https://github.com/yousif-khalil-abdulkarim/eridu-tech/blob/main/src/circuit-breaker/implementations/policies/consecutive-breaker/consecutive-breaker.ts) implementation. ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/circuit-breaker`](https://eridu-tech.github.io/eridu-tech-core/modules/CircuitBreaker.html) API docs. --- # Reliability Keep your services available and responsive under load and failure. * [Circuit Breaker](/docs/reliability/circuit_breaker/circuit_breaker_usage/.md) — Prevent cascading failures with configurable thresholds, half-open recovery, and custom fallback strategies. * [Rate Limiter](/docs/reliability/rate-limiter/rate_limiter_usage/.md) — Throttle request rates with configurable limits, sliding windows, and pluggable backends, protect your services from overload. * [Resilience](/docs/reliability/resilience/.md) — Timeout, fallback, retry, with configurable policies and backoffs. ## Existing middlewares[​](#existing-middlewares "Direct link to Existing middlewares") Beyond the components themselves, every component provides middleware factories for composing component behavior around your own functions. Wrap a function with use() or method with enhance() and apply any of the following middlewares. * [withCircuitBreakerFactory](/docs/reliability/circuit_breaker/circuit_breaker_middlewares/.md#with_circuit_breaker_factory_middleware) — Wraps function calls with a circuit breaker. * [withRateLimiterFactory](/docs/reliability/rate-limiter/rate_limiter_middlewares/.md#with_rate_limiter_factory_middleware) — Wraps function calls with a rate limiter. * [fallback](/docs/reliability/resilience/.md#fallback) — Returns a fallback value when the wrapped function fails. * [retry](/docs/reliability/resilience/.md#retry) — Retries the wrapped function up to a maximum number of attempts. * [retryInterval](/docs/reliability/resilience/.md#retry_by_interval) — Retries the wrapped function at a fixed interval until it succeeds or the time budget is exhausted. * [timeout](/docs/reliability/resilience/.md#timeout) — Rejects the wrapped function if it does not complete within a duration. --- # Configuring RateLimiter adapters ## RedisRateLimiterAdapter[​](#redisratelimiteradapter "Direct link to RedisRateLimiterAdapter") To use the `RedisRateLimiterAdapter`, you'll need to: 1. Install the required dependency: [`ioredis`](https://www.npmjs.com/package/ioredis) package. ### Setup[​](#setup "Direct link to Setup") Connect to Redis: ./samples/redis-rate-limiter-adapter-setup.ts ``` import { Redis } from "ioredis"; export const database = new Redis("YOUR_REDIS_CONNECTION_STRING"); ``` ### Usage[​](#usage "Direct link to Usage") Create the adapter: ./samples/redis-rate-limiter-adapter.ts ``` import { RedisRateLimiterAdapter } from "eridu-tech/rate-limiter/redis-rate-limiter-adapter"; import { database } from "./redis-rate-limiter-adapter-setup.js"; const redisRateLimiterAdapter = new RedisRateLimiterAdapter({ database, }); ``` ### Configuring backoff policy[​](#configuring-backoff-policy "Direct link to Configuring backoff policy") The `type` field is the only required field. All other fields are optional. ./samples/redis-rate-limiter-backoff-policy.ts ``` import { BACKOFFS } from "eridu-tech/backoff-policies"; import { RedisRateLimiterAdapter } from "eridu-tech/rate-limiter/redis-rate-limiter-adapter"; import { Redis } from "ioredis"; import { TimeSpan } from "eridu-tech/time-span"; const database = new Redis("YOUR_REDIS_CONNECTION_STRING"); const redisRateLimiterAdapter = new RedisRateLimiterAdapter({ database, backoffPolicy: { type: BACKOFFS.CONSTANT, delay: TimeSpan.fromSeconds(1), jitter: 0.5, }, }); ``` The settings are the same as [backoff policies](/docs/utilities/backoff_policies/.md) settings. ### Configuring RateLimiter policy[​](#configuring-ratelimiter-policy "Direct link to Configuring RateLimiter policy") The `type` field is the only required field. All other fields are optional. ./samples/redis-rate-limiter-policy.ts ``` import { LIMITER_POLICIES } from "eridu-tech/rate-limiter/policies"; import { RedisRateLimiterAdapter } from "eridu-tech/rate-limiter/redis-rate-limiter-adapter"; import { Redis } from "ioredis"; const database = new Redis("YOUR_REDIS_CONNECTION_STRING"); const redisRateLimiterAdapter = new RedisRateLimiterAdapter({ database, rateLimiterPolicy: { type: LIMITER_POLICIES.SLIDING_WINDOW, }, }); ``` The settings are the same as [rate-limiter policies](/docs/reliability/rate-limiter/configuring_rate_limiter_policies/.md) settings. ## DatabaseRateLimiterAdapter[​](#database_rate_limiter_adapter "Direct link to DatabaseRateLimiterAdapter") To use the `DatabaseRateLimiterAdapter`, you'll need to use `IRateLimiterStorageAdapter`: 1. Creating `IRateLimiterStorageAdapter`: ./samples/rate-limiter-storage-adapter.ts ``` import { MemoryRateLimiterStorageAdapter } from "eridu-tech/rate-limiter/memory-rate-limiter-storage-adapter"; export const rateLimiterStorageAdapter = new MemoryRateLimiterStorageAdapter(); ``` 2. Creating `DatabaseRateLimiterAdapter`: ./samples/database-rate-limiter-adapter.ts ``` import { DatabaseRateLimiterAdapter } from "eridu-tech/rate-limiter/database-rate-limiter-adapter"; import { rateLimiterStorageAdapter } from "./rate-limiter-storage-adapter.js"; const rateLimiterAdapter = new DatabaseRateLimiterAdapter({ adapter: rateLimiterStorageAdapter, }); ``` ### Configuring backoff policy[​](#configuring-backoff-policy-1 "Direct link to Configuring backoff policy") You can use any of defined [backoff policies](/docs/utilities/backoff_policies/.md). ./samples/database-rate-limiter-backoff-policy.ts ``` import { DatabaseRateLimiterAdapter } from "eridu-tech/rate-limiter/database-rate-limiter-adapter"; import { constantBackoff } from "eridu-tech/backoff-policies"; import { rateLimiterStorageAdapter } from "./rate-limiter-storage-adapter.js"; const rateLimiterAdapter = new DatabaseRateLimiterAdapter({ adapter: rateLimiterStorageAdapter, backoffPolicy: constantBackoff(), }); ``` ### Configuring RateLimiter policy[​](#configuring-ratelimiter-policy-1 "Direct link to Configuring RateLimiter policy") You can use any of defined [rate-limiter policies](/docs/reliability/rate-limiter/configuring_rate_limiter_policies/.md) or [create your own](/docs/reliability/rate-limiter/creating_rate_limiter_policies/.md). ./samples/database-rate-limiter-policy.ts ``` import { DatabaseRateLimiterAdapter } from "eridu-tech/rate-limiter/database-rate-limiter-adapter"; import { SlidingWindowLimiter } from "eridu-tech/rate-limiter/policies"; import { rateLimiterStorageAdapter } from "./rate-limiter-storage-adapter.js"; const rateLimiterAdapter = new DatabaseRateLimiterAdapter({ adapter: rateLimiterStorageAdapter, rateLimiterPolicy: new SlidingWindowLimiter(), }); ``` ## NoOpRateLimiterAdapter[​](#noopratelimiteradapter "Direct link to NoOpRateLimiterAdapter") The `NoOpRateLimiterAdapter` is a no-operation implementation, it performs no actions when called: ./samples/no-op-rate-limiter-adapter.ts ``` import { NoOpRateLimiterAdapter } from "eridu-tech/rate-limiter/no-op-rate-limiter-adapter"; const noOpRateLimiterAdapter = new NoOpRateLimiterAdapter(); ``` info The `NoOpRateLimiterAdapter` is useful when you want to mock out or disable your [`RateLimiterProvider`](https://eridu-tech.github.io/eridu-tech-core/classes/RateLimiter.RateLimiterProvider.html) instance. ## KyselyRateLimiterStorageAdapter[​](#kyselyratelimiterstorageadapter "Direct link to KyselyRateLimiterStorageAdapter") To use the `KyselyRateLimiterStorageAdapter`, you'll need to: 1. Use database provider that has support for transactions. 2. Install the required dependency: [`kysely`](https://www.npmjs.com/package/kysely) package. 3. Provide a string serializer ([`ISerde`](/docs/foundation/serde/.md)). We recommend using `SuperJsonSerdeAdapter` for this purpose. ### Setup[​](#setup-1 "Direct link to Setup") Create the string serializer ([`ISerde`](/docs/foundation/serde/.md)) and a function that creates the [`TransactionContext`](/docs/foundation/transaction_context/transaction_context_usage/.md) by wrapping a `Kysely` instance in a [`KyselyTransactionAdapter`](/docs/foundation/transaction_context/configuring_transaction_context_adapters/.md#kysely_transaction_adapter): ./samples/kysely-rate-limiter-adapter-setup.ts ``` import { ExecutionContext } from "eridu-tech/execution-context"; import { AlsExecutionContextAdapter } from "eridu-tech/execution-context/als-execution-context-adapter"; import { contextToken } from "eridu-tech/execution-context/contracts"; import { SuperJsonSerde } from "eridu-tech/serde/super-json-serde"; import { TransactionContext } from "eridu-tech/transaction-context"; import { KyselyTransactionAdapter } from "eridu-tech/transaction-context/kysely-transaction-adapter"; import type { ITransactionContext } from "eridu-tech/transaction-context/contracts"; import type { Kysely } from "kysely"; export const serde = new SuperJsonSerde(); const executionContext = new ExecutionContext(new AlsExecutionContextAdapter()); // `KyselyRateLimiterStorageAdapter` is transaction aware: it runs every // rate-limiter operation through the `current` client of this context, which // is the transaction-scoped client while a transaction is active and the base // client otherwise. export function createTransactionContext( kysely: Kysely, ): ITransactionContext> { return new TransactionContext>({ token: contextToken("kysely"), adapter: new KyselyTransactionAdapter({ database: kysely, }), executionContext, }); } ``` info The `transactionContext` setting makes the adapter transaction aware. Adapters given the same instance share the same transaction when available. ### With Sqlite[​](#with-sqlite "Direct link to With Sqlite") You will need to install [`better-sqlite3`](https://www.npmjs.com/package/better-sqlite3) package: ./samples/kysely-storage-sqlite.ts ``` import { KyselyRateLimiterStorageAdapter } from "eridu-tech/rate-limiter/kysely-rate-limiter-storage-adapter"; import Sqlite from "better-sqlite3"; import { Kysely, SqliteDialect } from "kysely"; import { createTransactionContext, serde, } from "./kysely-rate-limiter-adapter-setup.js"; const database = new Sqlite("DATABASE_NAME.db"); const kysely = new Kysely({ dialect: new SqliteDialect({ database, }), }); const transactionContext = createTransactionContext(kysely); export const kyselyRateLimiterStorageAdapter = new KyselyRateLimiterStorageAdapter({ transactionContext, serde, }); // You need initialize the adapter once before using it. // During the initialization the schema will be created await kyselyRateLimiterStorageAdapter.init(); ``` ### With Postgres[​](#with-postgres "Direct link to With Postgres") You will need to install [`pg`](https://www.npmjs.com/package/pg) package: ./samples/kysely-storage-postgres.ts ``` import { KyselyRateLimiterStorageAdapter } from "eridu-tech/rate-limiter/kysely-rate-limiter-storage-adapter"; import { Pool } from "pg"; import { Kysely, PostgresDialect } from "kysely"; import { createTransactionContext, serde, } from "./kysely-rate-limiter-adapter-setup.js"; const database = new Pool({ database: "DATABASE_NAME", host: "DATABASE_HOST", user: "DATABASE_USER", // DATABASE port port: 5432, password: "DATABASE_PASSWORD", max: 10, }); const kysely = new Kysely({ dialect: new PostgresDialect({ pool: database, }), }); const transactionContext = createTransactionContext(kysely); const kyselyRateLimiterStorageAdapter = new KyselyRateLimiterStorageAdapter({ transactionContext, serde, }); // You need initialize the adapter once before using it. // During the initialization the schema will be created await kyselyRateLimiterStorageAdapter.init(); ``` ### Settings[​](#settings "Direct link to Settings") To clean up expired rate-limiter records, call `removeAllExpired` at a regular interval (for example, using a cron job): ./samples/kysely-storage-remove-all-expired.ts ``` import { kyselyRateLimiterStorageAdapter } from "./kysely-storage-sqlite.js"; // Remove all expired rate-limiter records manually. await kyselyRateLimiterStorageAdapter.removeAllExpired(); ``` ## MemoryRateLimiterStorageAdapter[​](#memoryratelimiterstorageadapter "Direct link to MemoryRateLimiterStorageAdapter") To use the `MemoryRateLimiterStorageAdapter` you only need to create instance of it: ./samples/memory-rate-limiter-storage-adapter.ts ``` import { MemoryRateLimiterStorageAdapter } from "eridu-tech/rate-limiter/memory-rate-limiter-storage-adapter"; const memoryRateLimiterStorageAdapter = new MemoryRateLimiterStorageAdapter(); ``` You can also provide an `Map` that will be used for storing the data in memory: ./samples/memory-rate-limiter-storage-with-map.ts ``` import { MemoryRateLimiterStorageAdapter } from "eridu-tech/rate-limiter/memory-rate-limiter-storage-adapter"; const map = new Map(); const memoryRateLimiterStorageAdapter = new MemoryRateLimiterStorageAdapter( map, ); ``` info `MemoryRateLimiterStorageAdapter` lets you test your app without external dependencies like `Redis`, ideal for local development, unit tests, integration tests and fast E2E test for the backend application. ### Settings[​](#settings-1 "Direct link to Settings") To clean up expired rate-limiter records, call `removeAllExpired` at a regular interval (for example, using a cron job): ./samples/memory-rate-limiter-remove-all-expired.ts ``` import { MemoryRateLimiterStorageAdapter } from "eridu-tech/rate-limiter/memory-rate-limiter-storage-adapter"; const memoryRateLimiterStorageAdapter = new MemoryRateLimiterStorageAdapter(); // Remove all expired rate-limiter records manually. await memoryRateLimiterStorageAdapter.removeAllExpired(); ``` ## MongodbRateLimiterStorageAdapter[​](#mongodbratelimiterstorageadapter "Direct link to MongodbRateLimiterStorageAdapter") To use the `MongodbRateLimiterStorageAdapter`, you'll need to: 1. Use database provider that has support for transactions. 2. Install the required dependency: [`mongodb`](https://www.npmjs.com/package/mongodb) package. 3. Provide a string serializer ([`ISerde`](/docs/foundation/serde/.md)). We recommend using `SuperJsonSerdeAdapter` for this purpose. ### Setup[​](#setup-2 "Direct link to Setup") Connect to MongoDB, create the string serializer ([`ISerde`](/docs/foundation/serde/.md)) and the [`TransactionContext`](/docs/foundation/transaction_context/transaction_context_usage/.md), which wraps the `Db` instance in a [`MongodbTransactionAdapter`](/docs/foundation/transaction_context/configuring_transaction_context_adapters/.md#mongodb_transaction_adapter): ./samples/mongodb-rate-limiter-adapter-setup.ts ``` import { ExecutionContext } from "eridu-tech/execution-context"; import { AlsExecutionContextAdapter } from "eridu-tech/execution-context/als-execution-context-adapter"; import { contextToken } from "eridu-tech/execution-context/contracts"; import { SuperJsonSerde } from "eridu-tech/serde/super-json-serde"; import { TransactionContext } from "eridu-tech/transaction-context"; import { MongodbTransactionAdapter } from "eridu-tech/transaction-context/mongodb-transaction-adapter"; import { MongoClient } from "mongodb"; import type { ClientSession, Db } from "mongodb"; export const serde = new SuperJsonSerde(); const client = await MongoClient.connect("YOUR_MONGODB_CONNECTION_STRING"); const database = client.db("database"); // `MongodbRateLimiterStorageAdapter` is transaction aware: it runs every // rate-limiter operation through the `current` client of this context. export const transactionContext = new TransactionContext({ token: contextToken("mongodb"), adapter: new MongodbTransactionAdapter({ client, database, }), executionContext: new ExecutionContext(new AlsExecutionContextAdapter()), }); ``` info The `transactionContext` setting makes the adapter transaction aware. Adapters given the same instance share the same transaction when available. ### Usage[​](#usage-1 "Direct link to Usage") Create the adapter: ./samples/mongodb-rate-limiter-storage-adapter.ts ``` import { MongodbRateLimiterStorageAdapter } from "eridu-tech/rate-limiter/mongodb-rate-limiter-storage-adapter"; import { serde, transactionContext, } from "./mongodb-rate-limiter-adapter-setup.js"; const mongodbRateLimiterStorageAdapter = new MongodbRateLimiterStorageAdapter({ transactionContext, serde, }); // You need initialize the adapter once before using it. // During the initialization the indexes will be created await mongodbRateLimiterStorageAdapter.init(); ``` ## NoOpRateLimiterStorageAdapter[​](#noopratelimiterstorageadapter "Direct link to NoOpRateLimiterStorageAdapter") The `NoOpRateLimiterStorageAdapter` is a no-operation implementation, it performs no actions when called: ./samples/no-op-rate-limiter-storage-adapter.ts ``` import { NoOpRateLimiterStorageAdapter } from "eridu-tech/rate-limiter/no-op-rate-limiter-storage-adapter"; const noOpRateLimiterStorageAdapter = new NoOpRateLimiterStorageAdapter(); ``` info The `NoOpRateLimiterStorageAdapter` is useful when you want to mock out or disable your [`DatabaseRateLimiterAdapter`](https://eridu-tech.github.io/eridu-tech-core/classes/RateLimiter.DatabaseRateLimiterAdapter.html) instance. ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/rate-limiter`](https://eridu-tech.github.io/eridu-tech-core/modules/RateLimiter.html) API docs. --- # Configuring RateLimiter policies ## SlidingWindowLimiter[​](#slidingwindowlimiter "Direct link to SlidingWindowLimiter") ./samples/sliding-window-limiter.ts ```` import { SlidingWindowLimiter } from "eridu-tech/rate-limiter/policies"; import { TimeSpan } from "eridu-tech/time-span"; new SlidingWindowLimiter({ /** * The time span in which attempts are active before reseting. * The field is optional. * */ window: TimeSpan.fromSeconds(1), /** * The field is optional. * ``` */ margin: TimeSpan.fromSeconds(4).divide(4), }); ```` ## FixedWindowLimiter[​](#fixedwindowlimiter "Direct link to FixedWindowLimiter") ./samples/fixed-window-limiter.ts ``` import { FixedWindowLimiter } from "eridu-tech/rate-limiter/policies"; import { TimeSpan } from "eridu-tech/time-span"; new FixedWindowLimiter({ /** * The time span in which attempts are active before reseting. * The field is optional. */ window: TimeSpan.fromSeconds(1), }); ``` ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/rate-limiter`](https://eridu-tech.github.io/eridu-tech-core/modules/RateLimiter.html) API docs. --- # Creating RateLimiter adapters ## Implementing your custom IRateLimiterAdapter[​](#implementing-your-custom-iratelimiteradapter "Direct link to Implementing your custom IRateLimiterAdapter") In order to create an adapter you need to implement the [`IRateLimiterAdapter`](https://eridu-tech.github.io/eridu-tech-core/types/RateLimiter.IRateLimiterAdapter.html) contract. ## Implementing your custom IRateLimiterStorageAdapter[​](#implementing-your-custom-iratelimiterstorageadapter "Direct link to Implementing your custom IRateLimiterStorageAdapter") We provide an additional contract [`IRateLimiterStorageAdapter`](https://eridu-tech.github.io/eridu-tech-core/types/RateLimiter.IRateLimiterStorageAdapter.html) for building custom rate-limiter storage adapters tailored to [`DatabaseRateLimiterAdapter`](/docs/reliability/rate-limiter/configuring_rate_limiter_adapters/.md#database_rate_limiter_adapter) and [`DatabaseRateLimiterProviderFactory`](/docs/reliability/rate-limiter/rate_limiter_factory_resolver/.md#database_rate_limiter_factory_resolver). ## Testing your custom IRateLimiterStorageAdapter[​](#testing-your-custom-iratelimiterstorageadapter "Direct link to Testing your custom IRateLimiterStorageAdapter") We provide a complete test suite to test your rate-limiter storage adapter implementation. Simply use the [`rateLimiterBreakerStorageTestSuite`](https://eridu-tech.github.io/eridu-tech-core/functions/RateLimiter.rateLimiterBreakerStorageTestSuite.html) function: * Preconfigured Vitest test cases * Common edge case coverage Usage example: ./samples/rate-limiter-storage-test-suite.ts ``` import { beforeEach, describe, expect, test } from "vitest"; import { rateLimiterStorageAdapterTestSuite } from "eridu-tech/rate-limiter/test-utilities"; import { MemoryRateLimiterStorageAdapter } from "eridu-tech/rate-limiter/memory-rate-limiter-storage-adapter"; describe("class: MyRateLimiterStorageAdapter", () => { rateLimiterStorageAdapterTestSuite({ createAdapter: () => new MemoryRateLimiterStorageAdapter(), test, beforeEach, expect, describe, }); }); ``` ## Implementing your custom IRateLimiterProvider class[​](#implementing-your-custom-iratelimiterprovider-class "Direct link to Implementing your custom IRateLimiterProvider class") In some cases, you may need to implement a custom [`RateLimiterProvider`](https://eridu-tech.github.io/eridu-tech-core/classes/RateLimiter.RateLimiterProvider.html) class to optimize performance for your specific technology stack. You can then directly implement the [`IRateLimiterProvider`](https://eridu-tech.github.io/eridu-tech-core/types/RateLimiter.IRateLimiterProvider.html) contract. ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/rate-limiter`](https://eridu-tech.github.io/eridu-tech-core/modules/RateLimiter.html) API docs. --- # Creating RateLimiter policies ## Implementing your custom IRateLimiterPolicy[​](#implementing-your-custom-iratelimiterpolicy "Direct link to Implementing your custom IRateLimiterPolicy") In order to create custom rate-limiter you need to implement the [`IRateLimiterPolicy`](https://eridu-tech.github.io/eridu-tech-core/types/RateLimiter.IRateLimiterPolicy.html) contract. Custom rate-limiter policies can be used with [`DatabaseRateLimiterAdapter`](/docs/reliability/rate-limiter/configuring_rate_limiter_adapters/.md#database_rate_limiter_adapter) and [`DatabaseRateLimiterProviderFactory`](/docs/reliability/rate-limiter/rate_limiter_factory_resolver/.md#database_rate_limiter_factory_resolver). To understand how to implement a custom [`IRateLimiterPolicy`](https://eridu-tech.github.io/eridu-tech-core/types/RateLimiter.IRateLimiterPolicy.html), refer to the [`FixedWindowLimiter`](https://github.com/yousif-khalil-abdulkarim/eridu-tech/blob/main/src/rate-limiter/implementations/policies/fixed-window-limiter/fixed-window-limiter.ts) implementation. ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/rate-limiter`](https://eridu-tech.github.io/eridu-tech-core/modules/RateLimiter.html) API docs. --- # RateLimiter resolver factory classes ## RateLimiterFactoryResolver[​](#ratelimiterfactoryresolver "Direct link to RateLimiterFactoryResolver") The `RateLimiterFactoryResolver` class provides a flexible way to configure and switch between different rate-limiter adapters at runtime. ### Initial configuration[​](#initial-configuration "Direct link to Initial configuration") To begin using the `RateLimiterFactoryResolver`, You will need to register all required adapters during initialization. ./samples/rate-limiter-factory-resolver-initial-config.ts ``` import { RateLimiterFactoryResolver } from "eridu-tech/rate-limiter"; import { MemoryRateLimiterStorageAdapter } from "eridu-tech/rate-limiter/memory-rate-limiter-storage-adapter"; import { DatabaseRateLimiterAdapter } from "eridu-tech/rate-limiter/database-rate-limiter-adapter"; import { RedisRateLimiterAdapter } from "eridu-tech/rate-limiter/redis-rate-limiter-adapter"; import { SuperJsonSerde } from "eridu-tech/serde/super-json-serde"; import { Redis } from "ioredis"; const serde = new SuperJsonSerde(); export const rateLimiterFactoryResolver = new RateLimiterFactoryResolver({ serde, adapters: { memory: new DatabaseRateLimiterAdapter({ adapter: new MemoryRateLimiterStorageAdapter(), }), redis: new RedisRateLimiterAdapter({ database: new Redis("YOUR_REDIS_CONNECTION"), }), }, defaultAdapter: "memory", }); await rateLimiterFactoryResolver.init(); ``` ### Usage[​](#usage "Direct link to Usage") #### 1. Using the default adapter[​](#1-using-the-default-adapter "Direct link to 1. Using the default adapter") ./samples/rate-limiter-factory-resolver-default-adapter.ts ``` import { rateLimiterFactoryResolver } from "./rate-limiter-factory-resolver-initial-config.js"; // Will apply rate-limiter logic the default adapter which is MemoryRateLimiterStorageAdapter await rateLimiterFactoryResolver .use() .create("a", { limit: 10, }) .runOrFail(async () => { // ... code to apply rate-limiter logic }); ``` danger Note that if you dont set a default adapter, an error will be thrown. #### 2. Specifying an adapter explicitly[​](#2-specifying-an-adapter-explicitly "Direct link to 2. Specifying an adapter explicitly") ./samples/rate-limiter-factory-resolver-specific-adapter.ts ``` import { rateLimiterFactoryResolver } from "./rate-limiter-factory-resolver-initial-config.js"; // Will apply rate-limiter logic using the redis adapter await rateLimiterFactoryResolver .use("redis") .create("a", { limit: 10, }) .runOrFail(async () => { // ... code to apply rate-limiter logic }); ``` danger Note that if you specify a non-existent adapter, an error will be thrown. ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/rate-limiter`](https://eridu-tech.github.io/eridu-tech-core/modules/RateLimiter.html) API docs. --- # RateLimiter middlewares ## Initial configuration[​](#initial-configuration "Direct link to Initial configuration") To begin using the rate-limiter middlewares, you'll need to create and configure a `RateLimiterFactoryResolver` instance: ./samples/rate-limiter.ts ``` import { RateLimiterFactoryResolver } from "eridu-tech/rate-limiter"; import { DatabaseRateLimiterAdapter } from "eridu-tech/rate-limiter/database-rate-limiter-adapter"; import { MemoryRateLimiterStorageAdapter } from "eridu-tech/rate-limiter/memory-rate-limiter-storage-adapter"; import { RedisRateLimiterAdapter } from "eridu-tech/rate-limiter/redis-rate-limiter-adapter"; import { SuperJsonSerde } from "eridu-tech/serde/super-json-serde"; import { Redis } from "ioredis"; const serde = new SuperJsonSerde(); export const rateLimiterFactoryResolver = new RateLimiterFactoryResolver({ serde, adapters: { memory: new DatabaseRateLimiterAdapter({ adapter: new MemoryRateLimiterStorageAdapter(), }), redis: new RedisRateLimiterAdapter({ database: new Redis("YOUR_REDIS_CONNECTION_STRING"), }), }, defaultAdapter: "memory", }); await rateLimiterFactoryResolver.init(); ``` ## withRateLimiterFactory middleware[​](#with_rate_limiter_factory_middleware "Direct link to withRateLimiterFactory middleware") The RateLimiter middleware wraps function calls with a rate limiter, controlling how many times a function can be invoked within a configured policy window. Each unique key (derived from the function's arguments) gets its own rate limit counter. Once the limit is reached, further invocations are blocked until the policy permits attempts again. ### Usage[​](#usage "Direct link to Usage") ./samples/with-rate-limiter.ts ``` import { withRateLimiterFactory } from "eridu-tech/rate-limiter/middlewares"; import { use } from "eridu-tech/middleware"; import { rateLimiterFactoryResolver } from "./rate-limiter.js"; const withRateLimiter = withRateLimiterFactory(rateLimiterFactoryResolver); const fetchHandler = async (request: Request): Promise => { // ... handle the request return new Response("OK"); }; // Wrap with rate limiter using the default adapter (`memory`) — max 10 calls per window const rateLimitedCall = use( fetchHandler, withRateLimiter({ key: ([req]) => `api:${String(req.headers.get("x-ip"))}`, limit: 10, }), ); // Wrap with rate limiter using a specific adapter (`redis`) const rateLimitedCallOnRedis = use( fetchHandler, withRateLimiter.use("redis")({ key: ([req]) => `api:${String(req.headers.get("x-ip"))}`, limit: 10, }), ); await rateLimitedCall( new Request("/url", { method: "POST", }), ); await rateLimitedCallOnRedis( new Request("/url", { method: "POST", }), ); ``` Calling `withRateLimiter()` directly uses the resolver's default adapter (`memory`). Use `withRateLimiter.use("redis")` to run the wrapped function through a specific registered adapter. info Here is a complete list of settings for the [`withRateLimiter`](https://eridu-tech.github.io/eridu-tech-core/types/RateLimiter.WithRateLimiterSettings.html) function. ### Settings[​](#settings "Direct link to Settings") | Option | Type | Description | | ------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | `key` | `Invocable` | A function that produces the rate-limiter key from the wrapped function's arguments. Each unique key gets its own rate-limit counter | | `limit` | `number` | Maximum number of invocations allowed within the configured window | | `onlyError` | `boolean` | When `true`, only failed (errored) invocations count toward the rate limit. Defaults to `false` | | `errorPolicy` | `ErrorPolicy` | Determines which errors count toward the rate limit. Defaults to treating all errors as failures | ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/rate-limiter`](https://eridu-tech.github.io/eridu-tech-core/modules/RateLimiter.html) API docs. --- # RateLimiter Plugins ## withRateLimiterPrefix plugin[​](#withratelimiterprefix-plugin "Direct link to withRateLimiterPrefix plugin") The RateLimiter prefix plugin intercepts calls to a rate-limiter adapter and transparently prefixes all rate-limiter keys with a configurable string. This enables logical key namespacing without modifying the adapter implementation. ### Use cases[​](#use-cases "Direct link to Use cases") * **Multi-tenant rate limiting** — Prefix rate-limiter keys with a tenant identifier to apply separate rate limits per tenant * **Endpoint scoping** — Organize rate limits by API endpoint or route prefix * **Environment isolation** — Separate development, staging, and production rate limit state * **User tier differentiation** — Prefix keys with a tier identifier (e.g., "free:", "premium:") to apply different rate limits ### How it works[​](#how-it-works "Direct link to How it works") The `withRateLimiterPrefix` function returns a [`PluginFn`](/docs/foundation/middleware/.md) that calls `enhance` on each adapter method that accepts a rate-limiter key. When an enhanced method is invoked, the plugin intercepts the call, prepends the configured prefix to the key argument, and forwards the modified arguments to the original method. The plugin prefixes keys for the following methods: | Method | Key argument | Pattern | | ------------- | ---------------------- | -------------- | | `getState` | First argument (`key`) | `prefix + key` | | `reset` | First argument (`key`) | `prefix + key` | | `updateState` | First argument (`key`) | `prefix + key` | ### Usage[​](#usage "Direct link to Usage") ./samples/with-rate-limiter-prefix.ts ``` import { withPlugin } from "eridu-tech/middleware"; import { MemoryRateLimiterStorageAdapter } from "eridu-tech/rate-limiter/memory-rate-limiter-storage-adapter"; import { DatabaseRateLimiterAdapter } from "eridu-tech/rate-limiter/database-rate-limiter-adapter"; import { withRateLimiterPrefix } from "eridu-tech/rate-limiter/plugins"; const adapter = new DatabaseRateLimiterAdapter({ adapter: new MemoryRateLimiterStorageAdapter(), }); // Apply the prefix plugin to the adapter const prefixedAdapter = withPlugin( adapter, withRateLimiterPrefix("tenant-42:"), ); ``` ### Before/after behavior[​](#beforeafter-behavior "Direct link to Before/after behavior") **Before** — Rate-limiter keys are used as-is: ./samples/unprefixed-get-state.ts ``` import { MemoryRateLimiterStorageAdapter } from "eridu-tech/rate-limiter/memory-rate-limiter-storage-adapter"; import { DatabaseRateLimiterAdapter } from "eridu-tech/rate-limiter/database-rate-limiter-adapter"; const adapter = new DatabaseRateLimiterAdapter({ adapter: new MemoryRateLimiterStorageAdapter(), }); await adapter.getState("api:login"); // -> checks rate limit for "api:login" ``` **After** — Rate-limiter keys are automatically prefixed: ./samples/prefixed-get-state.ts ``` import { withPlugin } from "eridu-tech/middleware"; import { MemoryRateLimiterStorageAdapter } from "eridu-tech/rate-limiter/memory-rate-limiter-storage-adapter"; import { DatabaseRateLimiterAdapter } from "eridu-tech/rate-limiter/database-rate-limiter-adapter"; import { withRateLimiterPrefix } from "eridu-tech/rate-limiter/plugins"; const adapter = new DatabaseRateLimiterAdapter({ adapter: new MemoryRateLimiterStorageAdapter(), }); // Apply the prefix plugin to the adapter const prefixedAdapter = withPlugin( adapter, withRateLimiterPrefix("tenant-42:"), ); await prefixedAdapter.getState("api:login"); // -> checks rate limit for "tenant-42:api:login" ``` danger Because `withPlugin` uses `enhance` under the hood, the same edge case applies: if one enhanced method internally calls another enhanced method via `this`, the middleware will apply **twice**. Be mindful of inter-method calls when applying plugins that enhance multiple methods on the same instance. info For more information about the `withPlugin` function and applying plugins to adapters, see the [Middleware plugin](/docs/foundation/middleware/.md#plugin) documentation. --- # RateLimiter usage The `eridu-tech/rate-limiter` component provides a way for managing rate-limiter independent of underlying platform or storage. ## Initial configuration[​](#initial-configuration "Direct link to Initial configuration") To begin using the `RateLimiterFactory` class, you'll need to create and configure an instance: ./samples/rate-limiter-factory-initial-config.ts ``` import { MemoryRateLimiterStorageAdapter } from "eridu-tech/rate-limiter/memory-rate-limiter-storage-adapter"; import { DatabaseRateLimiterAdapter } from "eridu-tech/rate-limiter/database-rate-limiter-adapter"; import { RateLimiterFactory } from "eridu-tech/rate-limiter"; export const rateLimiterFactory = new RateLimiterFactory({ // You can provide default settings // You can choose the adapter to use adapter: new DatabaseRateLimiterAdapter({ adapter: new MemoryRateLimiterStorageAdapter(), }), }); await rateLimiterFactory.init(); ``` info Here is a complete list of settings for the [`RateLimiterFactory`](https://eridu-tech.github.io/eridu-tech-core/types/RateLimiter.RateLimiterFactorySettingsBase.html) class. ## RateLimiter basics[​](#ratelimiter-basics "Direct link to RateLimiter basics") ### Creating a rate-limiter[​](#creating-a-rate-limiter "Direct link to Creating a rate-limiter") ./samples/rate-limiter-create.ts ``` import { rateLimiterFactory } from "./rate-limiter-factory-initial-config.js"; export const rateLimiter = rateLimiterFactory.create("resource", { limit: 10, }); ``` ### Using the rate-limiter[​](#using-the-rate-limiter "Direct link to Using the rate-limiter") ./samples/rate-limiter-run-or-fail.ts ``` import { rateLimiter } from "./rate-limiter-create.js"; // The function will only be called when the rate-limiter allows the attempt. await rateLimiter.runOrFail(async () => { // The code / function to rate limit, called it here }); ``` info Note the method throws an error when the rate-limiter is blocked. info You can provide synchronous or asynchronous [`Invocable<[], TValue | Promise>`](/docs/utilities/invocable/.md) as values for the `runOrFail` method. ### Applying rate-limiter on only erros[​](#applying-rate-limiter-on-only-erros "Direct link to Applying rate-limiter on only erros") The rate-limiter defaults to counting all attempts. You can optionally configure it to track only failed requests. ./samples/rate-limiter-only-error.ts ``` import { rateLimiterFactory } from "./rate-limiter-factory-initial-config.js"; class ErrorA extends Error {} const rateLimiter = rateLimiterFactory.create("resource", { onlyError: true, limit: 10, }); await rateLimiter.runOrFail(async () => { // The code / function to rate limit, called it here }); ``` ### Applying rate-limiter on certiain errors[​](#applying-rate-limiter-on-certiain-errors "Direct link to Applying rate-limiter on certiain errors") ./samples/rate-limiter-error-policy.ts ``` import { rateLimiterFactory } from "./rate-limiter-factory-initial-config.js"; class ErrorA extends Error {} const rateLimiter = rateLimiterFactory.create("resource", { onlyError: true, limit: 10, // Error policy will only work "onlyError" is set to true errorPolicy: ErrorA, }); await rateLimiter.runOrFail(async () => { // The code / function to rate limit, called it here }); ``` ### Reseting the rate-limiter[​](#reseting-the-rate-limiter "Direct link to Reseting the rate-limiter") You can reset rate-limiter state to the allowed state manually. ./samples/rate-limiter-reset.ts ``` import { rateLimiter } from "./rate-limiter-create.js"; await rateLimiter.reset(); ``` ### Checking rate-limiter state[​](#checking-rate-limiter-state "Direct link to Checking rate-limiter state") You can get the rate-limiter state by using the `getState` method, it returns [`RateLimiterState`](https://eridu-tech.github.io/eridu-tech-core/types/RateLimiter.RateLimiterState.html). ./samples/rate-limiter-get-state.ts ``` import { rateLimiter } from "./rate-limiter-create.js"; import { RATE_LIMITER_STATE } from "eridu-tech/rate-limiter/contracts"; const state = await rateLimiter.getState(); if (state.type === RATE_LIMITER_STATE.EXPIRED) { console.log("The rate limiter key doesnt exists"); } if (state.type === RATE_LIMITER_STATE.ALLOWED) { console.log("The rate limiter is allowing calls"); } if (state.type === RATE_LIMITER_STATE.BLOCKED) { console.log("The rate limiter is blocking calls"); } ``` ### RateLimiter instance variables[​](#ratelimiter-instance-variables "Direct link to RateLimiter instance variables") The `RateLimiter` class exposes instance variables such as: ./samples/rate-limiter-instance-variables.ts ``` import { rateLimiterFactory } from "./rate-limiter-factory-initial-config.js"; const rateLimiter = rateLimiterFactory.create("resource", { limit: 10, }); // Will return the key of the rate-limiter which is "resource" console.log(rateLimiter.key); ``` ## Patterns[​](#patterns "Direct link to Patterns") ### Serialization and deserialization of rate-limiters[​](#serialization-and-deserialization-of-rate-limiters "Direct link to Serialization and deserialization of rate-limiters") rate-limiters can be serialized, allowing them to be transmitted over the network to another server and later deserialized for reuse. This means you can, for example, acquire the rate-limiter on the main server, transfer it to a queue worker server, and release it there. In order to serialize or deserialize a rate-limiter you need pass an object that implements [`ISerderRegister`](/docs/foundation/serde/.md) contract like the [`Serde`](/docs/foundation/serde/.md) class to `RateLimiterFactory`. Manually serializing and deserializing the rate-limiter: ./samples/rate-limiter-manual-serialization.ts ``` import { Redis } from "ioredis"; import { RedisRateLimiterAdapter } from "eridu-tech/rate-limiter/redis-rate-limiter-adapter"; import { RateLimiterFactory } from "eridu-tech/rate-limiter"; import { SuperJsonSerde } from "eridu-tech/serde/super-json-serde"; const serde = new SuperJsonSerde(); const redisClient = new Redis("YOUR_REDIS_CONNECTION"); const rateLimiterFactory = new RateLimiterFactory({ // You can laso pass in an array of Serde class instances serde, adapter: new RedisRateLimiterAdapter({ database: redisClient }), }); const rateLimiter = rateLimiterFactory.create("resource", { limit: 10, }); const serializedRateLimiter = await serde.serialize(rateLimiter); const deserializedRateLimiter = await serde.deserialize(serializedRateLimiter); ``` danger When serializing or deserializing a rate-limiter, you must use the same `Serde` instances that were provided to the `RateLimiterFactory`. This is required because the `RateLimiterFactory` injects custom serialization logic for `IRateLimiter` instance into `Serde` instances. info Note you only need manuall serialization and deserialization when integrating with external libraries. As long you pass the same `Serde` instances with all other components you dont need to serialize and deserialize the rate-limiter manually. ./samples/rate-limiter-event-bus-serialization.ts ``` import { Redis } from "ioredis"; import { RedisRateLimiterAdapter } from "eridu-tech/rate-limiter/redis-rate-limiter-adapter"; import type { IRateLimiter } from "eridu-tech/rate-limiter/contracts"; import { RateLimiterFactory } from "eridu-tech/rate-limiter"; import { RedisPubSubEventBusAdapter } from "eridu-tech/event-bus/redis-pub-sub-event-bus-adapter"; import { EventBus } from "eridu-tech/event-bus"; import { SuperJsonSerde } from "eridu-tech/serde/super-json-serde"; const serde = new SuperJsonSerde(); const redis = new Redis("YOUR_REDIS_CONNECTION"); type EventMap = { "sending-rate-limiter-over-network": { rateLimiter: IRateLimiter; }; }; const eventBus = new EventBus({ adapter: new RedisPubSubEventBusAdapter({ client: redis, serde, }), }); const rateLimiterFactory = new RateLimiterFactory({ serde, adapter: new RedisRateLimiterAdapter({ database: redis }), }); const rateLimiter = rateLimiterFactory.create("resource", { limit: 10, }); // We are sending the rateLimiter over the network to other servers. await eventBus.dispatch("sending-rate-limiter-over-network", { rateLimiter, }); // The other servers will recieve the serialized rateLimiter and automattically deserialize it. await eventBus.addListener( "sending-rate-limiter-over-network", ({ rateLimiter }) => { // The rateLimiter is deserialized and can be used console.log("RATE_LIMITER:", rateLimiter); }, ); ``` ### Separating rate-limiter creation from usage[​](#separating-rate-limiter-creation-from-usage "Direct link to Separating rate-limiter creation from usage") The library includes 2 additional contracts: * [`IRateLimiter`](https://eridu-tech.github.io/eridu-tech-core/types/RateLimiter.IRateLimiter.html) - Allows only for manipulating of the rate-limiter. * [`IRateLimiterFactory`](https://eridu-tech.github.io/eridu-tech-core/types/RateLimiter.IRateLimiterFactory.html) - Allows only for creation of rate-limiters. ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/rate-limiter`](https://eridu-tech.github.io/eridu-tech-core/modules/RateLimiter.html) API docs. --- # DI Integration The `rateLimiterFactoryResolverDiFactory` function creates an [`IRateLimiterFactoryResolver`](/docs/reliability/rate-limiter/rate_limiter_factory_resolver/.md) whose underlying resolver is resolved from a [DI container](/docs/foundation/di/.md). The returned proxy implements both `IRateLimiterFactoryResolver` and `IRateLimiterFactory`: * `use(adapterName?)` selects an adapter on the resolved `RateLimiterFactoryResolver`. * `create(key, settings)` creates a rate limiter through the resolver's default adapter. The proxy is wired to the container during `container.init()`. Create it **before** calling `init()`; calling `use()` or `create()` before the container is initialized throws an error. ## Initial configuration[​](#initial-configuration "Direct link to Initial configuration") Set up the memory and Redis rate-limiter adapters, create the container, then register the `RateLimiterFactoryResolver` and wrap it with `rateLimiterFactoryResolverDiFactory`: #### 1. Set up the memory rate-limiter adapter[​](#1-set-up-the-memory-rate-limiter-adapter "Direct link to 1. Set up the memory rate-limiter adapter") ./samples/memory-rate-limiter-adapter.ts ``` import { DatabaseRateLimiterAdapter } from "eridu-tech/rate-limiter/database-rate-limiter-adapter"; import { MemoryRateLimiterStorageAdapter } from "eridu-tech/rate-limiter/memory-rate-limiter-storage-adapter"; export const memoryRateLimiterAdapter = new DatabaseRateLimiterAdapter({ adapter: new MemoryRateLimiterStorageAdapter(), }); ``` #### 2. Set up the Redis rate-limiter adapter[​](#2-set-up-the-redis-rate-limiter-adapter "Direct link to 2. Set up the Redis rate-limiter adapter") ./samples/redis-rate-limiter-adapter.ts ``` import { RedisRateLimiterAdapter } from "eridu-tech/rate-limiter/redis-rate-limiter-adapter"; import { Redis } from "ioredis"; export const redisRateLimiterAdapter = new RedisRateLimiterAdapter({ database: new Redis("YOUR_REDIS_CONNECTION_STRING"), }); ``` #### 3. Create the dependency injection container[​](#3-create-the-dependency-injection-container "Direct link to 3. Create the dependency injection container") ./samples/container.ts ``` import { Container } from "eridu-tech/di"; import { ExecutionContext } from "eridu-tech/execution-context"; import { AlsExecutionContextAdapter } from "eridu-tech/execution-context/als-execution-context-adapter"; export const executionContext = new ExecutionContext( new AlsExecutionContextAdapter(), ); export const container = new Container({ executionContext }); ``` #### 4. Register and initialize the rate-limiter factory resolver[​](#4-register-and-initialize-the-rate-limiter-factory-resolver "Direct link to 4. Register and initialize the rate-limiter factory resolver") ./samples/rate-limiter-factory-resolver-di-factory.ts ``` import { RateLimiterFactoryResolver } from "eridu-tech/rate-limiter"; import { ProxyRateLimiterFactoryResolver } from "eridu-tech/rate-limiter/di"; import { container } from "./container.js"; import { memoryRateLimiterAdapter } from "./memory-rate-limiter-adapter.js"; import { redisRateLimiterAdapter } from "./redis-rate-limiter-adapter.js"; import { SuperJsonSerde } from "eridu-tech/serde/super-json-serde"; type Adapters = "memory" | "redis"; const serde = new SuperJsonSerde(); const rateLimiterFactoryResolver = new RateLimiterFactoryResolver({ serde, adapters: { memory: memoryRateLimiterAdapter, redis: redisRateLimiterAdapter, }, defaultAdapter: "memory", }); await rateLimiterFactoryResolver.init(); container.registerValue({ token: RateLimiterFactoryResolver, value: rateLimiterFactoryResolver, }); // Create the proxy before container.init() export const rateLimiterFactory = new ProxyRateLimiterFactoryResolver( { container, resolverToken: RateLimiterFactoryResolver, }, ); await container.init(); ``` ## Usage[​](#usage "Direct link to Usage") ### 1. Using the default adapter[​](#1-using-the-default-adapter "Direct link to 1. Using the default adapter") ./samples/rate-limiter-factory-resolver-di-factory-default-adapter.ts ``` import { rateLimiterFactory } from "./rate-limiter-factory-resolver-di-factory.js"; // Uses the adapter configured as the default (memory) await rateLimiterFactory .create("shared-resource", { limit: 10, }) .runOrFail(async () => { // code to run }); ``` ### 2. Specifying an adapter explicitly[​](#2-specifying-an-adapter-explicitly "Direct link to 2. Specifying an adapter explicitly") ./samples/rate-limiter-factory-resolver-di-factory-specific-adapter.ts ``` import { rateLimiterFactory } from "./rate-limiter-factory-resolver-di-factory.js"; // Uses the Redis adapter await rateLimiterFactory .use("redis") .create("shared-resource", { limit: 10, }) .runOrFail(async () => { // code to run }); ``` ## Using it with the middlewares[​](#using-it-with-the-middlewares "Direct link to Using it with the middlewares") Because the proxy implements `IRateLimiterFactoryResolver`, it can be passed straight to the [rate-limiter middleware factories](/docs/reliability/rate-limiter/rate_limiter_middlewares/.md): ./samples/rate-limiter-factory-resolver-di-factory-with-middleware.ts ``` import { use } from "eridu-tech/middleware"; import { withRateLimiterFactory } from "eridu-tech/rate-limiter/middlewares"; import { rateLimiterFactory } from "./rate-limiter-factory-resolver-di-factory.js"; const withRateLimiter = withRateLimiterFactory(rateLimiterFactory); const fetchHandler = async (request: Request): Promise => { // ... handle the request return new Response("OK"); }; const rateLimitedCall = use( fetchHandler, withRateLimiter.use("redis")({ key: ([req]) => `api:${String(req.headers.get("x-ip"))}`, limit: 10, }), ); await rateLimitedCall( new Request("/url", { method: "POST", }), ); ``` ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/rate-limiter`](https://eridu-tech.github.io/eridu-tech-core/modules/RateLimiter.html) API docs. --- # Resilience The `eridu-tech/resilience` component provides predefined fault tolerant `middlewares`. info For further information about `middlewares` refer to [`eridu-tech/middleware`](/docs/foundation/middleware/.md) documentation. ## Fallback[​](#fallback "Direct link to Fallback") The `fallback` middleware adds fallback value when an error occurs: ### Usage[​](#usage "Direct link to Usage") ./samples/fallback-usage.ts ``` import { fallback } from "eridu-tech/resilience"; import { use } from "eridu-tech/middleware"; async function unstableFn(): Promise { // We simulate a function that can throw unexpected errors if (Math.round(Math.random() * 1.5) === 0) { throw new Error("Unexpected error occurred"); } return Math.round((Math.random() + 1) * 99); } const fn = use(unstableFn, [ fallback({ fallbackValue: 1, }), ]); // Will never throw and when error occurs the fallback value will be returned. console.log(await fn()); ``` info You can provide synchronous or asynchronous [`Invocable<[], TValue | Promise>`](/docs/utilities/invocable/.md) as fallback value. ### Custom ErrorPolicy[​](#custom-errorpolicy "Direct link to Custom ErrorPolicy") You can define an [`ErrorPolicy`](/docs/utilities/error_policy_type/.md) to specify fallback values for specific error cases: ./samples/fallback-error-policy.ts ``` import { fallback } from "eridu-tech/resilience"; import { use } from "eridu-tech/middleware"; async function unstableFn(): Promise { // We simulate a function that can throw unexpected errors if (Math.round(Math.random() * 1.5) === 0) { throw new Error("Unexpected error occurred"); } return Math.round((Math.random() + 1) * 99); } const fn = use(unstableFn, [ fallback({ fallbackValue: 1, // Will only fallback errors that are not a TypeError errorPolicy: (error) => !(error instanceof TypeError), }), ]); await fn(); ``` ### Callbacks[​](#callbacks "Direct link to Callbacks") You can add callback [`Invocable`](/docs/utilities/invocable/.md) that will be called before the fallback value is returned. ./samples/fallback-on-fallback.ts ``` import { fallback } from "eridu-tech/resilience"; import { use } from "eridu-tech/middleware"; async function unstableFn(): Promise { // We simulate a function that can throw unexpected errors if (Math.round(Math.random() * 1.5) === 0) { throw new Error("Unexpected error occurred"); } return Math.round((Math.random() + 1) * 99); } const fn = use(unstableFn, [ fallback({ fallbackValue: 1, onFallback: (fallbackData) => console.log(fallbackData), }), ]); await fn(); ``` info For more details about `onFallback` callback data, see the OnFallbackData type. ## Retry[​](#retry "Direct link to Retry") The `retry` middleware enables automatic retries for all errors or specific errors, with configurable backoff policies. An error will be thrown when all retry attempts fail. ### Usage[​](#usage-1 "Direct link to Usage") ./samples/retry-usage.ts ``` import { retry } from "eridu-tech/resilience"; import { use } from "eridu-tech/middleware"; async function unstableFn(): Promise { // We simulate a function that can throw unexpected errors if (Math.round(Math.random() * 1.5) === 0) { throw new Error("Unexpected error occurred"); } return Math.round((Math.random() + 1) * 99); } const fn = use(unstableFn, [ retry({ // Will retry 4 times maxAttempts: 4, }), ]); await fn(); ``` ### Custom ErrorPolicy[​](#custom-errorpolicy-1 "Direct link to Custom ErrorPolicy") You can define an [`ErrorPolicy`](/docs/utilities/error_policy_type/.md) to retry specific error cases: ./samples/retry-error-policy.ts ``` import { retry } from "eridu-tech/resilience"; import { use } from "eridu-tech/middleware"; async function unstableFn(): Promise { // We simulate a function that can throw unexpected errors if (Math.round(Math.random() * 1.5) === 0) { throw new Error("Unexpected error occurred"); } return Math.round((Math.random() + 1) * 99); } const fn = use(unstableFn, [ retry({ maxAttempts: 4, // Will only retry errors that are not TypeError errorPolicy: (error) => !(error instanceof TypeError), }), ]); await fn(); ``` ### Throw last error[​](#throw-last-error "Direct link to Throw last error") By default, a `RetryResilienceError` is thrown when the time window expires. This error aggregates all errors encountered during the retry process. You can instead rethrow the last encountered error: ./samples/retry-throw-last-error.ts ``` import { retry } from "eridu-tech/resilience"; import { use } from "eridu-tech/middleware"; async function unstableFn(): Promise { // We simulate a function that can throw unexpected errors if (Math.round(Math.random() * 1.5) === 0) { throw new Error("Unexpected error occurred"); } return Math.round((Math.random() + 1) * 99); } const fn = use(unstableFn, [ retry({ maxAttempts: 4, throwLastError: true, }), ]); await fn(); ``` ### Custom BackoffPolicy[​](#custom-backoffpolicy "Direct link to Custom BackoffPolicy") You can use custom [`BackoffPolicy`](/docs/utilities/backoff_policies/.md): ./samples/retry-backoff-policy.ts ``` import { retry } from "eridu-tech/resilience"; import { use } from "eridu-tech/middleware"; import { TimeSpan } from "eridu-tech/time-span"; async function unstableFn(): Promise { // We simulate a function that can throw unexpected errors if (Math.round(Math.random() * 1.5) === 0) { throw new Error("Unexpected error occurred"); } return Math.round((Math.random() + 1) * 99); } const fn = use(unstableFn, [ retry({ maxAttempts: 4, // By default a exponential policy is used backoffPolicy: (attempt: number, _error: unknown) => TimeSpan.fromMilliseconds(attempt * 100), }), ]); await fn(); ``` ### Callbacks[​](#callbacks-1 "Direct link to Callbacks") You can add callback [`Invocable`](/docs/utilities/invocable/.md) that will be called before execution attempt: ./samples/retry-on-execution-attempt.ts ``` import { retry } from "eridu-tech/resilience"; import { use } from "eridu-tech/middleware"; async function unstableFn(): Promise { // We simulate a function that can throw unexpected errors if (Math.round(Math.random() * 1.5) === 0) { throw new Error("Unexpected error occurred"); } return Math.round((Math.random() + 1) * 99); } const fn = use(unstableFn, [ retry({ maxAttempts: 4, onExecutionAttempt: (data) => console.log(data), }), ]); await fn(); ``` You can add callback [`Invocable`](/docs/utilities/invocable/.md) that will be called before the retry delay starts: info For more details about `onExecutionAttempt` callback data, see the `OnRetryAttemptData` type. ./samples/retry-on-retry-delay.ts ``` import { retry } from "eridu-tech/resilience"; import { use } from "eridu-tech/middleware"; async function unstableFn(): Promise { // We simulate a function that can throw unexpected errors if (Math.round(Math.random() * 1.5) === 0) { throw new Error("Unexpected error occurred"); } return Math.round((Math.random() + 1) * 99); } const fn = use(unstableFn, [ retry({ maxAttempts: 4, onRetryDelay: (data) => console.log(data), }), ]); await fn(); ``` info For more details about `onRetryDelay` callback data, see the `OnRetryDelayData` type. ## Retry by interval[​](#retry_by_interval "Direct link to Retry by interval") The `retryInterval` middleware retries a function repeatedly within a given time window, waiting a fixed interval between each attempt. A `RetryIntervalResilienceError` is thrown when the time window expires and all attempts have failed. ### Usage[​](#usage-2 "Direct link to Usage") ./samples/retry-interval-usage.ts ``` import { retryInterval } from "eridu-tech/resilience"; import { use } from "eridu-tech/middleware"; import { TimeSpan } from "eridu-tech/time-span"; async function unstableFn(): Promise { // We simulate a function that can throw unexpected errors if (Math.round(Math.random() * 1.5) === 0) { throw new Error("Unexpected error occurred"); } return Math.round((Math.random() + 1) * 99); } const fn = use(unstableFn, [ retryInterval({ // Retry for up to 10 seconds time: TimeSpan.fromSeconds(10), // Wait 500ms between each attempt interval: TimeSpan.fromMilliseconds(500), }), ]); await fn(); ``` ### Custom ErrorPolicy[​](#custom-errorpolicy-2 "Direct link to Custom ErrorPolicy") You can define an [`ErrorPolicy`](/docs/utilities/error_policy_type/.md) to retry only specific error cases: ./samples/retry-interval-error-policy.ts ``` import { retryInterval } from "eridu-tech/resilience"; import { use } from "eridu-tech/middleware"; import { TimeSpan } from "eridu-tech/time-span"; async function unstableFn(): Promise { // We simulate a function that can throw unexpected errors if (Math.round(Math.random() * 1.5) === 0) { throw new Error("Unexpected error occurred"); } return Math.round((Math.random() + 1) * 99); } const fn = use(unstableFn, [ retryInterval({ time: TimeSpan.fromSeconds(10), interval: TimeSpan.fromMilliseconds(500), // Will only retry errors that are not a TypeError errorPolicy: (error) => !(error instanceof TypeError), }), ]); await fn(); ``` ### Throw last error[​](#throw-last-error-1 "Direct link to Throw last error") By default, a `RetryIntervalResilienceError` is thrown when the time window expires. This error aggregates all errors encountered during the retry process. You can instead rethrow the last encountered error: ./samples/retry-interval-throw-last-error.ts ``` import { retryInterval } from "eridu-tech/resilience"; import { use } from "eridu-tech/middleware"; import { TimeSpan } from "eridu-tech/time-span"; async function unstableFn(): Promise { // We simulate a function that can throw unexpected errors if (Math.round(Math.random() * 1.5) === 0) { throw new Error("Unexpected error occurred"); } return Math.round((Math.random() + 1) * 99); } const fn = use(unstableFn, [ retryInterval({ time: TimeSpan.fromSeconds(10), interval: TimeSpan.fromMilliseconds(500), throwLastError: true, }), ]); await fn(); ``` ### Callbacks[​](#callbacks-2 "Direct link to Callbacks") You can add callback [`Invocable`](/docs/utilities/invocable/.md) that will be called before each execution attempt: ./samples/retry-interval-on-execution-attempt.ts ``` import { retryInterval } from "eridu-tech/resilience"; import { use } from "eridu-tech/middleware"; import { TimeSpan } from "eridu-tech/time-span"; async function unstableFn(): Promise { // We simulate a function that can throw unexpected errors if (Math.round(Math.random() * 1.5) === 0) { throw new Error("Unexpected error occurred"); } return Math.round((Math.random() + 1) * 99); } const fn = use(unstableFn, [ retryInterval({ time: TimeSpan.fromSeconds(10), interval: TimeSpan.fromMilliseconds(500), onExecutionAttempt: (data) => console.log(data), }), ]); await fn(); ``` info For more details about `onExecutionAttempt` callback data, see the `OnRetryAttemptData` type. You can add callback [`Invocable`](/docs/utilities/invocable/.md) that will be called before the retry delay starts: ./samples/retry-interval-on-retry-delay.ts ``` import { retryInterval } from "eridu-tech/resilience"; import { use } from "eridu-tech/middleware"; import { TimeSpan } from "eridu-tech/time-span"; async function unstableFn(): Promise { // We simulate a function that can throw unexpected errors if (Math.round(Math.random() * 1.5) === 0) { throw new Error("Unexpected error occurred"); } return Math.round((Math.random() + 1) * 99); } const fn = use(unstableFn, [ retryInterval({ time: TimeSpan.fromSeconds(10), interval: TimeSpan.fromMilliseconds(500), onRetryDelay: (data) => console.log(data), }), ]); await fn(); ``` info For more details about `onRetryDelay` callback data, see the `OnRetryDelayData` type. ## Timeout[​](#timeout "Direct link to Timeout") The `timeout` middleware automatically aborts functions after a specified time period, throwing an error when aborted. ### Usage[​](#usage-3 "Direct link to Usage") ./samples/timeout-usage.ts ``` import { timeout } from "eridu-tech/resilience"; import { use } from "eridu-tech/middleware"; import { TimeSpan } from "eridu-tech/time-span"; async function fetchData(): Promise { const response = await fetch("ENDPOINT"); console.log("DONE"); return response; } const fn = use(fetchData, [ timeout({ waitTime: TimeSpan.fromSeconds(2), }), ]); await fn(); ``` ### Callbacks[​](#callbacks-3 "Direct link to Callbacks") You can add callback [`Invocable`](/docs/utilities/invocable/.md) that will be called before the timeout occurs. ./samples/timeout-on-timeout.ts ``` import { timeout } from "eridu-tech/resilience"; import { use } from "eridu-tech/middleware"; import { TimeSpan } from "eridu-tech/time-span"; async function fetchData(): Promise { const response = await fetch("ENDPOINT"); return response; } const fn = use(fetchData, [ timeout({ waitTime: TimeSpan.fromSeconds(2), onTimeout: (data) => console.log(data), }), ]); await fn(); ``` info For more details about `onTimeout` callback data, see the `OnTimeoutData` type. ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/resilience`](https://eridu-tech.github.io/eridu-tech-core/modules/Resilience.html) API docs. --- # Cache middlewares ## Initial configuration[​](#initial-configuration "Direct link to Initial configuration") To begin using the cache middlewares, you'll need to create and configure a `CacheResolver` instance: ./samples/cache.ts ``` import { CacheResolver } from "eridu-tech/cache"; import { MemoryCacheAdapter } from "eridu-tech/cache/memory-cache-adapter"; import { RedisCacheAdapter } from "eridu-tech/cache/redis-cache-adapter"; import { SuperJsonSerde } from "eridu-tech/serde/super-json-serde"; import { Redis } from "ioredis"; const serde = new SuperJsonSerde(); export const cacheResolver = new CacheResolver({ adapters: { memory: new MemoryCacheAdapter(), redis: new RedisCacheAdapter({ database: new Redis("YOUR_REDIS_CONNECTION_STRING"), serde, }), }, defaultAdapter: "memory", }); await cacheResolver.init(); export const cache = cacheResolver.use(); ``` ## withCacheFactory middleware[​](#with_cache_factory_middleware "Direct link to withCacheFactory middleware") The Cache middleware intercepts function calls and caches their return values using a configurable cache store. When the wrapped function is invoked, the middleware derives a cache key from the function's arguments. If the key exists in the cache, the cached value is returned immediately without executing the function. Otherwise, the function runs, its result is stored in the cache, and the result is returned. ### Usage[​](#usage "Direct link to Usage") ./samples/with-cache-factory.ts ``` import { withCacheFactory } from "eridu-tech/cache/middlewares"; import { use } from "eridu-tech/middleware"; import { TimeSpan } from "eridu-tech/time-span"; import { cacheResolver } from "./cache.js"; const withCache = withCacheFactory(cacheResolver); const fetchUser = async (userId: string): Promise<{ name: string }> => { const response = await fetch(`/api/users/${userId}`); return response.json(); }; // Wrap with caching using the default adapter (`memory`) const cachedFetchUser = use( fetchUser, withCache({ key: ([userId]) => `user:${userId}`, ttl: TimeSpan.fromMinutes(10), // Cache for 10 minutes }), ); // Wrap with caching using a specific adapter (`redis`) const cachedFetchUserOnRedis = use( fetchUser, withCache.use("redis")({ key: ([userId]) => `user:${userId}`, ttl: TimeSpan.fromMinutes(10), }), ); const user = await cachedFetchUser("123"); // Cache miss — fetches and caches const userAgain = await cachedFetchUser("123"); // Cache hit — returns immediately const userOnRedis = await cachedFetchUserOnRedis("123"); // Uses the Redis adapter ``` Calling `withCache()` directly uses the resolver's default adapter (`memory`). Use `withCache.use("redis")` to run the wrapped function through a specific registered adapter. info Here is a complete list of settings for the [`withCache`](https://eridu-tech.github.io/eridu-tech-core/types/Cache.WithCacheSettings.html) function. ## withInvalidationFactory middleware[​](#with_invalidation_factory_middleware "Direct link to withInvalidationFactory middleware") The Cache invalidation middleware intercepts function calls and invalidates a cache entry after the wrapped function has been invoked. The cache key is derived from the function's arguments via the `key` setting. After the wrapped function runs, the `shouldInvalidate` setting decides whether to invalidate based on the function's arguments and return value; when it returns `true` (the default), the cache entry is removed from the selected cache adapter. This is useful for write-invalidation caching patterns, where stale cached data must be cleared after a mutation. ### Usage[​](#usage-1 "Direct link to Usage") ./samples/with-invalidation.ts ``` import { withInvalidationFactory } from "eridu-tech/cache/middlewares"; import { use } from "eridu-tech/middleware"; import { cacheResolver } from "./cache.js"; const withInvalidation = withInvalidationFactory(cacheResolver); const updateUser = async (userId: string, name: string): Promise => { await fetch(`/api/users/${userId}`, { method: "PUT", body: JSON.stringify({ name }), }); }; // Wrap with invalidation using the default adapter (`memory`) const invalidatingUpdateUser = use( updateUser, withInvalidation({ key: ([userId]) => `user:${userId}`, }), ); // Wrap with invalidation using a specific adapter (`redis`) const invalidatingUpdateUserOnRedis = use( updateUser, withInvalidation.use("redis")({ key: ([userId]) => `user:${userId}`, }), ); await invalidatingUpdateUser("123", "John"); // The "user:123" cache entry is removed after updateUser runs await invalidatingUpdateUserOnRedis("456", "Jane"); // The "user:456" cache entry is removed from the Redis adapter ``` Use `withInvalidation()` to invalidate through the resolver's default adapter (`memory` in this example), or `withInvalidation.use("redis")({ ... })` to select a specific adapter. info Here is a complete list of settings for the [`withInvalidation`](https://eridu-tech.github.io/eridu-tech-core/types/Cache.WithInvalidationSettings.html) function. ### Settings[​](#settings "Direct link to Settings") | Option | Type | Description | | ------------------ | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | `key` | `Invocable` | A function (or invocable object) that produces the cache key from the wrapped function's arguments | | `shouldInvalidate` | `Invocable<[args: TParameters, returnValue: TReturn], boolean>` | Determines whether to invalidate the cache entry after the wrapped function runs, based on its arguments and return value. Defaults to `() => true` | ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/cache`](https://eridu-tech.github.io/eridu-tech-core/modules/Cache.html) API docs. --- # Cache Plugins ## withCachePrefix plugin[​](#withcacheprefix-plugin "Direct link to withCachePrefix plugin") The Cache prefix plugin intercepts calls to a cache adapter and transparently prefixes all cache keys with a configurable string. This enables logical key namespacing without modifying the adapter implementation. ### Use cases[​](#use-cases "Direct link to Use cases") * **Multi-tenant systems** — Prefix keys with a tenant identifier to isolate cache data between tenants * **Environment isolation** — Separate development, staging, and production cache data * **Versioning** — Prefix keys with a schema version for cache invalidation across deployments * **Module scoping** — Organize cache keys by feature or module to avoid collisions ### How it works[​](#how-it-works "Direct link to How it works") The `withCachePrefix` function returns a [`PluginFn`](/docs/foundation/middleware/.md) that calls `enhance` on each adapter method that accepts a cache key. When an enhanced method is invoked, the plugin intercepts the call, prepends the configured prefix to the key argument, and forwards the modified arguments to the original method. The plugin prefixes keys for the following methods: | Method | Key argument | Pattern | | ---------------- | ------------------------ | --------------------------- | | `get` | Second argument (`key`) | `prefix + key` | | `getAndRemove` | Second argument (`key`) | `prefix + key` | | `add` | Second argument (`key`) | `prefix + key` | | `getOrAdd` | Second argument (`key`) | `prefix + key` | | `put` | Second argument (`key`) | `prefix + key` | | `update` | Second argument (`key`) | `prefix + key` | | `increment` | Second argument (`key`) | `prefix + key` | | `removeMany` | Second argument (`keys`) | `keys.map(k => prefix + k)` | | `removeByPrefix` | Second argument (`key`) | `prefix + key` | Methods that do not accept a key (`removeAll`) are unaffected. ### Usage[​](#usage "Direct link to Usage") ./samples/with-cache-prefix.ts ``` import { withPlugin } from "eridu-tech/middleware"; import { MemoryCacheAdapter } from "eridu-tech/cache/memory-cache-adapter"; import { withCachePrefix } from "eridu-tech/cache/plugins"; const adapter = new MemoryCacheAdapter(); // Apply the prefix plugin to the adapter const prefixedAdapter = withPlugin(adapter, withCachePrefix("tenant-42:")); ``` ### Before/after behavior[​](#beforeafter-behavior "Direct link to Before/after behavior") **Before** — Keys are stored as-is: ./samples/unprefixed-lookup.ts ``` import { MemoryCacheAdapter } from "eridu-tech/cache/memory-cache-adapter"; const adapter = new MemoryCacheAdapter(); await adapter.get("user:123"); // -> looks up key "user:123" ``` **After** — Keys are automatically prefixed: ./samples/prefixed-lookup.ts ``` import { withPlugin } from "eridu-tech/middleware"; import { MemoryCacheAdapter } from "eridu-tech/cache/memory-cache-adapter"; import { withCachePrefix } from "eridu-tech/cache/plugins"; const adapter = new MemoryCacheAdapter(); // Apply the prefix plugin to the adapter const prefixedAdapter = withPlugin(adapter, withCachePrefix("tenant-42:")); await prefixedAdapter.get("user:123"); // -> looks up key "tenant-42:user:123" ``` danger Because `withPlugin` uses `enhance` under the hood, the same edge case applies: if one enhanced method internally calls another enhanced method via `this`, the middleware will apply **twice**. Be mindful of inter-method calls when applying plugins that enhance multiple methods on the same instance. info For more information about the `withPlugin` function and applying plugins to adapters, see the [Middleware plugin](/docs/foundation/middleware/.md#plugin) documentation. ### Multiple keys — `removeMany`[​](#multiple-keys--removemany "Direct link to multiple-keys--removemany") The `removeMany` method receives an array of keys. The plugin maps over the array, prefixing each entry: ./samples/remove-many-prefix.ts ``` import { withPlugin } from "eridu-tech/middleware"; import { MemoryCacheAdapter } from "eridu-tech/cache/memory-cache-adapter"; import { withCachePrefix } from "eridu-tech/cache/plugins"; const adapter = new MemoryCacheAdapter(); // Apply the prefix plugin to the adapter const prefixedAdapter = withPlugin(adapter, withCachePrefix("tenant-42:")); await prefixedAdapter.removeMany(["a", "b", "c"]); // -> prefixedAdapter.removeMany(["tenant-42:a", "tenant-42:b", "tenant-42:c"]) ``` ## withCacheJitter plugin[​](#withcachejitter-plugin "Direct link to withCacheJitter plugin") The Cache jitter plugin adds random jitter to TTL values on cache `add` and `put` operations. Applying jitter to TTLs helps prevent cache stampedes (thundering-herd problems) by staggering the expiration times of cache entries that were originally created with the same TTL. ### Use cases[​](#use-cases-1 "Direct link to Use cases") * **Cache stampede prevention** — Stagger cache entry expiry times to avoid multiple concurrent cache refreshes * **Load smoothing** — Distribute cache refresh load across time rather than having it spike at predictable intervals * **Distributed systems** — Reduce synchronized expiration across many cache nodes or instances ### How it works[​](#how-it-works-1 "Direct link to How it works") The `withCacheJitter` function returns a [`PluginFn`](/docs/foundation/middleware/.md) that calls `enhance` on the `add` and `put` methods of the adapter. When either method is invoked, the plugin intercepts the call, applies a random jitter factor to the TTL, and forwards the modified arguments to the original method. The jitter is calculated as a random percentage of the original TTL. For example, with the default `defaultJitter` of `0.2` (20 %), a TTL of 60 seconds will be randomly adjusted to somewhere between 48 and 72 seconds. | Method | TTL argument | Behaviour | | ---------- | -------------- | -------------------------------- | | `add` | Third argument | Applies random jitter to the TTL | | `add` | Third argument | Applies random jitter to the TTL | | `getOrAdd` | Third argument | Applies random jitter to the TTL | ### Usage[​](#usage-1 "Direct link to Usage") ./samples/with-cache-jitter.ts ``` import { withPlugin } from "eridu-tech/middleware"; import { MemoryCacheAdapter } from "eridu-tech/cache/memory-cache-adapter"; import { withCacheJitter } from "eridu-tech/cache/plugins"; const adapter = new MemoryCacheAdapter(); // Apply the jitter plugin to the adapter const jitteredAdapter = withPlugin(adapter, withCacheJitter()); ``` ### Settings[​](#settings "Direct link to Settings") | Option | Type | Default | Description | | --------------- | -------- | ------- | -------------------------------------------------------------------------- | | `defaultJitter` | `number` | `0.2` | The jitter factor as a ratio of the original TTL (e.g., `0.2` means ±20 %) | danger Because `withPlugin` uses `enhance` under the hood, the same edge case applies: if one enhanced method internally calls another enhanced method via `this`, the middleware will apply **twice**. Be mindful of inter-method calls when applying plugins that enhance multiple methods on the same instance. info For more information about the `withPlugin` function and applying plugins to adapters, see the [Middleware plugin](/docs/foundation/middleware/.md#plugin) documentation. ## withCacheWriteLock plugin[​](#withcachewritelock-plugin "Direct link to withCacheWriteLock plugin") The Cache write lock plugin acquires a distributed lock before executing mutating cache operations. It wraps write operations (`add`, `put`, `update`, `increment`, `getAndRemove`, `removeMany`) with a lock acquired via an [`ILockFactory`](/docs/concurrency/lock/lock_usage/.md), ensuring that concurrent writes to the same cache entry are serialised. ### Use cases[​](#use-cases-2 "Direct link to Use cases") * **Concurrency control** — Prevent race conditions when multiple processes write to the same cache key * **Distributed environments** — Coordinate writes across multiple application instances * **Critical sections** — Ensure exclusive access for read-modify-write operations like `increment` and `update` * **Batch safety** — Serialise operations on multiple keys in `removeMany` ### How it works[​](#how-it-works-2 "Direct link to How it works") The `withCacheWriteLock` function returns a [`PluginFn`](/docs/foundation/middleware/.md) that calls `enhance` on the selected mutating methods of the adapter. When an enhanced method is invoked, the plugin acquires a lock keyed by the cache key (or keys, for `removeMany`) before executing the operation. The lock is released automatically after the operation completes. The lock key is derived directly from the cache key, ensuring that concurrent writes to the same cache entry are serialised while writes to different entries can proceed in parallel. By default, all mutating methods are protected: | Method | Lock key source | Behaviour | | -------------- | --------------- | --------------------------------------------- | | `add` | Single key | Acquires lock for the key before adding | | `getOrAdd` | Single key | Acquires lock for the key before adding | | `put` | Single key | Acquires lock for the key before putting | | `update` | Single key | Acquires lock for the key before updating | | `increment` | Single key | Acquires lock for the key before incrementing | | `getAndRemove` | Single key | Acquires lock for the key before removing | | `removeMany` | Multiple keys | Acquires locks for each key sequentially | Read-only methods (`get`, `removeAll`, `removeByPrefix`) are unaffected. ### Usage[​](#usage-2 "Direct link to Usage") ./samples/with-cache-write-lock.ts ``` import { withPlugin } from "eridu-tech/middleware"; import { MemoryCacheAdapter } from "eridu-tech/cache/memory-cache-adapter"; import { withCacheWriteLock } from "eridu-tech/cache/plugins"; import { LockFactory } from "eridu-tech/lock"; import { MemoryLockAdapter } from "eridu-tech/lock/memory-lock-adapter"; const adapter = new MemoryCacheAdapter(); const lockFactory = new LockFactory({ adapter: new MemoryLockAdapter(), }); // Apply the write lock plugin to the adapter const lockedAdapter = withPlugin(adapter, withCacheWriteLock({ lockFactory })); ``` ### Settings[​](#settings-1 "Direct link to Settings") | Option | Type | Default | Description | | ------------- | ---------------------------------- | --------------------------------------------------------------------- | -------------------------------------------------- | | `lockFactory` | `ILockFactory` | *(required)* | A factory that creates named locks | | `onlyMethods` | `Array` | `["getAndRemove", "add", "put", "update", "increment", "removeMany"]` | The subset of methods to protect with a write lock | danger Because `withPlugin` uses `enhance` under the hood, the same edge case applies: if one enhanced method internally calls another enhanced method via `this`, the middleware will apply **twice**. Be mindful of inter-method calls when applying plugins that enhance multiple methods on the same instance. info For more information about the `withPlugin` function and applying plugins to adapters, see the [Middleware plugin](/docs/foundation/middleware/.md#plugin) documentation. For more information about lock factories, see the [Lock](/docs/concurrency/lock/lock_usage/.md) documentation. --- # CacheResolver The `CacheResolver` class provides a flexible way to configure and switch between different cache adapters at runtime. ## Initial configuration[​](#initial-configuration "Direct link to Initial configuration") To begin using the `CacheResolver`, you will need to register all required adapters during initialization. ./samples/cache-resolver-initial-config.ts ``` import { CacheResolver } from "eridu-tech/cache"; import { MemoryCacheAdapter } from "eridu-tech/cache/memory-cache-adapter"; import { RedisCacheAdapter } from "eridu-tech/cache/redis-cache-adapter"; import { SuperJsonSerde } from "eridu-tech/serde/super-json-serde"; import { Redis } from "ioredis"; type User = { name: string; age: number; }; const serde = new SuperJsonSerde(); export const cacheResolver = new CacheResolver<"memory" | "redis", User>({ adapters: { memory: new MemoryCacheAdapter(), redis: new RedisCacheAdapter({ database: new Redis("YOUR_REDIS_CONNECTION"), serde, }), }, // You can set an optional default adapter defaultAdapter: "memory", }); await cacheResolver.init(); ``` ## Usage[​](#usage "Direct link to Usage") ### 1. Using the default adapter[​](#1-using-the-default-adapter "Direct link to 1. Using the default adapter") ./samples/cache-resolver-default-adapter.ts ``` import { cacheResolver } from "./cache-resolver-initial-config.js"; await cacheResolver.use().add("user/jose@gmail.com", { name: "Jose", age: 20, }); ``` danger Note that if you dont set a default adapter, an error will be thrown. ### 2. Specifying an adapter explicitly[​](#2-specifying-an-adapter-explicitly "Direct link to 2. Specifying an adapter explicitly") ./samples/cache-resolver-specific-adapter.ts ``` import { cacheResolver } from "./cache-resolver-initial-config.js"; await cacheResolver.use("redis").add("user/jose@gmail.com", { name: "Jose", age: 20, }); ``` danger Note that if you specify a non-existent adapter, an error will be thrown. ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/cache`](https://eridu-tech.github.io/eridu-tech-core/modules/Cache.html) API docs. --- # Cache usage The `eridu-tech/cache` component provides a way for storing key-value pairs with expiration independent of data storage ## Initial configuration[​](#initial-configuration "Direct link to Initial configuration") To begin using the `Cache` class, you'll need to create and configure an instance: ./samples/cache-initial-config.ts ``` import { TimeSpan } from "eridu-tech/time-span"; import { MemoryCacheAdapter } from "eridu-tech/cache/memory-cache-adapter"; import { Cache } from "eridu-tech/cache"; export const cache = new Cache({ // You can provide default TTL value // If you set it to null it means keys will be stored forever. defaultTtl: TimeSpan.fromSeconds(2), // You can choose the adapter to use adapter: new MemoryCacheAdapter(), }); ``` info Here is a complete list of settings for the [`Cache`](https://eridu-tech.github.io/eridu-tech-core/types/Cache.CacheSettingsBase.html) class. ## Cache basics[​](#cache-basics "Direct link to Cache basics") ### Adding keys[​](#adding-keys "Direct link to Adding keys") You can add a key with a optional TTL to overide the default: ./samples/cache-add.ts ``` import { cache } from "./cache-initial-config.js"; import { TimeSpan } from "eridu-tech/time-span"; await cache.add("a", "value", TimeSpan.fromSeconds(1)); ``` The method returns true if the key does not exists. ### Retrieving keys[​](#retrieving-keys "Direct link to Retrieving keys") You can retrieve the key: ./samples/cache-get.ts ``` import { cache } from "./cache-initial-config.js"; await cache.get("a"); ``` ### Checking key existence[​](#checking-key-existence "Direct link to Checking key existence") You can check if the key exists: ./samples/cache-exists.ts ``` import { cache } from "./cache-initial-config.js"; await cache.exists("a"); ``` You can check if the key is missing: ./samples/cache-missing.ts ``` import { cache } from "./cache-initial-config.js"; await cache.missing("a"); ``` ### Updating keys[​](#updating-keys "Direct link to Updating keys") You can update a key and true will be returned if the key exists and was updated: ./samples/cache-update.ts ``` import { cache } from "./cache-initial-config.js"; await cache.update("a", 2); ``` You can increment the a key and true will be returned if the key exists and was updated. If the key is not a number an error will be thrown: ./samples/cache-increment.ts ``` import { cache } from "./cache-initial-config.js"; await cache.increment("a", 2); ``` You can decrement the a key and true will be returned if the key exists and was updated. If the key is not a number an error will be thrown,: ./samples/cache-decrement.ts ``` import { cache } from "./cache-initial-config.js"; await cache.decrement("a", 1); ``` You can perform an upsert that replaces the ttl when updated. True will be returned if the key was updated otherwise false is returned: ./samples/cache-put.ts ``` import { cache } from "./cache-initial-config.js"; import { TimeSpan } from "eridu-tech/time-span"; await cache.put("a", 2); await cache.put("a", 4, TimeSpan.fromSeconds(3)); ``` ### Removing keys[​](#removing-keys "Direct link to Removing keys") You can remove a key and true will be returned if the key was found and removed: ./samples/cache-remove.ts ``` import { cache } from "./cache-initial-config.js"; await cache.remove("a"); ``` You can remove multiple keys and true will be returned if one of the keys exists and where removed: ./samples/cache-remove-many.ts ``` import { cache } from "./cache-initial-config.js"; await cache.removeMany(["a", "b"]); ``` You can clear all the keys of the given namespace: ./samples/cache-clear.ts ``` import { cache } from "./cache-initial-config.js"; await cache.clear(); ``` ## Patterns[​](#patterns "Direct link to Patterns") ### Compile time type safety[​](#compile-time-type-safety "Direct link to Compile time type safety") You can enforce compile time type safety by setting the cache value type: ./samples/compile-time-type-safety.ts ``` import { MemoryCacheAdapter } from "eridu-tech/cache/memory-cache-adapter"; import { Cache } from "eridu-tech/cache"; type IUser = { name: string; email: string; age: number; }; const cache = new Cache({ adapter: new MemoryCacheAdapter(), }); // A typescript error will occur because the type is not matching. await cache.add("a", "asd"); ``` If you have multiple types you can use algeberical enums: ./samples/cache-union-types.ts ``` import { MemoryCacheAdapter } from "eridu-tech/cache/memory-cache-adapter"; import { Cache } from "eridu-tech/cache"; type IUser = { type: "USER"; name: string; email: string; age: number; }; type IProduct = { type: "PRODUCT"; name: string; price: number; }; type CacheValue = IUser | IProduct; const cache = new Cache({ adapter: new MemoryCacheAdapter(), }); const cacheValue = await cache.get("user1"); // You need to check the type is "USER" inorder to access IUser fields. if (cacheValue.type === "USER") { console.log(cacheValue.name, cacheValue.age); } // You need to check the type is "PRODUCT" inorder to access IProduct fields. if (cacheValue.type === "PRODUCT") { console.log(cacheValue.name, cacheValue.price); } ``` Alternatively you can use different `Cache` classes with different namespaces: ./samples/cache-multiple-namespaces.ts ``` import { MemoryCacheAdapter } from "eridu-tech/cache/memory-cache-adapter"; import { Cache } from "eridu-tech/cache"; const cacheAdapter = new MemoryCacheAdapter(); type IUser = { name: string; email: string; age: number; }; const userCache = new Cache({ adapter: cacheAdapter, }); type IProduct = { name: string; price: number; }; const productCache = new Cache({ adapter: cacheAdapter, }); ``` ### Runtime type safety[​](#runtime-type-safety "Direct link to Runtime type safety") You can validate cache values against a standard-schema-compliant schema by providing the `schema` setting. This works with any library that implements the `StandardSchemaV1` specification, such as Zod, ArkType and Valibot. When a schema is provided, values are validated: * **On write** — before a value is stored, for the `add`, `put`, `update` and `getOrAdd` methods. * **On read** — when `shouldValidateOutput` is `true` (the default), values returned by `get`, `getAndRemove` and `getOrAdd` are validated on retrieval. This catches malformed data already present in the cache at read time, instead of silently returning it. If validation fails, a `ValidationError` is thrown. ./samples/cache-runtime-validation.ts ``` import { MemoryCacheAdapter } from "eridu-tech/cache/memory-cache-adapter"; import { Cache } from "eridu-tech/cache"; import { z } from "zod"; const userSchema = z.object({ name: z.string(), email: z.string().email(), age: z.number(), }); const cache = new Cache({ adapter: new MemoryCacheAdapter(), schema: userSchema, }); await cache.add("user1", { name: "John", email: "john@example.com", age: 30, }); // Throws a ValidationError because the email is not valid await cache.add("user2", { name: "Jane", email: "not-an-email", age: "25", }); ``` #### Disabling output validation[​](#disabling-output-validation "Direct link to Disabling output validation") If you only want to validate values on write and skip validation when reading, set `shouldValidateOutput` to `false`: ./samples/cache-disable-output-validation.ts ``` import { MemoryCacheAdapter } from "eridu-tech/cache/memory-cache-adapter"; import { Cache } from "eridu-tech/cache"; import { z } from "zod"; const userSchema = z.object({ name: z.string(), email: z.string().email(), age: z.number(), }); const cache = new Cache({ adapter: new MemoryCacheAdapter(), schema: userSchema, shouldValidateOutput: false, }); ``` ### Additional methods[​](#additional-methods "Direct link to Additional methods") You can retrieve the key and if it does not exist an error will be thrown: ./samples/cache-get-or-fail.ts ``` import { cache } from "./cache-initial-config.js"; await cache.getOrFail("ab"); ``` You can retrieve the key and if it does not exist you can return a default value: ./samples/cache-get-or.ts ``` import { cache } from "./cache-initial-config.js"; await cache.getOr("ab", 1); ``` You can retrieve the key and if it does not exist you can insert a default value that will aslo be returned: ./samples/cache-get-or-add.ts ``` import { cache } from "./cache-initial-config.js"; await cache.getOrAdd("ab", 1); ``` You can retrieve the key and afterwards remove it: ./samples/cache-get-and-remove.ts ``` import { cache } from "./cache-initial-config.js"; await cache.getAndRemove("ab"); ``` You can add key and if it does exist an error will be thrown: ./samples/cache-add-or-fail.ts ``` import { cache } from "./cache-initial-config.js"; await cache.addOrFail("ab", 1); ``` You can update the key and if it does not exist an error will be thrown: ./samples/cache-update-or-fail.ts ``` import { cache } from "./cache-initial-config.js"; await cache.updateOrFail("ab", 1); ``` You can increment the key and if it does not exist an error will be thrown: ./samples/cache-increment-or-fail.ts ``` import { cache } from "./cache-initial-config.js"; await cache.incrementOrFail("ab", 1); ``` You can decrement the key and if it does not exist an error will be thrown: ./samples/cache-decrement-or-fail.ts ``` import { cache } from "./cache-initial-config.js"; await cache.decrementOrFail("ab", 1); ``` You can remove the key and if it does not exist an error will be thrown: ./samples/cache-remove-or-fail.ts ``` import { cache } from "./cache-initial-config.js"; await cache.removeOrFail("ab"); ``` ### Separating cache reading from manipulation[​](#separating-cache-reading-from-manipulation "Direct link to Separating cache reading from manipulation") The library includes 2 additional contracts: * [`IReadableCache`](https://eridu-tech.github.io/eridu-tech-core/types/Cache.IReadableCache.html) - Allows only for reading cache. * [`ICache`](https://eridu-tech.github.io/eridu-tech-core/types/Cache.ICache.html) - Allows for both reading and manipulating the cache. This separation makes it easy to visually distinguish the two contracts, making it immediately obvious that they serve different purposes. ./samples/cache-read-write-contracts.ts ``` import type { ICache, IReadableCache } from "eridu-tech/cache/contracts"; import { Cache } from "eridu-tech/cache"; import { MemoryCacheAdapter } from "eridu-tech/cache/memory-cache-adapter"; async function readingFunc(cache: IReadableCache): Promise { // You cannot access write methods like put, add and update // You will get typescript error if you try console.log("reading only:", await cache.get("a")); } async function manipulatingFunc(cache: ICache): Promise { // You will get typescript error if you try await cache.add("a", 1); console.log("writing and reading:", await cache.get("a")); } const cache = new Cache({ adapter: new MemoryCacheAdapter(), }); await manipulatingFunc(cache); await readingFunc(cache); ``` ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/cache`](https://eridu-tech.github.io/eridu-tech-core/modules/Cache.html) API docs. --- # DI Integration The `cacheResolverDiFactory` function creates an [`ICacheResolver`](/docs/storage/cache/cache_resolver/.md) whose underlying resolver is resolved from a [DI container](/docs/foundation/di/.md). The returned proxy implements both `ICacheResolver` and `ICache`: * `use(adapterName?)` selects an adapter on the resolved `CacheResolver`. * The `ICache` methods such as `get`, `add` and `getOrAdd` delegate to the resolver's default adapter. The proxy is wired to the container during `container.init()`. Create it **before** calling `init()`; calling `use()` or a cache method before the container is initialized throws an error. ## Initial configuration[​](#initial-configuration "Direct link to Initial configuration") Set up the memory and Redis cache adapters, create the container, then register the `CacheResolver` and wrap it with `cacheResolverDiFactory`: #### 1. Set up the memory cache adapter[​](#1-set-up-the-memory-cache-adapter "Direct link to 1. Set up the memory cache adapter") ./samples/memory-cache-adapter.ts ``` import { MemoryCacheAdapter } from "eridu-tech/cache/memory-cache-adapter"; export const memoryCacheAdapter = new MemoryCacheAdapter(); ``` #### 2. Set up the Redis cache adapter[​](#2-set-up-the-redis-cache-adapter "Direct link to 2. Set up the Redis cache adapter") ./samples/redis-cache-adapter.ts ``` import { RedisCacheAdapter } from "eridu-tech/cache/redis-cache-adapter"; import { SuperJsonSerde } from "eridu-tech/serde/super-json-serde"; import { Redis } from "ioredis"; export const redisCacheAdapter = new RedisCacheAdapter({ database: new Redis("YOUR_REDIS_CONNECTION_STRING"), serde: new SuperJsonSerde(), }); ``` #### 3. Create the dependency injection container[​](#3-create-the-dependency-injection-container "Direct link to 3. Create the dependency injection container") ./samples/container.ts ``` import { Container } from "eridu-tech/di"; import { ExecutionContext } from "eridu-tech/execution-context"; import { AlsExecutionContextAdapter } from "eridu-tech/execution-context/als-execution-context-adapter"; export const executionContext = new ExecutionContext( new AlsExecutionContextAdapter(), ); export const container = new Container({ executionContext }); ``` #### 4. Register and initialize the cache resolver[​](#4-register-and-initialize-the-cache-resolver "Direct link to 4. Register and initialize the cache resolver") ./samples/cache-resolver-di-factory.ts ``` import { CacheResolver } from "eridu-tech/cache"; import { ProxyCacheResolver } from "eridu-tech/cache/di"; import { container } from "./container.js"; import { memoryCacheAdapter } from "./memory-cache-adapter.js"; import { redisCacheAdapter } from "./redis-cache-adapter.js"; type Adapters = "memory" | "redis"; const cacheResolver = new CacheResolver({ adapters: { memory: memoryCacheAdapter, redis: redisCacheAdapter, }, defaultAdapter: "memory", }); await cacheResolver.init(); container.registerValue({ token: CacheResolver, value: cacheResolver, }); // Create the proxy before container.init() export const cache = new ProxyCacheResolver({ container, resolverToken: CacheResolver, }); await container.init(); ``` ## Usage[​](#usage "Direct link to Usage") ### 1. Using the default adapter[​](#1-using-the-default-adapter "Direct link to 1. Using the default adapter") ./samples/cache-resolver-di-factory-default-adapter.ts ``` import { cache } from "./cache-resolver-di-factory.js"; // Uses the adapter configured as the default (memory) await cache.add("user/jose@gmail.com", { name: "Jose", age: 20, }); ``` ### 2. Specifying an adapter explicitly[​](#2-specifying-an-adapter-explicitly "Direct link to 2. Specifying an adapter explicitly") ./samples/cache-resolver-di-factory-specific-adapter.ts ``` import { cache } from "./cache-resolver-di-factory.js"; // Uses the Redis adapter await cache.use("redis").add("user/jose@gmail.com", { name: "Jose", age: 20, }); ``` ## Using it with the middlewares[​](#using-it-with-the-middlewares "Direct link to Using it with the middlewares") Because the proxy implements `ICacheResolver`, it can be passed straight to the [cache middleware factories](/docs/storage/cache/cache_middlewares/.md): ./samples/cache-resolver-di-factory-with-middleware.ts ``` import { withCacheFactory } from "eridu-tech/cache/middlewares"; import { use } from "eridu-tech/middleware"; import { TimeSpan } from "eridu-tech/time-span"; import { cache } from "./cache-resolver-di-factory.js"; const withCache = withCacheFactory(cache); const fetchUser = async (userId: string): Promise<{ name: string }> => { const response = await fetch(`/api/users/${userId}`); return response.json(); }; const cachedFetchUser = use( fetchUser, withCache.use("redis")({ key: ([userId]) => `user:${userId}`, ttl: TimeSpan.fromMinutes(10), }), ); await cachedFetchUser("123"); ``` ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/cache`](https://eridu-tech.github.io/eridu-tech-core/modules/Cache.html) API docs. --- # Configuring Cache adapters ## MemoryCacheAdapter[​](#memorycacheadapter "Direct link to MemoryCacheAdapter") To use the `MemoryCacheAdapter` you only need to create instance of it: ./samples/memory-cache-adapter.ts ``` import { MemoryCacheAdapter } from "eridu-tech/cache/memory-cache-adapter"; export const memoryCacheAdapter = new MemoryCacheAdapter(); ``` You can also provide an `Map` that will be used for storing the data in memory: ./samples/memory-cache-adapter-with-map.ts ``` import { MemoryCacheAdapter } from "eridu-tech/cache/memory-cache-adapter"; const map = new Map(); const memoryCacheAdapter = new MemoryCacheAdapter(map); ``` info `MemoryCacheAdapter` lets you test your app without external dependencies like `Redis`, ideal for local development, unit tests, integration tests and fast E2E test for the backend application. ### Settings[​](#settings "Direct link to Settings") To clean up expired cache keys, call `removeAllExpired` at a regular interval (for example, using a cron job): ./samples/memory-cache-remove-all-expired.ts ``` import { MemoryCacheAdapter } from "eridu-tech/cache/memory-cache-adapter"; const memoryCacheAdapter = new MemoryCacheAdapter(); // Remove all expired cache keys manually. await memoryCacheAdapter.removeAllExpired(); ``` info Note `removeAllExpired` must be called to remove expired data that is no longer being used. info To remove the cache map and all stored cache data, use `deInit` method: ./samples/memory-cache-adapter-deinit.ts ``` import { memoryCacheAdapter } from "./memory-cache-adapter.js"; await memoryCacheAdapter.deInit(); ``` ## MongodbCacheAdapter[​](#mongodbcacheadapter "Direct link to MongodbCacheAdapter") To use the `MongodbCacheAdapter`, you'll need to: 1. Install the required dependency: [`mongodb`](https://www.npmjs.com/package/mongodb) package. 2. Provide a string serializer ([`ISerde`](/docs/foundation/serde/.md)). We recommend using `SuperJsonSerdeAdapter` for this purpose. ### Setup[​](#setup "Direct link to Setup") Connect to MongoDB and create the string serializer: ./samples/mongodb-cache-adapter-setup.ts ``` import { SuperJsonSerde } from "eridu-tech/serde/super-json-serde"; import { MongoClient } from "mongodb"; export const serde = new SuperJsonSerde(); const client = await MongoClient.connect("YOUR_MONGODB_CONNECTION_STRING"); export const database = client.db("database"); ``` info The `database` setting also accepts a [`TransactionContext`](/docs/foundation/transaction_context/transaction_context_usage/.md), which makes the adapter transaction aware. Adapters given the same instance share the same transaction when available. ### Usage[​](#usage "Direct link to Usage") Create the adapter: ./samples/mongodb-cache-adapter-init.ts ``` import { MongodbCacheAdapter } from "eridu-tech/cache/mongodb-cache-adapter"; import { database, serde } from "./mongodb-cache-adapter-setup.js"; export const mongodbCacheAdapter = new MongodbCacheAdapter({ database, serde, }); // You need initialize the adapter once before using it. // During the initialization the indexes will be created await mongodbCacheAdapter.init(); ``` You can change the collection name: ./samples/mongodb-cache-collection-name.ts ``` import { MongodbCacheAdapter } from "eridu-tech/cache/mongodb-cache-adapter"; import { database, serde } from "./mongodb-cache-adapter-setup.js"; const mongodbCacheAdapter = new MongodbCacheAdapter({ database, serde, // By default "cache" is used as collection name collectionName: "my-cache", }); await mongodbCacheAdapter.init(); ``` You can change the collection settings: ./samples/mongodb-cache-collection-settings.ts ``` import { MongodbCacheAdapter } from "eridu-tech/cache/mongodb-cache-adapter"; import { database, serde } from "./mongodb-cache-adapter-setup.js"; const mongodbCacheAdapter = new MongodbCacheAdapter({ database, serde, // You configure additional collection settings collectionSettings: {}, }); await mongodbCacheAdapter.init(); ``` info To remove the cache collection and all stored cache data, use `deInit` method: ./samples/mongodb-cache-adapter-deinit.ts ``` import { mongodbCacheAdapter } from "./mongodb-cache-adapter-init.js"; await mongodbCacheAdapter.deInit(); ``` ## RedisCacheAdapter[​](#rediscacheadapter "Direct link to RedisCacheAdapter") To use the `RedisCacheAdapter`, you'll need to: 1. Install the required dependency: [`ioredis`](https://www.npmjs.com/package/ioredis) package. 2. Provide a string serializer ([`ISerde`](/docs/foundation/serde/.md)). We recommend using `SuperJsonSerdeAdapter` for this purpose. ### Setup[​](#setup-1 "Direct link to Setup") Connect to Redis and create the string serializer: ./samples/redis-cache-adapter-setup.ts ``` import { SuperJsonSerde } from "eridu-tech/serde/super-json-serde"; import { Redis } from "ioredis"; export const serde = new SuperJsonSerde(); export const database = new Redis("YOUR_REDIS_CONNECTION_STRING"); ``` ### Usage[​](#usage-1 "Direct link to Usage") Create the adapter: ./samples/redis-cache-adapter.ts ``` import { RedisCacheAdapter } from "eridu-tech/cache/redis-cache-adapter"; import { database, serde } from "./redis-cache-adapter-setup.js"; const redisCacheAdapter = new RedisCacheAdapter({ database, serde, }); ``` ## KyselyCacheAdapter[​](#kyselycacheadapter "Direct link to KyselyCacheAdapter") To use the `KyselyCacheAdapter`, you'll need to: 1. Install the required dependency: [`kysely`](https://www.npmjs.com/package/kysely) package. 2. Provide a string serializer ([`ISerde`](/docs/foundation/serde/.md)). We recommend using `SuperJsonSerdeAdapter` for this purpose. 3. Provide a [`TransactionContext`](/docs/foundation/transaction_context/transaction_context_usage/.md) that wraps the `Kysely` instance. The adapter is transaction aware: every cache operation runs through the context's `current` client, so cache reads and writes join the transaction of the current scope. ### Setup[​](#setup-2 "Direct link to Setup") Create the string serializer ([`ISerde`](/docs/foundation/serde/.md)) and a function that creates the [`TransactionContext`](/docs/foundation/transaction_context/transaction_context_usage/.md) by wrapping a `Kysely` instance in a [`KyselyTransactionAdapter`](/docs/foundation/transaction_context/configuring_transaction_context_adapters/.md#kysely_transaction_adapter): ./samples/kysely-cache-adapter-setup.ts ``` import { ExecutionContext } from "eridu-tech/execution-context"; import { AlsExecutionContextAdapter } from "eridu-tech/execution-context/als-execution-context-adapter"; import { contextToken } from "eridu-tech/execution-context/contracts"; import { SuperJsonSerde } from "eridu-tech/serde/super-json-serde"; import { TransactionContext } from "eridu-tech/transaction-context"; import { KyselyTransactionAdapter } from "eridu-tech/transaction-context/kysely-transaction-adapter"; import type { ITransactionContext } from "eridu-tech/transaction-context/contracts"; import type { Kysely } from "kysely"; export const serde = new SuperJsonSerde(); const executionContext = new ExecutionContext(new AlsExecutionContextAdapter()); // `KyselyCacheAdapter` is transaction aware: it runs every cache operation // through the `current` client of this context, which is the transaction-scoped // client while a transaction is active and the base client otherwise. export function createTransactionContext( kysely: Kysely, ): ITransactionContext> { return new TransactionContext>({ token: contextToken("kysely"), adapter: new KyselyTransactionAdapter({ database: kysely, }), executionContext, }); } ``` info The `transactionContext` setting makes the adapter transaction aware. Adapters given the same instance share the same transaction when available. ### Usage with Sqlite[​](#usage-with-sqlite "Direct link to Usage with Sqlite") You will need to install [`better-sqlite3`](https://www.npmjs.com/package/better-sqlite3) package: ./samples/kysely-cache-sqlite.ts ``` import { KyselyCacheAdapter } from "eridu-tech/cache/kysely-cache-adapter"; import Sqlite from "better-sqlite3"; import { Kysely, SqliteDialect } from "kysely"; import { createTransactionContext, serde, } from "./kysely-cache-adapter-setup.js"; const database = new Sqlite("DATABASE_NAME.db"); const kysely = new Kysely({ dialect: new SqliteDialect({ database, }), }); const transactionContext = createTransactionContext(kysely); export const kyselyCacheAdapter = new KyselyCacheAdapter({ transactionContext, serde, }); // You need initialize the adapter once before using it. // During the initialization the schema will be created await kyselyCacheAdapter.init(); ``` ### Usage with Postgres[​](#usage-with-postgres "Direct link to Usage with Postgres") You will need to install [`pg`](https://www.npmjs.com/package/pg) package: ./samples/kysely-cache-postgres.ts ``` import { KyselyCacheAdapter } from "eridu-tech/cache/kysely-cache-adapter"; import { Pool } from "pg"; import { Kysely, PostgresDialect } from "kysely"; import { createTransactionContext, serde, } from "./kysely-cache-adapter-setup.js"; const database = new Pool({ database: "DATABASE_NAME", host: "DATABASE_HOST", user: "DATABASE_USER", // DATABASE port port: 5432, password: "DATABASE_PASSWORD", max: 10, }); const kysely = new Kysely({ dialect: new PostgresDialect({ pool: database, }), }); const transactionContext = createTransactionContext(kysely); const kyselyCacheAdapter = new KyselyCacheAdapter({ transactionContext, serde, }); // You need initialize the adapter once before using it. // During the initialization the schema will be created await kyselyCacheAdapter.init(); ``` ### Usage with other databases[​](#usage-with-other-databases "Direct link to Usage with other databases") Note [`kysely`](https://www.npmjs.com/package/kysely) has support for multiple [databases](https://github.com/kysely-org/awesome-kysely?tab=readme-ov-file#dialects). danger Before choosing a database, ensure it supports transactions, because `KyselyCacheAdapter` runs all of its operations through the `TransactionContext`. Without transaction support, starting a transaction fails and you won't be able to use the `put` and `increment` methods. ### Settings[​](#settings-1 "Direct link to Settings") To clean up expired cache keys, call `removeAllExpired` at a regular interval (for example, using a cron job): ./samples/kysely-cache-remove-all-expired.ts ``` import { kyselyCacheAdapter } from "./kysely-cache-sqlite.js"; // Remove all expired cache keys manually. await kyselyCacheAdapter.removeAllExpired(); ``` info To remove the cache table and all stored cache data, use `deInit` method: ./samples/kysely-cache-adapter-deinit.ts ``` import { kyselyCacheAdapter } from "./kysely-cache-sqlite.js"; await kyselyCacheAdapter.deInit(); ``` ## NoOpCacheAdapter[​](#noopcacheadapter "Direct link to NoOpCacheAdapter") The `NoOpCacheAdapter` is a no-operation implementation, it performs no actions when called: ./samples/no-op-cache-adapter.ts ``` import { NoOpCacheAdapter } from "eridu-tech/cache/no-op-cache-adapter"; const noOpCacheAdapter = new NoOpCacheAdapter(); ``` info The `NoOpCacheAdapter` is useful when you want to mock out or disable your [`Cache`](https://eridu-tech.github.io/eridu-tech-core/classes/Cache.Cache.html) class instance. info Note `NoOpCacheAdapter` returns always null when retrieving items and return true when adding, updating, and removing items. ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/cache`](https://eridu-tech.github.io/eridu-tech-core/modules/Cache.html) API docs. --- # Creating cache adapters ## Implementing your custom ICacheAdapter[​](#implementing-your-custom-icacheadapter "Direct link to Implementing your custom ICacheAdapter") In order to create an adapter you need to implement the [`ICacheAdapter`](https://eridu-tech.github.io/eridu-tech-core/types/Cache.ICacheAdapter.html) contract. ## Testing your custom ICacheAdapter[​](#testing-your-custom-icacheadapter "Direct link to Testing your custom ICacheAdapter") We provide a complete test suite to test your cache adapter implementation. Simply use the [`cacheAdapterTestSuite`](https://eridu-tech.github.io/eridu-tech-core/functions/Cache.cacheAdapterTestSuite.html) function: The suite provides preconfigured Vitest test cases with common edge case coverage and standardized cache adapter contract conformance testing. Usage example: ./samples/cache-adapter-test-suite.ts ``` import { beforeEach, describe, expect, test } from "vitest"; import { cacheAdapterTestSuite } from "eridu-tech/cache/test-utilities"; import { MemoryCacheAdapter } from "eridu-tech/cache/memory-cache-adapter"; describe("class: MyCacheAdapter", () => { cacheAdapterTestSuite({ createAdapter: () => new MemoryCacheAdapter(), test, beforeEach, expect, describe, }); }); ``` ## Implementing your custom ICache class[​](#implementing-your-custom-icache-class "Direct link to Implementing your custom ICache class") In some cases, you may need to implement a custom [`Cache`](https://eridu-tech.github.io/eridu-tech-core/classes/Cache.Cache.html) class to optimize performance for your specific technology stack. You can then directly implement the [`ICache`](https://eridu-tech.github.io/eridu-tech-core/types/Cache.ICache.html) contract. ## Testing your custom ICache class[​](#testing-your-custom-icache-class "Direct link to Testing your custom ICache class") We provide a complete test suite to verify your custom cache class implementation. Simply use the [`cacheTestSuite`](https://eridu-tech.github.io/eridu-tech-core/functions/Cache.cacheTestSuite.html) function: The suite provides preconfigured Vitest test cases with common edge case coverage and standardized cache contract conformance testing. Usage example: ./samples/cache-test-suite.ts ``` import { beforeEach, describe, expect, test } from "vitest"; import { cacheTestSuite } from "eridu-tech/cache/test-utilities"; import { Cache } from "eridu-tech/cache"; import { MemoryCacheAdapter } from "eridu-tech/cache/memory-cache-adapter"; describe("class: MyCache", () => { cacheTestSuite({ createCache: () => new Cache({ adapter: new MemoryCacheAdapter(), }), test, beforeEach, expect, describe, }); }); ``` ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/cache`](https://eridu-tech.github.io/eridu-tech-core/modules/Cache.html) API docs. --- # Configuring FileStorage adapters ## MemoryFileStorageAdapter[​](#memoryfilestorageadapter "Direct link to MemoryFileStorageAdapter") To use the `MemoryFileStorageAdapter` you only need to create instance of it: ./samples/memory-file-storage-adapter.ts ``` import { MemoryFileStorageAdapter } from "eridu-tech/file-storage/memory-file-storage-adapter"; const memoryFileStorageAdapter = new MemoryFileStorageAdapter(); ``` You can also provide an `Map` that will be used for storing the files in memory: ./samples/memory-file-storage-adapter-with-map.ts ``` import { MemoryFileStorageAdapter } from "eridu-tech/file-storage/memory-file-storage-adapter"; const map = new Map(); const memoryFileStorageAdapter = new MemoryFileStorageAdapter(map); ``` info `MemoryFileStorageAdapter` lets you test your app without external dependencies like `@aws-sdk/client-s3`, ideal for local development, unit tests, integration tests and fast E2E test for the backend application. warning Note this adapter doesnt have support for creating signed upload, signed download and public urls. ## FsFileStorageAdapter[​](#fsfilestorageadapter "Direct link to FsFileStorageAdapter") To use the `FsFileStorageAdapter` you only need to create an instance of it, every setting is optional: ./samples/fs-file-storage-adapter.ts ``` import { FsFileStorageAdapter } from "eridu-tech/file-storage/fs-file-storage-adapter"; const fsFileStorageAdapter = new FsFileStorageAdapter({}); ``` You can configure the root folder: ./samples/fs-file-storage-adapter-custom-location.ts ``` import { FsFileStorageAdapter } from "eridu-tech/file-storage/fs-file-storage-adapter"; const fsFileStorageAdapter = new FsFileStorageAdapter({ location: "/my-custom-location", }); ``` You can configure codec used for file names: ./samples/fs-file-storage-adapter-with-codec.ts ``` import { Base64Codec } from "eridu-tech/codec/base-64-codec"; import { FsFileStorageAdapter } from "eridu-tech/file-storage/fs-file-storage-adapter"; const fsFileStorageAdapter = new FsFileStorageAdapter({ codec: new Base64Codec(), }); ``` warning Not encoding and decoding is required for `FsFileStorageAdapter` to maintain a flat hierarchy within the root folder and to ensure compatibility with OS-restricted characters. warning Not this adapter does not support signed upload, signed download and public urls. It also doesnt support explictly setting the content-type and it will instead infer the content-type from the file name. ## S3FileStorageAdapter[​](#s3filestorageadapter "Direct link to S3FileStorageAdapter") To use the `S3FileStorageAdapter`, you'll need to: 1. Install the required dependency: [`@aws-sdk/client-s3`](https://www.npmjs.com/package/@aws-sdk/client-s3) package: ./samples/s3-file-storage-adapter.ts ``` import { S3FileStorageAdapter } from "eridu-tech/file-storage/s3-file-storage-adapter"; import { S3Client } from "@aws-sdk/client-s3"; const s3Client = new S3Client({ credentials: { accessKeyId: "AWS_ACCESS_KEY_ID", secretAccessKey: "AWS_SECRET_ACCESS_KEY", }, region: "AWS_REGION", }); const s3FileStorageAdapter = new S3FileStorageAdapter({ client: s3Client, bucket: "bucket", }); ``` Other settings: ./samples/s3-file-storage-adapter-settings.ts ``` import { S3FileStorageAdapter, defaultPublicUrlGenerator, } from "eridu-tech/file-storage/s3-file-storage-adapter"; import { S3Client } from "@aws-sdk/client-s3"; const s3Client = new S3Client({ credentials: { accessKeyId: "AWS_ACCESS_KEY_ID", secretAccessKey: "AWS_SECRET_ACCESS_KEY", }, region: "AWS_REGION", }); const s3FileStorageAdapter = new S3FileStorageAdapter({ client: s3Client, /** * The bucket option defines the S3 bucket to use for managing files. */ bucket: "bucket", /** * The cdnUrl field can be used to define the base URL for generating public URL for a file. For example, If you use CloudFront alongside S3 to serve public files, the cdnUrl property should be the CloudFront URL. */ cdnUrl: null, /** * Define ServerSideEncryption option for all objects uploaded to S3. */ serverSideEncryption: "AES256", /** * Define a custom public url generator for creating public and signed URLs. */ publicUrlGenerator: defaultPublicUrlGenerator, }); ``` info Note this adapter with object storage services that are compatible with aws s3 like: * Cloudflare r2 * Digital ocean spaces * Tigris * Supabase Storage * Minio ## SftpFileStorageAdapter[​](#sftpfilestorageadapter "Direct link to SftpFileStorageAdapter") To use the `SftpFileStorageAdapter`, you'll need to: 1. Install the required dependency: [`ssh2-sftp-client`](https://www.npmjs.com/package/ssh2-sftp-client) package. 2. Create a pool of SFTP connections with `sftpPool`, which combines `SftpFactory` with a `QueuePool`. ./samples/sftp-file-storage-adapter.ts ``` import { SftpFileStorageAdapter, sftpPool, } from "eridu-tech/file-storage/sftp-file-storage-adapter"; const sftpFileStorageAdapter = new SftpFileStorageAdapter({ pool: sftpPool({ connectionSettings: { host: "sftp.example.com", port: 22, username: "user", password: "password", }, }), }); ``` Other settings: ./samples/sftp-file-storage-adapter-settings.ts ``` import { Base64Codec } from "eridu-tech/codec/base-64-codec"; import { PATH_TYPE, SftpFileStorageAdapter, sftpPool, } from "eridu-tech/file-storage/sftp-file-storage-adapter"; const sftpFileStorageAdapter = new SftpFileStorageAdapter({ pool: sftpPool({ // Forwarded to client.connect() connectionSettings: { host: "sftp.example.com", port: 22, username: "user", password: "password", retries: 3, }, }), // Writable root folder within the SFTP user's chroot location: "/uploads", delimiter: "/", codec: new Base64Codec(), pathType: PATH_TYPE.POSIX, }); ``` You can build the pool yourself, for example to change the pool size or to share it between adapters: ./samples/sftp-file-storage-adapter-custom-pool.ts ``` import { SftpFactory, SftpFileStorageAdapter, } from "eridu-tech/file-storage/sftp-file-storage-adapter"; import { QueuePool } from "eridu-tech/generic-pool"; const sftpFileStorageAdapter = new SftpFileStorageAdapter({ pool: new QueuePool( new SftpFactory({ connectionSettings: { host: "sftp.example.com", port: 22, username: "user", password: "password", }, }), { maxResources: 5, maxWaitSize: 100, }, ), }); ``` info `SftpFactory` connects a new `SftpClient` in `create`, checks idle connections with `cwd()` in `validate`, and ends them in `destroy`. Any `IGenericPoolBase` can be passed as the `pool` setting. warning This adapter does not support signed upload, signed download and public urls. It also doesn't support explicitly setting the content-type and will instead infer the content-type from the file name. ## FtpFileStorageAdapter[​](#ftpfilestorageadapter "Direct link to FtpFileStorageAdapter") To use the `FtpFileStorageAdapter`, you'll need to: 1. Install the required dependency: [`basic-ftp`](https://www.npmjs.com/package/basic-ftp) package. 2. Create a pool of FTP connections with `ftpPool`, which combines `FtpFactory` with a `QueuePool`. ./samples/ftp-file-storage-adapter.ts ``` import { FtpFileStorageAdapter, ftpPool, } from "eridu-tech/file-storage/ftp-file-storage-adapter"; const ftpFileStorageAdapter = new FtpFileStorageAdapter({ pool: ftpPool({ accessSettings: { host: "ftp.example.com", port: 21, user: "user", password: "password", // Use FTPS over TLS secure: true, }, }), }); ``` Other settings: ./samples/ftp-file-storage-adapter-settings.ts ``` import { Base64Codec } from "eridu-tech/codec/base-64-codec"; import { FtpFileStorageAdapter, PATH_TYPE, ftpPool, } from "eridu-tech/file-storage/ftp-file-storage-adapter"; const ftpFileStorageAdapter = new FtpFileStorageAdapter({ pool: ftpPool({ // How the connection is opened and logged in accessSettings: { host: "ftp.example.com", port: 21, user: "user", password: "password", secure: true, }, // Client-level options clientSettings: { allowSeparateTransferHost: false, }, // Socket timeout in milliseconds, 0 disables it timeout: 30_000, }), // Writable root folder within the FTP user's chroot location: "/uploads", delimiter: "/", codec: new Base64Codec(), pathType: PATH_TYPE.POSIX, }); ``` You can build the pool yourself, for example to change the pool size or to share it between adapters: ./samples/ftp-file-storage-adapter-custom-pool.ts ``` import { FtpFactory, FtpFileStorageAdapter, } from "eridu-tech/file-storage/ftp-file-storage-adapter"; import { QueuePool } from "eridu-tech/generic-pool"; const ftpFileStorageAdapter = new FtpFileStorageAdapter({ pool: new QueuePool( new FtpFactory({ accessSettings: { host: "ftp.example.com", port: 21, user: "user", password: "password", secure: true, }, }), { maxResources: 5, maxWaitSize: 100, }, ), }); ``` info Set `secure` to `true` for explicit FTPS over TLS, or to `"implicit"` for legacy implicit FTPS. `FtpFactory` opens and logs in a `Client` in `create`, checks idle connections with `pwd()` in `validate`, and closes them in `destroy`. Any `IGenericPoolBase` can be passed as the `pool` setting. warning This adapter does not support signed upload, signed download and public urls. It also doesn't support explicitly setting the content-type and will instead infer the content-type from the file name. ## WebdavFileStorageAdapter[​](#webdavfilestorageadapter "Direct link to WebdavFileStorageAdapter") To use the `WebdavFileStorageAdapter`, you'll need to: 1. Install the required dependency: [`webdav`](https://www.npmjs.com/package/webdav) package. 2. Create a client with `createClient` and pass it to the adapter. ./samples/webdav-file-storage-adapter.ts ``` import { WebdavFileStorageAdapter } from "eridu-tech/file-storage/webdav-file-storage-adapter"; import { createClient } from "webdav"; const webdavClient = createClient("https://webdav.example.com/files", { username: "user", password: "password", }); const webdavFileStorageAdapter = new WebdavFileStorageAdapter({ client: webdavClient, }); ``` Other settings: ./samples/webdav-file-storage-adapter-settings.ts ``` import { Base64Codec } from "eridu-tech/codec/base-64-codec"; import { WebdavFileStorageAdapter } from "eridu-tech/file-storage/webdav-file-storage-adapter"; import { createClient } from "webdav"; const webdavClient = createClient("https://webdav.example.com", { username: "user", password: "password", // Prefix for every path, when the server is mounted on a sub path remoteBasePath: "files", headers: { "X-Custom-Header": "value", }, }); const webdavFileStorageAdapter = new WebdavFileStorageAdapter({ client: webdavClient, delimiter: "/", codec: new Base64Codec(), }); ``` info The adapter keeps the WebDAV client it is given, so a single client serves every operation. Configure authentication, headers and `remoteBasePath` on the client itself. warning This adapter does not support signed upload, signed download and public urls. It also doesn't support explicitly setting the content-type and will instead infer the content-type from the file name. ## SignedFileStorageAdapter[​](#signedfilestorageadapter "Direct link to SignedFileStorageAdapter") The `SignedFileStorageAdapter` merges a regular file storage adapter with a URL adapter into a single [`ISignedFileStorageAdapter`](https://eridu-tech.github.io/eridu-tech-core/types/FileStorage.ISignedFileStorageAdapter.html). The `adapter` handles all file CRUD operations (create, read, update, delete, copy and move), while the `urlAdapter` handles public and signed URL generation. This is useful for storage backends where file operations and signed URL generation are handled separately, for example a database-backed storage adapter combined with an S3 URL adapter. Since `S3FileStorageAdapter` already implements signed URL generation, you typically use `SignedFileStorageAdapter` when you need to pair a regular adapter (that does not generate URLs) with your own URL generation logic. To use the `SignedFileStorageAdapter` you need to provide: * `adapter`: The underlying file storage adapter that handles file operations. It is not required to implement signed URL generation. * `urlAdapter`: A partial URL adapter for generating public and signed URLs. Only the URL methods your storage backend supports need to be provided. Basic usage: ./samples/signed-file-storage-adapter.ts ``` import { SignedFileStorageAdapter } from "eridu-tech/file-storage/signed-file-storage-adapter"; import { MemoryFileStorageAdapter } from "eridu-tech/file-storage/memory-file-storage-adapter"; const signedFileStorageAdapter = new SignedFileStorageAdapter({ adapter: new MemoryFileStorageAdapter(), urlAdapter: {}, }); ``` You can provide the URL methods that your storage backend supports: ./samples/signed-file-storage-adapter-url-methods.ts ``` import { SignedFileStorageAdapter } from "eridu-tech/file-storage/signed-file-storage-adapter"; import { MemoryFileStorageAdapter } from "eridu-tech/file-storage/memory-file-storage-adapter"; import type { FileAdapterSignedDownloadUrlSettings, FileAdapterSignedUploadUrlSettings, } from "eridu-tech/file-storage/contracts"; // The following helpers generate the signed URLs for your storage backend. // Implement them using your storage provider SDK (e.g. AWS S3 presigned URLs). async function generateSignedDownloadUrl( key: string, settings: FileAdapterSignedDownloadUrlSettings, ): Promise { // ... generate the signed download URL return null; } async function generateSignedUploadUrl( key: string, settings: FileAdapterSignedUploadUrlSettings, ): Promise { // ... generate the signed upload URL return ""; } const signedFileStorageAdapter = new SignedFileStorageAdapter({ adapter: new MemoryFileStorageAdapter(), urlAdapter: { async getPublicUrl(key: string): Promise { return `https://cdn.example.com/${key}`; }, async getSignedDownloadUrl( key: string, settings: FileAdapterSignedDownloadUrlSettings, ): Promise { return generateSignedDownloadUrl(key, settings); }, async getSignedUploadUrl( key: string, settings: FileAdapterSignedUploadUrlSettings, ): Promise { return generateSignedUploadUrl(key, settings); }, }, }); ``` info Any omitted URL method falls back to a no-op implementation: * `getPublicUrl` returns `null` * `getSignedDownloadUrl` returns `null` * `getSignedUploadUrl` returns an empty string ## NoOpFileStorageAdapter[​](#noopfilestorageadapter "Direct link to NoOpFileStorageAdapter") The `NoOpFileStorageAdapter` is a no-operation implementation, it performs no actions when called: ./samples/no-op-file-storage-adapter.ts ``` import { NoOpFileStorageAdapter } from "eridu-tech/file-storage/no-op-file-storage-adapter"; const noOpFileStorageAdapter = new NoOpFileStorageAdapter(); ``` info The `NoOpFileStorageAdapter` is useful when you want to mock out or disable your `FileStorage` instance. ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/file-storage`](https://eridu-tech.github.io/eridu-tech-core/modules/file-storage.html) API docs. --- # Creating FileStorage adapters ## Implementing your custom IFileStorageAdapter[​](#implementing-your-custom-ifilestorageadapter "Direct link to Implementing your custom IFileStorageAdapter") In order to create an adapter you need to implement the [`IFileStorageAdapter`](https://eridu-tech.github.io/eridu-tech-core/types/file-storage.IFileStorageAdapter.html) contract. ## Implementing your custom ISignedFileStorageAdapter[​](#implementing-your-custom-isignedfilestorageadapter "Direct link to Implementing your custom ISignedFileStorageAdapter") We provide an additional contract [`ISignedFileStorageAdapter`](https://eridu-tech.github.io/eridu-tech-core/types/FileStorage.ISignedFileStorageAdapter.html) for building custom FileStorage adapters with support for creating signed download and upload urls. ## Implementing your custom IFileStorage class[​](#implementing-your-custom-ifilestorage-class "Direct link to Implementing your custom IFileStorage class") In some cases, you may need to implement a custom [`FileStorage`](https://eridu-tech.github.io/eridu-tech-core/classes/file-storage.FileStorage.html) class to optimize performance for your specific technology stack. You can then directly implement the [`IFileStorage`](https://eridu-tech.github.io/eridu-tech-core/types/file-storage.IFileStorage.html) contract. ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/file-storage`](https://eridu-tech.github.io/eridu-tech-core/modules/file-storage.html) API docs. --- # FileStorage Plugins ## withFileStoragePrefix plugin[​](#withfilestorageprefix-plugin "Direct link to withFileStoragePrefix plugin") The FileStorage prefix plugin intercepts calls to a file-storage adapter and transparently prefixes all file keys with a configurable string. This enables logical key namespacing without modifying the adapter implementation. ### Use cases[​](#use-cases "Direct link to Use cases") * **Multi-tenant storage** — Prefix file keys with a tenant identifier to isolate files between tenants * **Environment isolation** — Separate development, staging, and production file storage * **Directory scoping** — Organize files into virtual directories by prepending a path prefix * **Bucket consolidation** — Use a single storage bucket/container with namespaced keys instead of multiple buckets ### How it works[​](#how-it-works "Direct link to How it works") The `withFileStoragePrefix` function returns a [`PluginFn`](/docs/foundation/middleware/.md) that calls `enhance` on each adapter method that accepts a file key. When an enhanced method is invoked, the plugin intercepts the call, prepends the configured prefix to the key argument, and forwards the modified arguments to the original method. The plugin prefixes keys for the following methods: | Method | Key argument | Pattern | | ---------------------- | --------------------------------------------------- | ----------------------------------------- | | `getPublicUrl` | first argument (`key`) | `prefix + key` | | `getSignedDownloadUrl` | first argument (`key`) | `prefix + key` | | `getSignedUploadUrl` | first argument (`key`) | `prefix + key` | | `exists` | first argument (`key`) | `prefix + key` | | `getStream` | first argument (`key`) | `prefix + key` | | `getBytes` | first argument (`key`) | `prefix + key` | | `getMetaData` | first argument (`key`) | `prefix + key` | | `put` | first argument (`key`) | `prefix + key` | | `putStream` | first argument (`key`) | `prefix + key` | | `copy` | first and second argument (`source`, `destination`) | `prefix + source`, `prefix + destination` | | `move` | first and second argument (`source`, `destination`) | `prefix + source`, `prefix + destination` | | `removeMany` | first argument (`keys`) | `keys.map(k => prefix + k)` | | `removeByPrefix` | first argument (`key`) | `prefix + key` | #### Copy and move behavior[​](#copy-and-move-behavior "Direct link to Copy and move behavior") For the `copy` and `move` methods, both the **destination** and **source** keys are prefixed. ### Usage[​](#usage "Direct link to Usage") ./samples/with-file-storage-prefix.ts ``` import { withPlugin } from "eridu-tech/middleware"; import { MemoryFileStorageAdapter } from "eridu-tech/file-storage/memory-file-storage-adapter"; import { withFileStoragePrefix } from "eridu-tech/file-storage/plugins"; const adapter = new MemoryFileStorageAdapter(); // Apply the prefix plugin to the adapter const prefixedAdapter = withPlugin( adapter, withFileStoragePrefix("tenant-42/"), ); ``` ### Before/after behavior[​](#beforeafter-behavior "Direct link to Before/after behavior") **Before** — File keys are used as-is: ./samples/unprefixed-get-bytes.ts ``` import { MemoryFileStorageAdapter } from "eridu-tech/file-storage/memory-file-storage-adapter"; const adapter = new MemoryFileStorageAdapter(); await adapter.getBytes("uploads/report.pdf"); // -> retrieves "uploads/report.pdf" ``` **After** — File keys are automatically prefixed: ./samples/prefixed-get-bytes.ts ``` import { withPlugin } from "eridu-tech/middleware"; import { MemoryFileStorageAdapter } from "eridu-tech/file-storage/memory-file-storage-adapter"; import { withFileStoragePrefix } from "eridu-tech/file-storage/plugins"; const adapter = new MemoryFileStorageAdapter(); // Apply the prefix plugin to the adapter const prefixedAdapter = withPlugin( adapter, withFileStoragePrefix("tenant-42/"), ); await prefixedAdapter.getBytes("uploads/report.pdf"); // -> retrieves "tenant-42/uploads/report.pdf" ``` danger Because `withPlugin` uses `enhance` under the hood, the same edge case applies: if one enhanced method internally calls another enhanced method via `this`, the middleware will apply **twice**. Be mindful of inter-method calls when applying plugins that enhance multiple methods on the same instance. info For more information about the `withPlugin` function and applying plugins to adapters, see the [Middleware plugin](/docs/foundation/middleware/.md#plugin) documentation. ### Multiple keys — `removeMany`[​](#multiple-keys--removemany "Direct link to multiple-keys--removemany") The `removeMany` method receives an array of keys. The plugin maps over the array, prefixing each entry: ./samples/remove-many-prefix.ts ``` import { withPlugin } from "eridu-tech/middleware"; import { MemoryFileStorageAdapter } from "eridu-tech/file-storage/memory-file-storage-adapter"; import { withFileStoragePrefix } from "eridu-tech/file-storage/plugins"; const adapter = new MemoryFileStorageAdapter(); // Apply the prefix plugin to the adapter const prefixedAdapter = withPlugin( adapter, withFileStoragePrefix("tenant-42/"), ); await prefixedAdapter.removeMany(["a.pdf", "b.pdf"]); // -> prefixedAdapter.removeMany(["tenant-42/a.pdf", "tenant-42/b.pdf"]) ``` ## withFileStorageKeyValidator plugin[​](#withfilestoragekeyvalidator-plugin "Direct link to withFileStorageKeyValidator plugin") The FileStorage key validator plugin validates every file key before it reaches a file-storage adapter. When a key fails validation, an `InvalidKeyFileError` is thrown and the underlying adapter method is never invoked. ### Use cases[​](#use-cases-1 "Direct link to Use cases") * **Path traversal protection** — Reject keys containing `../` to prevent escaping the intended storage directory * **Input sanitization** — Block keys containing control characters such as newlines or tabs * **Empty key detection** — Reject keys that are empty or consist only of whitespace * **Custom validation rules** — Provide your own validator to enforce project-specific key constraints ### How it works[​](#how-it-works-1 "Direct link to How it works") The `withFileStorageKeyValidator` function returns a [`PluginFn`](/docs/foundation/middleware/.md) that calls `enhance` on every adapter method that accepts a file key. When an enhanced method is invoked, the plugin runs the key through the configured validator before forwarding the call. If the validator returns an error message, the plugin throws an `InvalidKeyFileError` and the adapter method is never invoked. By default the plugin uses the built-in `defaultKeyValidator`, which rejects keys that contain `"../"`, a newline (`\n`), or a tab (`\t`), as well as keys that are empty or consist only of whitespace. A custom validator can be supplied as the first argument. The plugin validates keys for the following methods: | Method | Key argument | | ---------------------- | ---------------------- | | `getPublicUrl` | Single key | | `getSignedDownloadUrl` | Single key | | `getSignedUploadUrl` | Single key | | `exists` | Single key | | `getStream` | Single key | | `getBytes` | Single key | | `getMetaData` | Single key | | `put` | Single key | | `putStream` | Single key | | `copy` | Source and destination | | `move` | Source and destination | | `removeMany` | Multiple keys | | `removeByPrefix` | Single key (prefix) | ### Usage[​](#usage-1 "Direct link to Usage") ./samples/with-file-storage-key-validator.ts ``` import { withPlugin } from "eridu-tech/middleware"; import { MemoryFileStorageAdapter } from "eridu-tech/file-storage/memory-file-storage-adapter"; import { withFileStorageKeyValidator } from "eridu-tech/file-storage/plugins"; const adapter = new MemoryFileStorageAdapter(); // Apply the key validator plugin to the adapter const validatedAdapter = withPlugin(adapter, withFileStorageKeyValidator()); ``` #### Custom validator[​](#custom-validator "Direct link to Custom validator") ./samples/file-storage-key-validator-custom.ts ``` import { withPlugin } from "eridu-tech/middleware"; import { MemoryFileStorageAdapter } from "eridu-tech/file-storage/memory-file-storage-adapter"; import { withFileStorageKeyValidator } from "eridu-tech/file-storage/plugins"; const adapter = new MemoryFileStorageAdapter(); // Apply the key validator plugin with a custom validator const validatedAdapter = withPlugin( adapter, withFileStorageKeyValidator((key) => { if (key.startsWith("temp/")) { return "Keys under temp/ are not allowed"; } return null; }), ); ``` danger Because `withPlugin` uses `enhance` under the hood, the same edge case applies: if one enhanced method internally calls another enhanced method via `this`, the middleware will apply **twice**. Be mindful of inter-method calls when applying plugins that enhance multiple methods on the same instance. info For more information about the `withPlugin` function and applying plugins to adapters, see the [Middleware plugin](/docs/foundation/middleware/.md#plugin) documentation. ## withFileStorageLowerCase plugin[​](#withfilestoragelowercase-plugin "Direct link to withFileStorageLowerCase plugin") The FileStorage lowercase plugin intercepts calls to a file-storage adapter and lowercases every file key before it reaches the underlying adapter. This enforces a consistent, case-insensitive key format without modifying the adapter implementation. ### Use cases[​](#use-cases-2 "Direct link to Use cases") * **Normalized keys** — Store and retrieve files with a consistent lowercase key format * **Case-insensitive lookups** — Avoid duplicate files that differ only by case * **Cross-system consistency** — Match keys to systems that are case-sensitive or case-insensitive differently ### How it works[​](#how-it-works-2 "Direct link to How it works") The `withFileStorageLowerCase` function returns a [`PluginFn`](/docs/foundation/middleware/.md) that calls `enhance` on every adapter method that accepts a file key. When an enhanced method is invoked, the plugin lowercases the key argument(s) and forwards the modified arguments to the original method. The plugin lowercases keys for the following methods: | Method | Key argument | Pattern | | ---------------------- | ---------------------- | -------------------- | | `getPublicUrl` | Single key | `key.toLowerCase()` | | `getSignedDownloadUrl` | Single key | `key.toLowerCase()` | | `getSignedUploadUrl` | Single key | `key.toLowerCase()` | | `exists` | Single key | `key.toLowerCase()` | | `getStream` | Single key | `key.toLowerCase()` | | `getBytes` | Single key | `key.toLowerCase()` | | `getMetaData` | Single key | `key.toLowerCase()` | | `put` | Single key | `key.toLowerCase()` | | `putStream` | Single key | `key.toLowerCase()` | | `copy` | Source and destination | both keys lowercased | | `move` | Source and destination | both keys lowercased | | `removeMany` | Multiple keys | each key lowercased | | `removeByPrefix` | Single key (prefix) | `key.toLowerCase()` | ### Usage[​](#usage-2 "Direct link to Usage") ./samples/with-file-storage-lower-case.ts ``` import { withPlugin } from "eridu-tech/middleware"; import { MemoryFileStorageAdapter } from "eridu-tech/file-storage/memory-file-storage-adapter"; import { withFileStorageLowerCase } from "eridu-tech/file-storage/plugins"; const adapter = new MemoryFileStorageAdapter(); // Apply the lowercase plugin to the adapter const loweredAdapter = withPlugin(adapter, withFileStorageLowerCase()); ``` danger Because `withPlugin` uses `enhance` under the hood, the same edge case applies: if one enhanced method internally calls another enhanced method via `this`, the middleware will apply **twice**. Be mindful of inter-method calls when applying plugins that enhance multiple methods on the same instance. info For more information about the `withPlugin` function and applying plugins to adapters, see the [Middleware plugin](/docs/foundation/middleware/.md#plugin) documentation. ## withFileStorageInferContentTypeOnWrite plugin[​](#withfilestorageinfercontenttypeonwrite-plugin "Direct link to withFileStorageInferContentTypeOnWrite plugin") The FileStorage write content-type plugin infers the content type from the file key extension when writing files or generating signed URLs. It uses the `mime-types` lookup to resolve a content type from the key extension, ensuring the stored or served content type matches the file extension. ### Use cases[​](#use-cases-3 "Direct link to Use cases") * **Correct content types** — Automatically set the content type from the file key extension * **Signed URL accuracy** — Ensure signed download/upload URLs advertise the correct content type * **Consistent metadata** — Keep content types aligned with file extensions across writes ### How it works[​](#how-it-works-3 "Direct link to How it works") The `withFileStorageInferContentTypeOnWrite` function returns a [`PluginFn`](/docs/foundation/middleware/.md) that calls `enhance` on the write and signed URL methods. When an enhanced method is invoked, the plugin resolves the `contentType` from the file key extension (via MIME lookup) and overrides the provided content type when a match is found. When the extension is unknown, the content type falls back to `application/octet-stream`, the most generic MIME type. Content type inference can be disabled per signed URL method through the plugin settings. The plugin infers the content type for the following methods: | Method | Behaviour | | ---------------------- | --------------------------------------------- | | `getSignedDownloadUrl` | Infers unless `inferSignedDownloadUrl: false` | | `getSignedUploadUrl` | Infers unless `inferSignedUploadUrl: false` | | `put` | Infers always | | `putStream` | Infers always | ### Usage[​](#usage-3 "Direct link to Usage") ./samples/with-file-storage-infer-content-type-on-write.ts ``` import { withPlugin } from "eridu-tech/middleware"; import { MemoryFileStorageAdapter } from "eridu-tech/file-storage/memory-file-storage-adapter"; import { withFileStorageInferContentTypeOnWrite } from "eridu-tech/file-storage/plugins"; const adapter = new MemoryFileStorageAdapter(); // Apply the write content-type plugin to the adapter const contentTypeAdapter = withPlugin( adapter, withFileStorageInferContentTypeOnWrite(), ); ``` #### Disabling inference for signed URLs[​](#disabling-inference-for-signed-urls "Direct link to Disabling inference for signed URLs") ./samples/file-storage-infer-content-type-disable-signed.ts ``` import { withPlugin } from "eridu-tech/middleware"; import { MemoryFileStorageAdapter } from "eridu-tech/file-storage/memory-file-storage-adapter"; import { withFileStorageInferContentTypeOnWrite } from "eridu-tech/file-storage/plugins"; const adapter = new MemoryFileStorageAdapter(); const contentTypeAdapter = withPlugin( adapter, withFileStorageInferContentTypeOnWrite({ inferSignedDownloadUrl: false, inferSignedUploadUrl: false, }), ); ``` ### Settings[​](#settings "Direct link to Settings") | Option | Type | Default | Description | | ------------------------ | --------- | ------- | ---------------------------------------------------------- | | `inferSignedDownloadUrl` | `boolean` | `true` | Whether to infer the content type for signed download URLs | | `inferSignedUploadUrl` | `boolean` | `true` | Whether to infer the content type for signed upload URLs | danger Because `withPlugin` uses `enhance` under the hood, the same edge case applies: if one enhanced method internally calls another enhanced method via `this`, the middleware will apply **twice**. Be mindful of inter-method calls when applying plugins that enhance multiple methods on the same instance. info For more information about the `withPlugin` function and applying plugins to adapters, see the [Middleware plugin](/docs/foundation/middleware/.md#plugin) documentation. ## withFileStorageInferContentTypeOnRead plugin[​](#with_file_storage_infer_content_type_on_read_plugin "Direct link to withFileStorageInferContentTypeOnRead plugin") The FileStorage read content-type plugin infers the content type from the file key extension when reading file metadata. It is meant for adapters that cannot save the content type of a file and instead need it inferred, such as the [`FsFileStorageAdapter`](/docs/storage/file_storage/configuring_file_storage_adapters/.md). It enhances the `getMetaData` method so the returned metadata reports a content type that matches the file key extension. ### Use cases[​](#use-cases-4 "Direct link to Use cases") * **Accurate metadata** — Report the correct content type for files based on their key extension * **Backend compatibility** — Normalize content types for backends that store generic or missing types ### How it works[​](#how-it-works-4 "Direct link to How it works") The `withFileStorageInferContentTypeOnRead` function returns a [`PluginFn`](/docs/foundation/middleware/.md) that enhances the `getMetaData` method. When invoked, the plugin calls the underlying adapter to obtain the metadata, then resolves the `contentType` from the file key extension (via MIME lookup) only when the metadata's `contentType` is `null`; otherwise the existing content type is preserved. If the extension is unknown, the content type falls back to `application/octet-stream`, the most generic MIME type. If the file does not exist, `null` is passed through. The plugin only affects the `getMetaData` method. ### Usage[​](#usage-4 "Direct link to Usage") ./samples/with-file-storage-infer-content-type-on-read.ts ``` import { withPlugin } from "eridu-tech/middleware"; import { MemoryFileStorageAdapter } from "eridu-tech/file-storage/memory-file-storage-adapter"; import { withFileStorageInferContentTypeOnRead } from "eridu-tech/file-storage/plugins"; const adapter = new MemoryFileStorageAdapter(); // Apply the read content-type plugin to the adapter const metadataAdapter = withPlugin( adapter, withFileStorageInferContentTypeOnRead(), ); ``` info For more information about the `withPlugin` function and applying plugins to adapters, see the [Middleware plugin](/docs/foundation/middleware/.md#plugin) documentation. ## withFileStorageInferFileTypeOnWrite plugin[​](#withfilestorageinferfiletypeonwrite-plugin "Direct link to withFileStorageInferFileTypeOnWrite plugin") The FileStorage write file-type plugin infers the content type from the actual file content when writing files. Unlike the extension-based content-type plugin, it detects the type from the bytes themselves using the `file-type` library, so files with misleading or missing extensions still get an accurate content type. ### Use cases[​](#use-cases-5 "Direct link to Use cases") * **Content-based detection** — Detect the real content type from the file bytes * **Misleading extensions** — Correct content types when the file extension does not match the actual content * **Streaming writes** — Detect the type of streamed content while writing ### How it works[​](#how-it-works-5 "Direct link to How it works") The `withFileStorageInferFileTypeOnWrite` function returns a [`PluginFn`](/docs/foundation/middleware/.md) that calls `enhance` on all write methods. When an enhanced method is invoked, the plugin detects the type of the provided data (via `file-type`) and replaces the provided content type with the detected MIME type when a match is found. When the file type cannot be detected, the content type falls back to `application/octet-stream`, the most generic MIME type. For streams, the plugin returns a transformed stream with the detected file type. The plugin infers the content type for the following methods: | Method | Behaviour | | ----------- | ------------------ | | `put` | Infers from buffer | | `putStream` | Infers from stream | ### Usage[​](#usage-5 "Direct link to Usage") ./samples/with-file-storage-infer-file-type-on-write.ts ``` import { withPlugin } from "eridu-tech/middleware"; import { MemoryFileStorageAdapter } from "eridu-tech/file-storage/memory-file-storage-adapter"; import { withFileStorageInferFileTypeOnWrite } from "eridu-tech/file-storage/plugins"; const adapter = new MemoryFileStorageAdapter(); // Apply the write file-type plugin to the adapter const fileTypeAdapter = withPlugin( adapter, withFileStorageInferFileTypeOnWrite(), ); ``` danger Because `withPlugin` uses `enhance` under the hood, the same edge case applies: if one enhanced method internally calls another enhanced method via `this`, the middleware will apply **twice**. Be mindful of inter-method calls when applying plugins that enhance multiple methods on the same instance. info For more information about the `withPlugin` function and applying plugins to adapters, see the [Middleware plugin](/docs/foundation/middleware/.md#plugin) documentation. ## withFileStorageInferFileTypeOnRead plugin[​](#withfilestorageinferfiletypeonread-plugin "Direct link to withFileStorageInferFileTypeOnRead plugin") The FileStorage read file-type plugin infers the content type from the actual file content when reading file metadata. It is meant for adapters that cannot save the content type of a file and instead need it inferred, such as the [`FsFileStorageAdapter`](/docs/storage/file_storage/configuring_file_storage_adapters/.md). It enhances the `getMetaData` method so the returned metadata reports a content type detected from the file bytes via the `file-type` library. ### Use cases[​](#use-cases-6 "Direct link to Use cases") * **Content-based metadata** — Report the real content type detected from the file bytes * **Misleading extensions** — Correct content types when the stored file does not match its key extension ### How it works[​](#how-it-works-6 "Direct link to How it works") The `withFileStorageInferFileTypeOnRead` function returns a [`PluginFn`](/docs/foundation/middleware/.md) that enhances the `getMetaData` method. When invoked, the plugin calls the underlying adapter to obtain the metadata. When the metadata's `contentType` is `null`, the plugin reads the file stream and detects its type via `file-type`. When a type is detected, the metadata content type is overridden; otherwise the content type falls back to `application/octet-stream`, the most generic MIME type. When the metadata already carries a content type, the plugin returns it as-is without any extra read. If the file does not exist, `null` is passed through. Because the type is detected from the actual content, `getMetaData` additionally opens and reads a leading sample of the object (via `getStream`) whenever inference is required. This extra read is additional I/O on every `getMetaData` call that needs inference. If you only need the content type for files whose keys carry a well-known extension, prefer the extension-based [`withFileStorageInferContentTypeOnRead`](#with_file_storage_infer_content_type_on_read_plugin) plugin, which inspects only the file key and performs no extra read. The plugin only affects the `getMetaData` method. ### Usage[​](#usage-6 "Direct link to Usage") ./samples/with-file-storage-infer-file-type-on-read.ts ``` import { withPlugin } from "eridu-tech/middleware"; import { MemoryFileStorageAdapter } from "eridu-tech/file-storage/memory-file-storage-adapter"; import { withFileStorageInferFileTypeOnRead } from "eridu-tech/file-storage/plugins"; const adapter = new MemoryFileStorageAdapter(); // Apply the read file-type plugin to the adapter const fileTypeAdapter = withPlugin( adapter, withFileStorageInferFileTypeOnRead(), ); ``` info For more information about the `withPlugin` function and applying plugins to adapters, see the [Middleware plugin](/docs/foundation/middleware/.md#plugin) documentation. --- # FileStorageResolver The `FileStorageResolver` class provides a flexible way to configure and switch between different FileStorage adapters at runtime. ## Initial configuration[​](#initial-configuration "Direct link to Initial configuration") To begin using the `IFileStorageFactory`, You will need to register all required adapters during initialization. ./samples/file-storage-resolver-initial-config.ts ``` import { FileStorageResolver } from "eridu-tech/file-storage"; import { MemoryFileStorageAdapter } from "eridu-tech/file-storage/memory-file-storage-adapter"; import { FsFileStorageAdapter } from "eridu-tech/file-storage/fs-file-storage-adapter"; import { SignedFileStorageAdapter } from "eridu-tech/file-storage/signed-file-storage-adapter"; export const fileStorageResolver = new FileStorageResolver({ defaultCacheControl: "public, max-age=31536000", adapters: { memory: new SignedFileStorageAdapter({ adapter: new MemoryFileStorageAdapter(), urlAdapter: {}, }), fs: new SignedFileStorageAdapter({ adapter: new FsFileStorageAdapter({}), urlAdapter: {}, }), }, // You can set an optional default adapter defaultAdapter: "memory", }); await fileStorageResolver.init(); ``` ## Usage[​](#usage "Direct link to Usage") ### 1. Using the default adapter[​](#1-using-the-default-adapter "Direct link to 1. Using the default adapter") ./samples/file-storage-resolver-default-adapter.ts ``` import { fileStorageResolver } from "./file-storage-resolver-initial-config.js"; await fileStorageResolver .use() .create("file.txt") .put({ data: "Text file content" }); ``` danger Note that if you dont set a default adapter, an error will be thrown. ### 2. Specifying an adapter explicitly[​](#2-specifying-an-adapter-explicitly "Direct link to 2. Specifying an adapter explicitly") ./samples/file-storage-resolver-specific-adapter.ts ``` import { fileStorageResolver } from "./file-storage-resolver-initial-config.js"; await fileStorageResolver .use("fs") .create("file.txt") .put({ data: "Text file content" }); ``` danger Note that if you specify a non-existent adapter, an error will be thrown. ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/file-storage`](https://eridu-tech.github.io/eridu-tech-core/modules/file-storage.html) API docs. --- # FileStorage usage The `eridu-tech/file-storage` component provides a way for managing files independent of underlying platform or storage. ## Initial configuration[​](#initial-configuration "Direct link to Initial configuration") To begin using the `FileStorage` class, you'll need to create and configure an instance: ./samples/file-storage-initial-config.ts ``` import { MemoryFileStorageAdapter } from "eridu-tech/file-storage/memory-file-storage-adapter"; import { SignedFileStorageAdapter } from "eridu-tech/file-storage/signed-file-storage-adapter"; import { FileStorage } from "eridu-tech/file-storage"; export const fileStorage = new FileStorage({ // You can choose the adapter to use adapter: new SignedFileStorageAdapter({ adapter: new MemoryFileStorageAdapter(), urlAdapter: {}, }), }); await fileStorage.init(); ``` info Here is a complete list of settings for the [`FileStorage`](https://eridu-tech.github.io/eridu-tech-core/types/file-storage.FileStorage.html) class. ## FileStorage basics[​](#filestorage-basics "Direct link to FileStorage basics") ### Creating a file object[​](#creating-a-file-object "Direct link to Creating a file object") ./samples/file-storage-create-file.ts ``` import { fileStorage } from "./file-storage-initial-config.js"; const file = fileStorage.create("file.txt"); ``` info Note the file object represents a reference to a file and doesnt create the real underlying file. ### Writing buffered files[​](#writing-buffered-files "Direct link to Writing buffered files") You can put a file. It upserts, so an existing file is overwritten: ./samples/file-storage-put.ts ``` import { fileStorage } from "./file-storage-initial-config.js"; await fileStorage.create("file.txt").put({ data: "TEXT 1" }); await fileStorage.create("file.txt").put({ data: "TEXT 2" }); ``` info Note you can pass the following types to the `put` method: * `Buffer` * `ArrayBuffer` * `SharedArrayBuffer` * `string` * `Uint8Array` * `Int8Array` * `Uint16Array` * `Int16Array` * `Uint32Array` * `Int32Array` * `BigUint64Array` * `BigInt64Array` * `Float32Array` * `Float64Array` * `DataView` But usually you would use `Uint8Array` because it represents data as bytes. You can pass additional optional metadata information to the `put` method: ./samples/file-storage-put-with-metadata.ts ``` import { fileStorage } from "./file-storage-initial-config.js"; await fileStorage.create("file.txt").put({ data: "CONTENT", /** * You can explicitly set a custom Content-Type. If one is not provided, it will be inferred from the key. For example, a key ending in .txt (such as key-a.txt) will be assigned text/plain. * If the key contains a non-standard extension it will default to application/octet-stream. */ contentType: "text/plain", /** * Note a default value is always provided. To explicitly unset a field and prevent it from being passed to the underlying adapter, pass in `null`. */ contentLanguage: "en-US", /** * Note a default value is always provided. To explicitly unset a field and prevent it from being passed to the underlying adapter, pass in `null`. */ contentEncoding: "gzip", /** * Note a default value is always provided. To explicitly unset a field and prevent it from being passed to the underlying adapter, pass in `null`. */ contentDisposition: "inline", /** * Note a default value is always provided. To explicitly unset a field and prevent it from being passed to the underlying adapter, pass in `null`. */ cacheControl: "no-cache", }); ``` ### Writing streamed files[​](#writing-streamed-files "Direct link to Writing streamed files") You can put a file stream. It upserts, so an existing file is overwritten: ./samples/file-storage-put-stream.ts ``` import { fileStorage } from "./file-storage-initial-config.js"; import { createReadStream } from "node:fs"; await fileStorage .create("file.txt") .putStream({ data: createReadStream("./file.txt") }); await fileStorage .create("file.txt") .putStream({ data: createReadStream("./file.txt") }); ``` info Note you can pass the following types to the `putStream` method: * `AsyncIteralbe` * `AsyncIteralbe` * `AsyncIteralbe` * `AsyncIteralbe` * `AsyncIteralbe` * `AsyncIteralbe` * `AsyncIteralbe` * `AsyncIteralbe` * `AsyncIteralbe` * `AsyncIteralbe` * `AsyncIteralbe` * `AsyncIteralbe` * `AsyncIteralbe` * `AsyncIteralbe` * `AsyncIteralbe` But usually you would use `AsyncIterable` because it represents stream as bytes. You can pass additional optional metadata information to `putStream`: ./samples/file-storage-put-stream-with-metadata.ts ``` import { fileStorage } from "./file-storage-initial-config.js"; import { createReadStream } from "node:fs"; await fileStorage.create("file.txt").putStream({ data: createReadStream("./file.txt"), /** * You can explicitly set a custom Content-Type. If one is not provided, it will be inferred from the key. For example, a key ending in .txt (such as key-a.txt) will be assigned text/plain. * If the key contains a non-standard extension it will default to application/octet-stream. */ contentType: "text/plain", /** * Note a default value is always provided. To explicitly unset a field and prevent it from being passed to the underlying adapter, pass in `null`. */ contentLanguage: "en-US", /** * Note a default value is always provided. To explicitly unset a field and prevent it from being passed to the underlying adapter, pass in `null`. */ contentEncoding: "gzip", /** * Note a default value is always provided. To explicitly unset a field and prevent it from being passed to the underlying adapter, pass in `null`. */ contentDisposition: "inline", /** * Note a default value is always provided. To explicitly unset a field and prevent it from being passed to the underlying adapter, pass in `null`. */ cacheControl: "no-cache", }); ``` You can also pass the file-size of the stream which used for optimizations by some adapters: ./samples/file-storage-put-stream-with-file-size.ts ``` import { fileStorage } from "./file-storage-initial-config.js"; import { createReadStream } from "node:fs"; import { stat } from "node:fs/promises"; import { FileSize } from "eridu-tech/file-size"; const { size } = await stat("./file.txt"); await fileStorage.create("file.txt").putStream({ data: createReadStream("./file.txt"), fileSize: FileSize.fromBytes(size), }); ``` info It is best practice to pass file-size whenever possible because of the optimizations. ### Retrieving files[​](#retrieving-files "Direct link to Retrieving files") The file can be read as utf8 text: ./samples/file-storage-get-text.ts ``` import { fileStorage } from "./file-storage-initial-config.js"; const content = await fileStorage.create("file.txt").getText(); console.log(content); ``` The file can be read as `Uint8Array`: ./samples/file-storage-get-bytes.ts ``` import { fileStorage } from "./file-storage-initial-config.js"; const content = await fileStorage.create("file.txt").getBytes(); console.log(content); ``` The file can be read as web `ArrayBuffer`: ./samples/file-storage-get-array-buffer.ts ``` import { fileStorage } from "./file-storage-initial-config.js"; const content = await fileStorage.create("file.txt").getArrayBuffer(); console.log(content); ``` The file can be read as web stream: ./samples/file-storage-get-readable-stream.ts ``` import { fileStorage } from "./file-storage-initial-config.js"; const content = await fileStorage.create("file.txt").getReadableStream(); console.log(content); ``` info Note all this methods return null if the file doesnt exists. ### Checking file existence[​](#checking-file-existence "Direct link to Checking file existence") You can check if the file exists: ./samples/file-storage-exists.ts ``` import { fileStorage } from "./file-storage-initial-config.js"; const exists = await fileStorage.create("file.txt").exists(); ``` You can check if the file doesnt exists: ./samples/file-storage-missing.ts ``` import { fileStorage } from "./file-storage-initial-config.js"; const missing = await fileStorage.create("file.txt").missing(); ``` ### Removing files[​](#removing-files "Direct link to Removing files") You can remove a file: ./samples/file-storage-remove.ts ``` import { fileStorage } from "./file-storage-initial-config.js"; await fileStorage.create("file.txt").remove(); ``` You can remove multiple files: ./samples/file-storage-remove-many.ts ``` import { fileStorage } from "./file-storage-initial-config.js"; await fileStorage.removeMany([ fileStorage.create("file-1.txt"), fileStorage.create("file-2.txt"), fileStorage.create("file-3.txt"), ]); ``` ### Retrieving file metadata[​](#retrieving-file-metadata "Direct link to Retrieving file metadata") You can retrieve the file metadata. Null is returned if the file doesnt exists: ./samples/file-storage-get-metadata.ts ``` import { fileStorage } from "./file-storage-initial-config.js"; const metadata = await fileStorage.create("file.txt").getMetadata(); console.log(metadata); ``` The `getMetadata` returns [FileMetadata](https://eridu-tech.github.io/eridu-tech-core/types/file-storage.FileMetadata.html) type. ## Patterns[​](#patterns "Direct link to Patterns") ### Additional methods[​](#additional-methods "Direct link to Additional methods") These variants are equivalent to the standard methods but throw an error if the file does not exist. * `getTextOrFail` * `getBytesOrFail` * `getArrayBufferOrFail` * `getReadableStreamOrFail` * `getMetadataOrFail` ### Copying files[​](#copying-files "Direct link to Copying files") You can copy a file. True is returned if the source exists, and the destination is overwritten: ./samples/file-storage-copy.ts ``` import { fileStorage } from "./file-storage-initial-config.js"; // Returns true when the source exists, the destination is overwritten. const hasCopied = await fileStorage .create("source.txt") .copy("destination.txt"); console.log(hasCopied); ``` Use `copyOrFail` method to perform the same operations as the `copy` method but it throws an error if the source file is missing. ### Moving files[​](#moving-files "Direct link to Moving files") You can move a file. True is returned if the source exists, and the destination is overwritten: ./samples/file-storage-move.ts ``` import { fileStorage } from "./file-storage-initial-config.js"; // Returns true when the source exists, the destination is overwritten. const hasMoved = await fileStorage.create("source.txt").move("destination.txt"); console.log(hasMoved); ``` Use `moveOrFail` method to perform the same operations as the `move` method but it throws an error if the source file is missing. ### Signed urls and public urls.[​](#signed-urls-and-public-urls "Direct link to Signed urls and public urls.") Create signed urls to allow clients to upload files directly to FileStorage. Upload url methods: * getSignedUploadUrl: Returns the signed upload url string. ./samples/file-storage-get-signed-upload-url.ts ``` import { TimeSpan } from "eridu-tech/time-span"; import { fileStorage } from "./file-storage-initial-config.js"; const uploadUrl = await fileStorage.create("source.txt").getSignedUploadUrl({ // All settings are optional ttl: TimeSpan.fromMinutes(10), // The content type will be infered from the filename by default contentType: "text/plain", }); console.log(uploadUrl); ``` Create signed urls to allow clients to download files directly from FileStorage. Download url methods: * getSignedDownloadUrl: Returns the signed download url string, and throws an error if the file does not exist. ./samples/file-storage-get-signed-download-url.ts ``` import { TimeSpan } from "eridu-tech/time-span"; import { fileStorage } from "./file-storage-initial-config.js"; const file = fileStorage.create("source.txt"); await file.put({ data: "CONTENT", }); const donwloadUrl = await file.getSignedDownloadUrl({ // All settings are optional ttl: TimeSpan.fromMinutes(10), // The content type will be infered from the filename by default contentType: "text/plain", contentDisposition: "inline", }); console.log(donwloadUrl); ``` Use these methods to retrieve a permanent link to a file that is publicly accessible within your storage provider. * `getPublicUrl`: Returns the public url as a string, and throws an error if the file does not exist or public access is not supported. ./samples/file-storage-get-public-url.ts ``` import { fileStorage } from "./file-storage-initial-config.js"; const file = fileStorage.create("source.txt"); await file.put({ data: "CONTENT" }); const publicUrl = await file.getPublicUrl(); console.log(publicUrl); ``` ### File instance variables[​](#file-instance-variables "Direct link to File instance variables") The `File` class exposes the key instance variable which is the filename: ./samples/file-storage-instance-variables.ts ``` import { fileStorage } from "./file-storage-initial-config.js"; const file = fileStorage.create("file.txt"); // Will return the file name console.log(file.key); ``` ### Serialization and deserialization of file[​](#serialization-and-deserialization-of-file "Direct link to Serialization and deserialization of file") File obejcts can be serialized, allowing them to be transmitted over the network to another server and later deserialized for reuse. info Note only file name will be saved when serialized and not it' content. Which makes it efficient to send file over the network. In order to serialize or deserialize a file object you need pass an object that implements [`ISerderRegister`](/docs/foundation/serde/.md) contract like the [`Serde`](/docs/foundation/serde/.md) class to `FileStorage`. Manually serializing and deserializing the file object: ./samples/file-storage-manual-serialization.ts ``` import { MemoryFileStorageAdapter } from "eridu-tech/file-storage/memory-file-storage-adapter"; import { SignedFileStorageAdapter } from "eridu-tech/file-storage/signed-file-storage-adapter"; import { FileStorage } from "eridu-tech/file-storage"; import { SuperJsonSerde } from "eridu-tech/serde/super-json-serde"; const serde = new SuperJsonSerde(); const fileStorage = new FileStorage({ // You can laso pass in an array of Serde class instances serde, adapter: new SignedFileStorageAdapter({ adapter: new MemoryFileStorageAdapter(), urlAdapter: {}, }), }); const file = fileStorage.create("file.txt"); const serializedFIle = await serde.serialize(file); const deserializedFIle = await serde.deserialize(serializedFIle); ``` danger When serializing or deserializing a file, you must use the same `Serde` instances that were provided to the `FilStorage`. This is required because the `FilStorage` injects custom serialization logic for `IFile` instance into `Serde` instances. info Note you only need manuall serialization and deserialization when integrating with external libraries. As long you pass the same `Serde` instances with all other components you dont need to serialize and deserialize the file object manually. ./samples/file-storage-event-bus-serialization.ts ``` import { Redis } from "ioredis"; import { MemoryFileStorageAdapter } from "eridu-tech/file-storage/memory-file-storage-adapter"; import { SignedFileStorageAdapter } from "eridu-tech/file-storage/signed-file-storage-adapter"; import type { IFile } from "eridu-tech/file-storage/contracts"; import { FileStorage } from "eridu-tech/file-storage"; import { RedisPubSubEventBusAdapter } from "eridu-tech/event-bus/redis-pub-sub-event-bus-adapter"; import { EventBus } from "eridu-tech/event-bus"; import { SuperJsonSerde } from "eridu-tech/serde/super-json-serde"; const serde = new SuperJsonSerde(); const redis = new Redis("YOUR_REDIS_CONNECTION"); type EventMap = { "sending-file-over-network": { file: IFile; }; }; const eventBus = new EventBus({ adapter: new RedisPubSubEventBusAdapter({ client: redis, serde, }), }); const fileStorage = new FileStorage({ serde, adapter: new SignedFileStorageAdapter({ adapter: new MemoryFileStorageAdapter(), urlAdapter: {}, }), }); const file = fileStorage.create("file.txt"); // We are sending the file over the network to other servers. await eventBus.dispatch("sending-file-over-network", { file, }); // The other servers will recieve the serialized file and automattically deserialize it. await eventBus.addListener("sending-file-over-network", ({ file }) => { // The file is deserialized and can be used console.log("file:", file); }); ``` ### Separating file creation from manipulation[​](#separating-file-creation-from-manipulation "Direct link to Separating file creation from manipulation") The library includes 3 additional contracts: * [`IReadableFile`](https://eridu-tech.github.io/eridu-tech-core/types/file-storage.IReadableFile.html) - Allows only for reading a file. * [`IFile`](https://eridu-tech.github.io/eridu-tech-core/types/file-storage.IFile.html) - Allows for both reading and manipulating the file. * [`IFileFactory`](https://eridu-tech.github.io/eridu-tech-core/types/file-storage.IFileFactory.html) - Allows only for creation of file. * [`IFileStorage`](https://eridu-tech.github.io/eridu-tech-core/types/file-storage.IFileStorage.html) - Allows for creation and removal of files. ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/file-storage`](https://eridu-tech.github.io/eridu-tech-core/modules/file-storage.html) API docs. --- # DI Integration The `fileStorageResolverDiFactory` function creates an [`IFileStorageResolver`](/docs/storage/file_storage/file_storage_resolver/.md) whose underlying resolver is resolved from a [DI container](/docs/foundation/di/.md). The returned proxy implements both `IFileStorageResolver` and `IFileStorage`: * `use(adapterName?)` selects an adapter on the resolved `FileStorageResolver`. * `create(key)` creates a file through the resolver's default adapter; `clear()` and `removeMany()` delegate as well. The proxy is wired to the container during `container.init()`. Create it **before** calling `init()`; calling `use()` or `create()` before the container is initialized throws an error. ## Initial configuration[​](#initial-configuration "Direct link to Initial configuration") Set up the memory and filesystem file-storage adapters, create the container, then register the `FileStorageResolver` and wrap it with `fileStorageResolverDiFactory`: #### 1. Set up the memory file-storage adapter[​](#1-set-up-the-memory-file-storage-adapter "Direct link to 1. Set up the memory file-storage adapter") ./samples/memory-file-storage-adapter.ts ``` import { MemoryFileStorageAdapter } from "eridu-tech/file-storage/memory-file-storage-adapter"; import { SignedFileStorageAdapter } from "eridu-tech/file-storage/signed-file-storage-adapter"; export const memoryFileStorageAdapter = new SignedFileStorageAdapter({ adapter: new MemoryFileStorageAdapter(), urlAdapter: {}, }); ``` #### 2. Set up the filesystem file-storage adapter[​](#2-set-up-the-filesystem-file-storage-adapter "Direct link to 2. Set up the filesystem file-storage adapter") ./samples/fs-file-storage-adapter.ts ``` import { FsFileStorageAdapter } from "eridu-tech/file-storage/fs-file-storage-adapter"; import { SignedFileStorageAdapter } from "eridu-tech/file-storage/signed-file-storage-adapter"; export const fsFileStorageAdapter = new SignedFileStorageAdapter({ adapter: new FsFileStorageAdapter({}), urlAdapter: {}, }); ``` #### 3. Create the dependency injection container[​](#3-create-the-dependency-injection-container "Direct link to 3. Create the dependency injection container") ./samples/container.ts ``` import { Container } from "eridu-tech/di"; import { ExecutionContext } from "eridu-tech/execution-context"; import { AlsExecutionContextAdapter } from "eridu-tech/execution-context/als-execution-context-adapter"; export const executionContext = new ExecutionContext( new AlsExecutionContextAdapter(), ); export const container = new Container({ executionContext }); ``` #### 4. Register and initialize the file-storage resolver[​](#4-register-and-initialize-the-file-storage-resolver "Direct link to 4. Register and initialize the file-storage resolver") ./samples/file-storage-resolver-di-factory.ts ``` import { FileStorageResolver } from "eridu-tech/file-storage"; import { ProxyFileStorageResolver } from "eridu-tech/file-storage/di"; import { container } from "./container.js"; import { fsFileStorageAdapter } from "./fs-file-storage-adapter.js"; import { memoryFileStorageAdapter } from "./memory-file-storage-adapter.js"; type Adapters = "memory" | "fs"; const fileStorageResolver = new FileStorageResolver({ adapters: { memory: memoryFileStorageAdapter, fs: fsFileStorageAdapter, }, defaultAdapter: "memory", }); await fileStorageResolver.init(); container.registerValue({ token: FileStorageResolver, value: fileStorageResolver, }); // Create the proxy before container.init() export const fileStorage = new ProxyFileStorageResolver({ container, resolverToken: FileStorageResolver, }); await container.init(); ``` ## Usage[​](#usage "Direct link to Usage") ### 1. Using the default adapter[​](#1-using-the-default-adapter "Direct link to 1. Using the default adapter") ./samples/file-storage-resolver-di-factory-default-adapter.ts ``` import { fileStorage } from "./file-storage-resolver-di-factory.js"; // Uses the adapter configured as the default (memory) await fileStorage.create("file.txt").put({ data: "Text file content" }); ``` ### 2. Specifying an adapter explicitly[​](#2-specifying-an-adapter-explicitly "Direct link to 2. Specifying an adapter explicitly") ./samples/file-storage-resolver-di-factory-specific-adapter.ts ``` import { fileStorage } from "./file-storage-resolver-di-factory.js"; // Uses the filesystem adapter await fileStorage .use("fs") .create("file.txt") .put({ data: "Text file content" }); ``` ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/file-storage`](https://eridu-tech.github.io/eridu-tech-core/modules/FileStorage.html) API docs. --- # Storage Persist and retrieve data behind a single, portable API. * [Cache](/docs/storage/cache/cache_usage/.md) — Caching with pluggable stores (in-memory, Redis, etc.), TTL policies, and stampede protection. * [File Storage](/docs/storage/file_storage/file_storage_usage/.md) — Abstract file storage with adapters for local disk, S3-compatible, and other backends, upload, stream, and serve with one API. ## Existing middlewares[​](#existing-middlewares "Direct link to Existing middlewares") Beyond the components themselves, every component provides middleware factories for composing component behavior around your own functions. Wrap a function with use() or method with enhance() and apply any of the following middlewares. * [withCacheFactory](/docs/storage/cache/cache_middlewares/.md#with_cache_factory_middleware) — Caches the wrapped function's return value. * [withInvalidationFactory](/docs/storage/cache/cache_middlewares/.md#with_invalidation_factory_middleware) — Invalidates a cache entry after the wrapped function runs. --- # Backoff policies The `eridu-tech/backoff-policies` component ## Predefined backoff policies[​](#predefined-backoff-policies "Direct link to Predefined backoff policies") The library includes predefined backoff policies: * `constantBackoff` - Constant backoff policy with jitter ./samples/constant-backoff.ts ``` import { TimeSpan } from "eridu-tech/time-span"; import { constantBackoff } from "eridu-tech/backoff-policies"; // The settings argument is optional and all its fields are optional const backoff = constantBackoff({ delay: TimeSpan.fromSeconds(1), jitter: 0.5, // You can pass null to disable jitter }); ``` * `exponentialBackoff` - Exponential backoff policy with jitter ./samples/exponential-backoff.ts ``` import { TimeSpan } from "eridu-tech/time-span"; import { exponentialBackoff } from "eridu-tech/backoff-policies"; // The settings argument is optional and all its fields are optional const backoff = exponentialBackoff({ maxDelay: TimeSpan.fromSeconds(60), minDelay: TimeSpan.fromMilliseconds(500), multiplier: 2, jitter: 0.5, // You can pass null to disable jitter }); ``` * `linearBackoff` - Linear backoff policy with jitter ./samples/linear-backoff.ts ``` import { TimeSpan } from "eridu-tech/time-span"; import { linearBackoff } from "eridu-tech/backoff-policies"; // The settings argument is optional and all its fields are optional const backoff = linearBackoff({ maxDelay: TimeSpan.fromSeconds(60), minDelay: TimeSpan.fromMilliseconds(500), jitter: 0.5, // You can pass null to disable jitter }); ``` * `polynomialBackoff` - Polynomial backoff policy with jitter ./samples/polynomial-backoff.ts ``` import { TimeSpan } from "eridu-tech/time-span"; import { linearBackoff } from "eridu-tech/backoff-policies"; // The settings argument is optional and all its fields are optional const backoff = linearBackoff({ maxDelay: TimeSpan.fromSeconds(60), minDelay: TimeSpan.fromMilliseconds(500), jitter: 0.5, // You can pass null to disable jitter }); ``` ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/backoff-policies`](https://eridu-tech.github.io/eridu-tech-core/modules/BackoffPolicy.html) API docs. --- # Collection The `eridu-tech/collection` component provides a fluent, convenient wrapper for working with a `Array`, `Iterable` and `AsyncIterable`. ## Creating a collection[​](#creating-a-collection "Direct link to Creating a collection") You can create a collection from an any object thats implements the `Iterable` or `ArrayLike` contract. Create a collection from an `Array`: ./samples/create-from-array.ts ``` import { ListCollection } from "eridu-tech/collection"; const fromArray = new ListCollection([1, 2, 3, 4]); // Logs [1, 2, 3, 4] console.log(fromArray.toArray()); ``` Create a collection from an `string`: ./samples/create-from-string.ts ``` import { ListCollection } from "eridu-tech/collection"; const fromString = new ListCollection("abc"); // Logs ["a", "b", "c"] console.log(fromString.toArray()); ``` Create a collection from a `Set`: ./samples/create-from-set.ts ``` import { ListCollection } from "eridu-tech/collection"; const fromSet = new ListCollection(new Set([1, 2, 2, 4])); // Logs [1, 2, 4] console.log(fromSet.toArray()); ``` Create a collection from a `Map`: ./samples/create-from-map.ts ``` import { ListCollection } from "eridu-tech/collection"; const fromMap = new ListCollection( new Map([ ["a", 1], ["b", 2], ]), ); // Logs [["a", 1], ["b", 2]] console.log(fromMap.toArray()); ``` Create collection from your own object that implements `Iterable` contract: ./samples/create-from-iterable.ts ``` import { ListCollection } from "eridu-tech/collection"; class MyIterable implements Iterable { *[Symbol.iterator](): Iterator { yield 1; yield 2; yield 3; } } const fromIterable: ListCollection = new ListCollection( new MyIterable(), ); // Logs [1, 2, 3] console.log(fromIterable.toArray()); ``` Create collection from your own object that implements `ArrayLike` contract: ./samples/create-from-array-like.ts ``` import { ListCollection } from "eridu-tech/collection"; const myArrayLike: ArrayLike = { 0: 1, 1: 2, 2: -3, 3: -1, length: 4, }; const fromArrayLike: ListCollection = new ListCollection(myArrayLike); // Logs [1, 2, 3] console.log(fromArrayLike.toArray()); ``` ## Immutability[​](#immutability "Direct link to Immutability") The collection is imutable, meaning that all methods will return a new collection and not modify the original collection. ./samples/immutability.ts ``` import { ListCollection } from "eridu-tech/collection"; const original = new ListCollection([1, 2, 3]); const modified = original.map((item) => item * 2); // Logs [1, 2, 3] console.log(original.toArray()); // Logs [2, 4, 6] console.log(modified.toArray()); ``` ## Acessing elements from a collection[​](#acessing-elements-from-a-collection "Direct link to Acessing elements from a collection") To access elements from a collection you can use the [`get`](#get) method. The [`get`](#get) method will return the item at the specified index. If the index is out of bounds, it will return null. ./samples/access-get.ts ``` import { ListCollection } from "eridu-tech/collection"; const collection = new ListCollection([1, 2, 3]); const value = collection.get(1); // Logs 2 console.log(value); ``` If you want to get the item at the specified index but return a default value if the index is out of bounds, you can use the [`getOr`](#get_or) method. ./samples/access-get-or.ts ``` import { ListCollection } from "eridu-tech/collection"; const collection = new ListCollection([1, 2, 3]); const value = collection.getOr(1, -1); // Logs 2 console.log(value); const value2 = collection.getOr(5, -1); // Logs -1 console.log(value2); ``` If you want to get the item at the specified index and throw an error if the index is out of bounds, you can use the [`getOrFail`](#get_or_fail) method. ./samples/access-get-or-fail.ts ``` import { ListCollection } from "eridu-tech/collection"; const collection = new ListCollection([1, 2, 3]); const value = collection.getOrFail(1); // Logs 2 console.log(value); // throws error const value2 = collection.getOrFail(5); ``` info All methods that ends with `Or` will return the item if it exists or a default value. All methods that ends with `OrFail` it will return the item if it exists or throw an error. ## Iterating over a collection[​](#iterating-over-a-collection "Direct link to Iterating over a collection") Since it is `Iterable` you can also use the `for of` loop to iterate over a collection. ./samples/iterate.ts ``` import { ListCollection } from "eridu-tech/collection"; const collection = new ListCollection([1, 2, 3]); // Logs 1, 2, 3 for (const item of collection) { console.log(item); } ``` ## Modifying and filtering a collection[​](#modifying-and-filtering-a-collection "Direct link to Modifying and filtering a collection") You can modify the collection by using for example the [`map`](#map) method: ./samples/modify-map.ts ``` import { ListCollection } from "eridu-tech/collection"; const collection = new ListCollection([1, 2, 3]).map((value) => value * value); // Logs [1, 4, 9] console.log(collection.toArray()); ``` You can filter the collection by using for example the [`filter`](#filter) method: ./samples/modify-filter.ts ``` import { ListCollection } from "eridu-tech/collection"; const collection = new ListCollection([1, 2, 3, 4, 5, 6]).filter( (value) => value % 2 === 0, ); // Logs [2, 4, 5] console.log(collection.toArray()); ``` info All methods that iterate, modify, or filter the collection expect a callback function with three arguments in the following order 1. `item`: The current element being processed. 2. `index`: The index of the current element. 3. `collection`: The original collection being traversed. ./samples/callback-arguments.ts ``` import { ListCollection } from "eridu-tech/collection"; const collection = new ListCollection([2, 3, 2, 3, 4, 3]).filter( (item, index, collection) => { // Logs each item console.log("item:", item); // Logs each index of the item console.log("index:", index); // Logs the original collection console.log("collection:", collection.toArray()); return item === 2; }, ); collection.toArray(); ``` ## Types of collections[​](#types-of-collections "Direct link to Types of collections") The library includes 3 types of collections: * `ListCollection` implements the `ICollection` contract and uses `Array` internally. * `IterableCollection` implements the `ICollection` contract and uses `Iterable` internally. It only filters and transforms items when you loop through or access its items. * `AsyncIterableCollection` implements the `IAsyncCollection` contract and uses `AsyncIterable` internally. It only filters and transforms items when you loop through or access its items. ## Serialization and deserialization[​](#serialization-and-deserialization "Direct link to Serialization and deserialization") The `ListCollection` and `IterableCollection` classes supports serialization and deserialization, allowing you to easily convert instances to and from serialized formats. However, registration is required first: Serializing and deserializing `ListCollection`: ./samples/serialize-list-collection.ts ``` import { SuperJsonSerde } from "eridu-tech/serde/super-json-serde"; import { ListCollection, IterableCollection } from "eridu-tech/collection"; const serde = new SuperJsonSerde(); serde.registerCustom(ListCollection.serdeTransformer); serde.registerCustom(IterableCollection.serdeTransformer); const listCollection = new ListCollection([1, 2, 3, 4, 5]); const serializedListCollection = await serde.serialize(listCollection); const deserializedListCollection = await serde.deserialize( serializedListCollection, ); // Logs false console.log(listCollection === deserializedListCollection); // Logs [1, 2, 3, 4, 5] [1, 2, 3, 4, 5] console.log(listCollection.toArray(), deserializedListCollection.toArray()); ``` Serializing and deserializing `IterableCollection`: ./samples/serialize-iterable-collection.ts ``` import { SuperJsonSerde } from "eridu-tech/serde/super-json-serde"; import { ListCollection, IterableCollection } from "eridu-tech/collection"; const serde = new SuperJsonSerde(); serde.registerCustom(ListCollection.serdeTransformer); serde.registerCustom(IterableCollection.serdeTransformer); const listCollection = new ListCollection([1, 2, 3, 4, 5]); const serializedListCollection = await serde.serialize(listCollection); const deserializedListCollection = await serde.deserialize( serializedListCollection, ); // Logs false console.log(listCollection === deserializedListCollection); // Logs [1, 2, 3, 4, 5] [1, 2, 3, 4, 5] console.log(listCollection.toArray(), deserializedListCollection.toArray()); ``` ## Available Methods[​](#available-methods "Direct link to Available Methods") For the remaining of the documentation, we'll discuss each method available on the `ICollection` contract. ### Instance methods[​](#instance-methods "Direct link to Instance methods") * 1. [after](#after) 1) [afterOr](#after_or) 1. [afterOrFail](#after_or_fail) 1) [append](#append) 1. [average](#average) 1) [before](#before) 1. [beforeOr](#before_or) 1) [beforeOrFail](#before_or_fail) 1. [change](#change) 1) [chunk](#chunk) 1. [chunkWhile](#chunk_while) 1) [collapse](#collapse) 1. [count](#count) 1) [countBy](#count_by) 1. [crossJoin](#cross_join) 1) [difference](#difference) 1. [entries](#entries) 1) [every](#every) 1. [filter](#filter) 1) [filter](#validate) 1. [first](#first) 1) [firstOr](#first_or) 1. [firstOrFail](#first_or_fail) 1) [flatMap](#flat_map) 1. [forEach](#for_each) 1) [get](#get) 1. [getOr](#get_or) 1) [getOrFail](#get_or_fail) 1. [groupBy](#group_by) * 1. [insertAfter](#insert_after) 1) [insertBefore](#insert_before) 1. [isEmpty](#is_empty) 1) [isNotEmpty](#is_not_empty) 1. [join](#join) 1) [keys](#keys) 1. [last](#last) 1) [lastOr](#last_or) 1. [lastOrFail](#last_or_fail) 1) [map](#map) 1. [max](#max) 1) [median](#median) 1. [min](#min) 1) [nth](#nth) 1. [padEnd](#pad_end) 1) [padStart](#pad_start) 1. [page](#page) 1) [partition](#partition) 1. [percentage](#percentage) 1) [pipe](#pipe) 1. [prepend](#prepend) 1) [reduce](#reduce) 1. [reject](#reject) 1) [repeat](#repeat) 1. [reverse](#reverse) 1) [searchFirst](#search_first) 1. [searchLast](#search_last) 1) [serialize](#serialize) * 1. [set](#set) 1) [shuffle](#shuffle) 1. [size](#size) 1) [skip](#skip) 1. [skipUntil](#skip_until) 1) [skipWhile](#skip_while) 1. [slice](#slice) 1) [sliding](#sliding) 1. [sole](#sole) 1) [some](#some) 1. [sort](#sort) 1) [split](#split) 1. [sum](#sum) 1) [take](#take) 1. [takeUntil](#take_until) 1) [takeWhile](#take_while) 1. [tap](#tap) 1) [toArray](#to_array) 1. [toIterator](#to_iterator) 1) [toMap](#to_map) 1. [toRecord](#to_record) 1) [unique](#unique) 1. [values](#values) 1) [when](#when) 1. [whenEmpty](#when_empty) 1) [whenNot](#when_not) 1. [whenNotEmpty](#when_not_empty) 1) [zip](#zip) ### Static methods[​](#static-methods "Direct link to Static methods") * 1. [concat](#concat) 1) [difference](#difference) 1. [zip](#class_zip) ## Instance methods[​](#instance-methods-1 "Direct link to Instance methods") ### after[​](#after "Direct link to after") The `after` method returns the item that comes after the first item that matches `predicateFn`. If the collection is empty or the `predicateFn` does not match or matches the last item then `null` is returned. ./samples/after.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([1, 2, 3, 4]).after((item) => item === 2); // 3 ``` ### afterOr[​](#after_or "Direct link to afterOr") The `afterOr` method returns the item that comes after the first item that matches `predicateFn`. If the collection is empty or the `predicateFn` does not match or matches the last item then `defaultValue` is returned. ./samples/after-or.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([1, 2, 3, 4]).afterOr(-1, (item) => item === 4); // -1 ``` ### afterOrFail[​](#after_or_fail "Direct link to afterOrFail") The `afterOrFail` method returns the item that comes after the first item that matches `predicateFn`. If the collection is empty or the `predicateFn` does not match or matches the last item then an error is thrown. ./samples/after-or-fail.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([1, 2, 3, 4]).afterOrFail((item) => item === 4); // throws error ``` ### append[​](#append "Direct link to append") The `append` method adds `Iterable` to the end of the collection. ./samples/append.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([1, 2, 3, 4, 5]).append([-1, -2]).toArray(); // [1, 2, 3, 4, 5, -1, -2] ``` ### average[​](#average "Direct link to average") The `average` method returns the average of all items in the collection. If the collection includes other than number items an error will be thrown. If the collection is empty an error will also be thrown. ./samples/average.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([1, 2, 3]).average(); // 2 ``` ### before[​](#before "Direct link to before") The `before` method returns the item that comes before the first item that matches `predicateFn`. If the `predicateFn` does not match or matches the first item then null is returned. ./samples/before.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([1, 2, 3, 4]).before((item) => item === 2); // 1 ``` ### beforeOr[​](#before_or "Direct link to beforeOr") The `beforeOr` method returns the item that comes before the first item that matches `predicateFn`. If the collection is empty or the `predicateFn` does not match or matches the first item then `defaultValue` is returned. ./samples/before-or.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([1, 2, 3, 4]).beforeOr(-1, (item) => item === 2); // 1 ``` ### beforeOrFail[​](#before_or_fail "Direct link to beforeOrFail") The `beforeOrFail` method returns the item that comes before the first item that matches `predicateFn`. If the collection is empty or the `predicateFn` does not match or matches the first item then an error is thrown. ./samples/before-or-fail.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([1, 2, 3, 4]).beforeOrFail((item) => item === 1); // throws error ``` ### change[​](#change "Direct link to change") The `change` method changes only the items that passes `predicateFn` using `mapFn`. ./samples/change.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([1, 2, 3, 4, 5]) .change( (item) => item % 2 === 0, (item) => item * 2, ) .toArray(); // [1, 4, 3, 8, 5] ``` ### chunk[​](#chunk "Direct link to chunk") The `chunk` method breaks the collection into multiple, smaller collections of size `chunkSize`. If `chunkSize` is not divisible with total number of items then the last chunk will contain the remaining items. ./samples/chunk.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([1, 2, 3, 4, 5, 6, 7]) .chunk(4) .map((chunk) => chunk.toArray()) .toArray(); // [[1, 2, 3, 4], [5, 6, 7]] ``` ### chunkWhile[​](#chunk_while "Direct link to chunkWhile") The `chunkWhile` method breaks the collection into multiple, smaller collections based on the evaluation of `predicateFn`. The chunk variable passed to the `predicateFn` may be used to inspect the previous item. ./samples/chunk-while.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection("AABBCCCD") .chunkWhile((item, _index, chunk) => item === chunk.last()) .map((chunk) => chunk.toArray()) .toArray(); // [["A", "A"], ["B", "B"], ["C", "C", "C"], ["D"]] ``` ### collapse[​](#collapse "Direct link to collapse") The `collapse` method collapses a collection of iterables into a single, flat collection. ./samples/collapse.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([ [1, 2], [3, 4], ]) .collapse() .toArray(); // [1, 2, 3, 4] ``` ### count[​](#count "Direct link to count") The `count` method returns the total number of items in the collection that passes `predicateFn`. ./samples/count.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([1, 2, 3, 4, 5, 6]).count((value) => value % 2 === 0); // 3 ``` ### countBy[​](#count_by "Direct link to countBy") The `countBy` method counts the occurrences of values in the collection by `selectFn`. By default the equality check occurs on the item. ./samples/count-by.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection(["a", "a", "a", "b", "b", "c"]).countBy().toArray(); // [["a", 3], ["b", 2], ["c", 1]] ``` ### crossJoin[​](#cross_join "Direct link to crossJoin") The `crossJoin` method cross joins the collection's values among `iterables`, returning a Cartesian product with all possible permutations. ./samples/cross-join.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([1, 2]).crossJoin(["a", "b"]).toArray(); // [[1, "a"], [1, "b"], [2, "a"], [2, "b"]] ``` ### entries[​](#entries "Direct link to entries") The `entries` returns an ListCollection of key, value pairs for every entry in the collection. ./samples/entries.ts ``` import { ListCollection } from "eridu-tech/collection"; const collection = new ListCollection(["a", "b", "c", "d"]).entries().toArray(); // [[0, "a"], [1, "b"], [2, "c"], [3, "d"]] ``` ### every[​](#every "Direct link to every") The `every` method determines whether all items in the collection matches `predicateFn`. ./samples/every.ts ``` import { ListCollection } from "eridu-tech/collection"; const isAllNumberLessThan6 = new ListCollection([0, 1, 2, 3, 4, 5]).every( (item) => item < 6, ); // true ``` ### filter[​](#filter "Direct link to filter") The `filter` method filters the collection using `predicateFn`, keeping only those items that pass `predicateFn`. ./samples/filter.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([0, 1, 2, 3, 4, 5, 6]) .filter((item) => 2 < item && item < 5) .toArray(); // [3, 4] ``` ### validate[​](#validate "Direct link to validate") The `validate` method filters all items that matches the `schema` and transforms them afterwards. The `schema` can be any [standard schema](https://standardschema.dev/). ./samples/validate.ts ``` import { ListCollection } from "eridu-tech/collection"; import { z } from "zod"; new ListCollection(["a", "1.2", "3", "null"]) .validate(z.string().pipe(z.coerce.number())) .toArray(); // [1.2, 3] ``` ### first[​](#first "Direct link to first") The `first` method returns the first item in the collection that passes `predicateFn`. By default it will get the first item. If the collection is empty or no items passes `predicateFn` than null i returned. ./samples/first.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([1, 2, 3, 4]).first(); // 1 ``` ### firstOr[​](#first_or "Direct link to firstOr") The `firstOr` method returns the first item in the collection that passes `predicateFn` By default it will get the first item. If the collection is empty or no items passes `predicateFn` than `defaultValue`. ./samples/first-or.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([1, 2, 3, 4]).firstOr(-1, (item) => item > 10); // -1 ``` ### firstOrFail[​](#first_or_fail "Direct link to firstOrFail") The `firstOrFail` method returns the first item in the collection that passes `predicateFn`. By default it will get the first item. If the collection is empty or no items passes `predicateFn` than error is thrown. ./samples/first-or-fail.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([1, 2, 3, 4]).firstOrFail((item) => item === 5); // throws error ``` ### flatMap[​](#flat_map "Direct link to flatMap") The `flatMap` method returns a new array formed by applying `mapFn` to each item of the array, and then collapses the result by one level. It is identical to a `map` method followed by a `collapse` method. ./samples/flat-map.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([ ["a", "b"], ["c", "d"], ]) .flatMap((item) => [item.length, ...item]) .toArray(); // [2, "a", "b", 2, "c", "d"] ``` ### forEach[​](#for_each "Direct link to forEach") The `forEach` method iterates through all items in the collection. ./samples/for-each.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([1, 2, 3]).forEach((item) => console.log(item)); // Logs: 1, 2, 3 ``` ### get[​](#get "Direct link to get") The `get` method returns the item by index. If the item is not found null will be returned. ./samples/get.ts ``` import { ListCollection } from "eridu-tech/collection"; const collection = new ListCollection([1, 4, 2, 8, -2]); collection.get(2); // 2 collection.get(5); // null ``` ### getOr[​](#get_or "Direct link to getOr") The `getOr` method returns the item by index. If the item is not found, a default value will be returned. ./samples/get-or.ts ``` import { ListCollection } from "eridu-tech/collection"; const collection = new ListCollection([1, 4, 2, 8, -2]); collection.getOr(2, -1); // 2 collection.getOr(5, -1); // -1 ``` ### getOrFail[​](#get_or_fail "Direct link to getOrFail") The `getOrFail` method returns the item by index. If the item is not found an error will be thrown. ./samples/get-or-fail.ts ``` import { ListCollection } from "eridu-tech/collection"; const collection = new ListCollection([1, 4, 2, 8, -2]); collection.getOrFail(2); // 2 collection.getOrFail(5); // throws error ``` ### groupBy[​](#group_by "Direct link to groupBy") The `groupBy` method groups the collection's items by `selectFn`. By default the equality check occurs on the item. ./samples/group-by.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection(["a", "a", "a", "b", "b", "c"]) .groupBy() .map(([k, v]) => [k, v.toArray()]) .toArray(); // [["a", ["a", "a", "a"]], ["b", ["b", "b"]], ["c", ["c"]]] ``` ### insertAfter[​](#insert_after "Direct link to insertAfter") The `insertAfter` method adds `Iterable` after the first item that matches `predicateFn`. ./samples/insert-after.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([1, 2, 2, 3, 4, 5]) .insertAfter((item) => item === 2, [-1, 20]) .toArray(); // [1, 2, -1, 20, 2, 3, 4, 5] ``` ### insertBefore[​](#insert_before "Direct link to insertBefore") The `insertBefore` method adds `Iterable` before the first item that matches `predicateFn`. ./samples/insert-before.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([1, 2, 2, 3, 4, 5]) .insertAfter((item) => item === 2, [-1, 20]) .toArray(); // [1, 2, -1, 20, 2, 3, 4, 5] ``` ### isEmpty[​](#is_empty "Direct link to isEmpty") The `isEmpty` returns true if the collection is empty. ./samples/is-empty.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([]).isEmpty(); // true ``` ### isNotEmpty[​](#is_not_empty "Direct link to isNotEmpty") The `isEmpty` returns true if the collection is empty. ./samples/is-not-empty.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([]).isEmpty(); // true ``` ### join[​](#join "Direct link to join") The `join` method joins the collection's items with `separator`. An error will be thrown when if a non-string item is encountered. ./samples/join.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([1, 2, 3, 4]).map((item) => item.toString()).join(); // "1,2,3,4" ``` ./samples/join-with-separator.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([1, 2, 3, 4]).map((item) => item.toString()).join("_"); // "1_2_3_4" ``` ### keys[​](#keys "Direct link to keys") The `keys` method returns an ListCollection of keys in the collection. ### last[​](#last "Direct link to last") The `last` method returns the last item in the collection that passes `predicateFn`. By default it will get the last item. If the collection is empty or no items passes `predicateFn` than null i returned. ./samples/last.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([1, 2, 3, 4]).last(); // 4 ``` ### lastOr[​](#last_or "Direct link to lastOr") The `lastOr` method returns the last item in the collection that passes `predicateFn`. By default it will get the last item. If the collection is empty or no items passes `predicateFn` than `defaultValue`. ./samples/last-or.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([1, 2, 3, 4]).lastOr(-1, (item) => item > 10); // -1 ``` ### lastOrFail[​](#last_or_fail "Direct link to lastOrFail") The `lastOrFail` method returns the last item in the collection that passes `predicateFn`. By default it will get the last item. If the collection is empty or no items passes `predicateFn` than error is thrown. ./samples/last-or-fail.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([1, 2, 3, 4]).lastOrFail((item) => item === 5); // throws error ``` ### map[​](#map "Direct link to map") The `map` method iterates through the collection and passes each item to `mapFn`. The `mapFn` is free to modify the item and return it, thus forming a new collection of modified items. ./samples/map.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([1, 2, 3, 4, 5]).map((item) => item * 2).toArray(); // [2, 4, 6, 8, 10] ``` ### max[​](#max "Direct link to max") The `max` method returns the max of all items in the collection. If the collection includes other than number items an error will be thrown. If the collection is empty an error will also be thrown. ./samples/max.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([1, 2, 3]).max(); // 3 ``` ### median[​](#median "Direct link to median") The `median` method returns the median of all items in the collection. If the collection includes other than number items an error will be thrown. If the collection is empty an error will also be thrown. ./samples/median.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([1, 2, 3]).median(); // 2 ``` ### min[​](#min "Direct link to min") The `min` method returns the min of all items in the collection. If the collection includes other than number items an error will be thrown. If the collection is empty an error will also be thrown. ./samples/min.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([1, 2, 3]).min(); // 1 ``` ### nth[​](#nth "Direct link to nth") The `nth` method creates a new collection consisting of every n-th item. ./samples/nth.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection(["a", "b", "c", "d", "e", "f"]).nth(4).toArray(); // ["a", "e"] ``` ### padEnd[​](#pad_end "Direct link to padEnd") The `padEnd` method pads this collection with `fillItems` until the resulting collection size reaches `maxLength`. The padding is applied from the end of this collection. ./samples/pad-end.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection("abc").padEnd(10, "foo").join(""); // "abcfoofoof" ``` ### padStart[​](#pad_start "Direct link to padStart") The `padStart` method pads this collection with `fillItems` until the resulting collection size reaches `maxLength`. The padding is applied from the start of this collection. ./samples/pad-start.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection("abc").padStart(10, "foo").join(""); // "foofoofabc" ``` ### page[​](#page "Direct link to page") The `page` method returns a new collection containing the items that would be present on `page` with custom `pageSize`. ./samples/page.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([1, 2, 3, 4, 5, 6, 7, 8, 9]) .page( 2, // Page number 3, // Page size ) .toArray(); // [4, 5, 6] ``` ### partition[​](#partition "Direct link to partition") The `partition` method is used to separate items that pass `predicateFn` from those that do not. ./samples/partition.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([1, 2, 3, 4, 5, 6]) .partition((nbr) => nbr % 2 === 0) .map((chunk) => chunk.toArray()) .toArray(); // [[2, 4, 6], [1, 3, 5]] ``` ### percentage[​](#percentage "Direct link to percentage") The `percentage` method may be used to quickly determine the percentage of items in the collection that pass `predicateFn`. If the collection is empty an error will also be thrown. ./samples/percentage.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([1, 1, 2, 2, 2, 3]).percentage((value) => value === 1); // 33.333 ``` ### pipe[​](#pipe "Direct link to pipe") The `pipe` method passes the orignal collection to `callback` and returns the result from `callback`. ./samples/pipe.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([1, "2", "a", 1, 3, {}]) .pipe((c) => c.map((item) => Number(item)).reject(isNaN)) .pipe((c) => c.repeat(2).toArray()); // [1, 2, 1, 3] ``` ### prepend[​](#prepend "Direct link to prepend") The `prepend` method adds `Iterable` to the beginning of the collection. ./samples/prepend.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([1, 2, 3, 4, 5]).prepend([-1, 20]).toArray(); // [-1, 20, 1, 2, 3, 4, 5] ``` ### reduce[​](#reduce "Direct link to reduce") The `reduce` method executes `reduceFn` function on each item of the array, passing in the return value from the calculation on the preceding item. The final result of running the reducer across all items of the array is a single value. ./samples/reduce.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([1, 2, 3]).reduce((sum, item) => sum + item); // 6 ``` ### reject[​](#reject "Direct link to reject") The `reject` method filters the collection using `predicateFn`, keeping only those items that not pass `predicateFn`. ./samples/reject.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([1, 2, 3, 4, 5, 6]) .reject((item) => 2 < item && item < 5) .toArray(); // [1, 2, 5, 6] ``` ### repeat[​](#repeat "Direct link to repeat") The `repeat` method will repeat the original collection `amount` times. ./samples/repeat.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([1, 2, 3]).repeat(3).toArray(); // [1, 2, 3, 1, 2, 3, 1, 2, 3] ``` ### reverse[​](#reverse "Direct link to reverse") The `reverse` method will reverse the order of the collection. The reversing of the collection will be applied in chunks that are the size of `chunkSize`. ./samples/reverse.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([-1, 2, 4, 3]).reverse().toArray(); // [3, 4, 2, -1] ``` ### searchFirst[​](#search_first "Direct link to searchFirst") The `searchFirst` return the index of the first item that matches `predicateFn`. ./samples/search-first.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection(["a", "b", "b", "c"]).searchFirst((item) => item === "b"); // 1 ``` ### searchLast[​](#search_last "Direct link to searchLast") The `searchLast` return the index of the last item that matches `predicateFn`. ./samples/search-last.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection(["a", "b", "b", "c"]).searchLast((item) => item === "b"); // 2 ``` ### set[​](#set "Direct link to set") The `set` method updates the specified index with new value. ./samples/set.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([1, 2, 3, 4, 5]).set(1, -1).toArray(); // [1, -1, 3, 4, 5] ``` ### shuffle[​](#shuffle "Direct link to shuffle") The `shuffle` method randomly shuffles the items in the collection. You can provide a custom Math.random function by passing in `mathRandom`. ./samples/shuffle.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([1, 2, 3, 4]).shuffle().toArray(); // Random order, e.g., [3, 1, 4, 2] ``` ### size[​](#size "Direct link to size") The `size` returns the size of the collection. ./samples/size.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([1, 2, 3]).size(); // 3 ``` ### skip[​](#skip "Direct link to skip") The `skip` method skips the first `offset` items. ./samples/skip.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]).skip(4).toArray(); // [5, 6, 7, 8, 9, 10] ``` ### skipUntil[​](#skip_until "Direct link to skipUntil") The `skipUntil` method skips items until `predicateFn` returns true. ./samples/skip-until.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([1, 2, 3, 4]).skipUntil((item) => item >= 3).toArray(); // [3, 4] ``` ### skipWhile[​](#skip_while "Direct link to skipWhile") The `skipWhile` method skips items until `predicateFn` returns false. ./samples/skip-while.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([1, 2, 3, 4]).skipWhile((item) => item <= 3).toArray(); // [4] ``` ### slice[​](#slice "Direct link to slice") The `slice` method creates porition of the original collection selected from `start` and `end` where `start` and `end` (end not included) represent the index of items in the collection. ./samples/slice.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection(["a", "b", "c", "d", "e", "f"]).slice(3).toArray(); // ["d", "e", "f"] ``` ### sliding[​](#sliding "Direct link to sliding") The `sliding` method returns a new collection of chunks representing a "sliding window" view of the items in the collection. ./samples/sliding.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([1, 2, 3, 4, 5]) .sliding(2) .map((chunk) => chunk.toArray()) .toArray(); // [[1, 2], [2, 3], [3, 4], [4, 5]] ``` ### sole[​](#sole "Direct link to sole") The `sole` method returns the first item in the collection that passes `predicateFn`, but only if `predicateFn` matches exactly one item. If no items matches or multiple items are found an error will be thrown. ./samples/sole.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([1, 2, 3, 4, 5]).sole((item) => item === 4); // 4 ``` ### some[​](#some "Direct link to some") The `some` method determines whether at least one item in the collection matches `predicateFn`. ./samples/some.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([0, 1, 2, 3, 4, 5]).some((item) => item === 1); // true ``` ### sort[​](#sort "Direct link to sort") The `sort` method sorts the collection. You can provide a `comparator` function. ./samples/sort.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([-1, 2, 4, 3]).sort().toArray(); // [-1, 2, 3, 4] ``` ### split[​](#split "Direct link to split") The `split` method breaks a collection evenly into `chunkAmount` of chunks. ./samples/split.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([1, 2, 3, 4, 5]) .split(3) .map((chunk) => chunk.toArray()) .toArray(); // [[1, 2], [3, 4], [5]] ``` ### sum[​](#sum "Direct link to sum") The `sum` method returns the sum of all items in the collection. If the collection includes other than number items an error will be thrown. If the collection is empty an error will also be thrown. ./samples/sum.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([1, 2, 3]).sum(); // 6 ``` ### take[​](#take "Direct link to take") The `take` method takes the first `limit` items. ./samples/take.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([0, 1, 2, 3, 4, 5]).take(3).toArray(); // [0, 1, 2] ``` ./samples/take-negative.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([0, 1, 2, 3, 4, 5]).take(-2).toArray(); // [0, 1, 2, 3] ``` ### takeUntil[​](#take_until "Direct link to takeUntil") The `takeUntil` method takes items until `predicateFn` returns true. ./samples/take-until.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([1, 2, 3, 4]).takeUntil((item) => item >= 3).toArray(); // [1, 2] ``` ### takeWhile[​](#take_while "Direct link to takeWhile") The `takeWhile` method takes items until `predicateFn` returns false. ./samples/take-while.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([1, 2, 3, 4]).takeWhile((item) => item < 4).toArray(); // [1, 2, 3] ``` ### tap[​](#tap "Direct link to tap") The `tap` method passes a copy of the original collection to `callback`, allowing you to do something with the items while not affecting the original collection. ./samples/tap.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([1, 2, 3, 4, 5, 6]) .tap((c) => c.filter((v) => v % 2 === 0).forEach(console.log)) .toArray(); // [1, 2, 3, 4, 5, 6] (logs 2, 4, 6) ``` ### toArray[​](#to_array "Direct link to toArray") The `toArray` method converts the collection to a new `Array`. ./samples/to-array.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([1, 2, 3]).toArray(); // [1, 2, 3] ``` ### toIterator[​](#to_iterator "Direct link to toIterator") The `toIterator` method converts the collection to a `Iterator`. ./samples/to-iterator.ts ``` import { ListCollection } from "eridu-tech/collection"; const iterator = new ListCollection([1, 2, 3, 4, 5]).toIterator(); console.log("item 1:", iterator.next()); console.log("item 2:", iterator.next()); console.log("item 3:", iterator.next()); console.log("done:", iterator.next()); ``` ### toMap[​](#to_map "Direct link to toMap") The `toMap` method converts the collection to a new `Map`. An error will be thrown if item is not a tuple of size 2. ./samples/to-map.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([ [0, "a"], [1, "b"], ]).toMap(); // Map { 0 => "a", 1 => "b" } ``` ### toRecord[​](#to_record "Direct link to toRecord") The `toRecord` method converts the collection to a new `Record`. An error will be thrown if item is not a tuple of size 2 where the first element is a string or a number. ./samples/to-record.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([ [0, "a"], [1, "b"], ]).toRecord(); // { 0: "a", 1: "b" } ``` ### unique[​](#unique "Direct link to unique") The `unique` method removes all duplicate values from the collection by `selectFn`. By default the equality check occurs on the item. ./samples/unique.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([1, 1, 2, 2, 3, 4, 2]).unique().toArray(); // [1, 2, 3, 4] ``` Equality check on a field in the item. ./samples/unique-by-field.ts ``` import { ListCollection } from "eridu-tech/collection"; type Phone = { name: string; brand: string; type: string }; new ListCollection([ { name: "iPhone 6", brand: "Apple", type: "phone" }, { name: "iPhone 5", brand: "Apple", type: "phone" }, { name: "Apple Watch", brand: "Apple", type: "watch" }, { name: "Galaxy S6", brand: "Samsung", type: "phone" }, { name: "Galaxy Gear", brand: "Samsung", type: "watch" }, ]) .unique((item) => item.brand) .toArray(); // [ // { name: "iPhone 6", brand: "Apple", type: "phone" }, // { name: "Galaxy S6", brand: "Samsung", type: "phone" }, // ] ``` ### copy[​](#copy "Direct link to copy") The `copy` method returns a copy of the collection. ./samples/copy.ts ``` import { ListCollection } from "eridu-tech/collection"; const collectionA = new ListCollection([1, 2, 3, 4]); const collectionB = collectionA.copy(); // Logs false console.log(collectionA === collectionB); // Logs false console.log(collectionA.toArray() === collectionB.toArray()); ``` ### when[​](#when "Direct link to when") The `when` method will execute `callback` when `condition` evaluates to true. ./samples/when.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([1, 2, 3, 4]) .when(true, (c) => c.append([-3])) .when(false, (c) => c.append([20])) .toArray(); // [1, 2, 3, 4, -3] ``` ### whenEmpty[​](#when_empty "Direct link to whenEmpty") The `whenEmpty` method will execute `callback` when the collection is empty. ./samples/when-empty.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([]).whenEmpty((c) => c.append([-3])).toArray(); // [-3] ``` ./samples/when-empty-skip.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([1]).whenEmpty((c) => c.append([-3])).toArray(); // [1] ``` ### whenNot[​](#when_not "Direct link to whenNot") The `whenNot` method will execute `callback` when `condition` evaluates to false. ./samples/when-not.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([1, 2, 3, 4]) .whenNot(true, (c) => c.append([-3])) .whenNot(false, (c) => c.append([20])) .toArray(); // [1, 2, 3, 4, 20] ``` ### whenNotEmpty[​](#when_not_empty "Direct link to whenNotEmpty") The `whenNotEmpty` method will execute `callback` when the collection is not empty. ./samples/when-not-empty-skip.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([]).whenNotEmpty((c) => c.append([-3])).toArray(); // [] ``` ./samples/when-not-empty.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([1]).whenNotEmpty((c) => c.append([-3])).toArray(); // [1, -3] ``` ## Static methods[​](#static-methods-1 "Direct link to Static methods") ### concat[​](#concat "Direct link to concat") The `concat` method concatenates multiple `Iterable`'s and returns a new collection. ./samples/concat.ts ``` import { ListCollection } from "eridu-tech/collection"; ListCollection.concat([ [1, 2, 3], [4, 5, 6], ]).toArray(); // [1, 2, 3, 4, 5, 6] ``` ### difference[​](#difference "Direct link to difference") The `difference` method will return the values in the original collection that are not present in `Iterable`. By default the equality check occurs on the item. ./samples/difference.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([1, 2, 2, 3, 4, 5]).difference([2, 4, 6, 8]).toArray(); // [1, 3, 5] ``` Equality check on a field in the item. ./samples/difference-by-field.ts ``` import { ListCollection } from "eridu-tech/collection"; new ListCollection([ { name: "iPhone 6", brand: "Apple", type: "phone" }, { name: "iPhone 5", brand: "Apple", type: "phone" }, { name: "Apple Watch", brand: "Apple", type: "watch" }, { name: "Galaxy S6", brand: "Samsung", type: "phone" }, { name: "Galaxy Gear", brand: "Samsung", type: "watch" }, ]) .difference( [{ name: "Apple Watch", brand: "Apple", type: "watch" }], // equality check occurs on product.type (product) => product.type, ) .toArray(); // [ // { name: "iPhone 6", brand: "Apple", type: "phone" }, // { name: "iPhone 5", brand: "Apple", type: "phone" }, // { name: "Galaxy S6", brand: "Samsung", type: "phone" }, // ] ``` The `difference` method is also available as a static method. ./samples/difference-static.ts ``` import { ListCollection } from "eridu-tech/collection"; ListCollection.difference([1, 2, 2, 3, 4, 5], [2, 4, 6, 8]).toArray(); // [1, 3, 5] ``` ### zip[​](#zip "Direct link to zip") The `zip` method merges together the values of `Iterable` with the values of the collection at their corresponding index. The returned collection has size of the shortest collection. The `zip` method is also available as a static method. ./samples/zip.ts ``` import { ListCollection } from "eridu-tech/collection"; ListCollection.zip(["Chair", "Desk"], [100, 200]).toArray(); // [["Chair", 100], ["Desk", 200]] ``` ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/collection`](https://eridu-tech.github.io/eridu-tech-core/modules/Collection.html) API docs. --- # ErrorPolicy type The `ErrorPolicy` type determines which errors should be handled for example in resilience middlewares like [`retry`](/docs/reliability/resilience/.md#retry) or [`fallback`](/docs/reliability/resilience/.md#fallback). ## Predicate as ErrorPolicy[​](#predicate-as-errorpolicy "Direct link to Predicate as ErrorPolicy") A predicate function can be used to dynamically determine if an error should be handled: ./samples/predicate.ts ``` import { fallback } from "eridu-tech/resilience"; import { use } from "eridu-tech/middleware"; class CustomError extends Error { constructor( readonly errorCode: string, message: string, cause?: unknown, ) { super(message, { cause }); this.name = CustomError.name; } } const func = use( () => Promise.resolve("DATA"), [ fallback({ fallbackValue: "DEFAULT_VALUE", errorPolicy: (error) => error instanceof CustomError, }), ], ); await func(); ``` ## Classes as ErrorPolicy:[​](#classes-as-errorpolicy "Direct link to Classes as ErrorPolicy:") You can directly pass an class to match if errors are instance of the class: ./samples/class.ts ``` import { fallback } from "eridu-tech/resilience"; import { use } from "eridu-tech/middleware"; import { UnexpectedError } from "eridu-tech/utilities"; const func = use( () => Promise.resolve("DATA"), [ fallback({ fallbackValue: "DEFAULT_VALUE", errorPolicy: UnexpectedError, }), ], ); await func(); ``` You can also pass multiple error classes: ./samples/multiple-classes.ts ``` import { fallback } from "eridu-tech/resilience"; import { use } from "eridu-tech/middleware"; import { UnexpectedError } from "eridu-tech/utilities"; class CustomErrorB extends Error { constructor(message: string, cause?: unknown) { super(message, { cause }); this.name = CustomErrorB.name; } } const func = use( () => Promise.resolve("DATA"), [ fallback({ fallbackValue: "DEFAULT_VALUE", errorPolicy: [UnexpectedError, CustomErrorB], }), ], ); await func(); ``` ## Standard Schema as ErrorPolicy[​](#standard-schema-as-errorpolicy "Direct link to Standard Schema as ErrorPolicy") You can use any [standard schema](https://standardschema.dev/) as error policy: ./samples/standard-schema.ts ``` import { z } from "zod"; import { fallback } from "eridu-tech/resilience"; import { use } from "eridu-tech/middleware"; const func = use( () => Promise.resolve("DATA"), [ fallback({ fallbackValue: "DEFAULT_VALUE", errorPolicy: z.object({ code: z.literal("e20"), message: z.string(), }), }), ], ); await func(); ``` ## False return values as error[​](#false-return-values-as-error "Direct link to False return values as error") You can treat false return values as errors. This useful when you want to retry functions that return boolean. ./samples/treat-false-as-error.ts ``` import { retry } from "eridu-tech/resilience"; import { use } from "eridu-tech/middleware"; const func = use(async (): Promise => { // Will be console.log("EXECUTING"); return false; }, [ retry({ maxAttempts: 4, errorPolicy: { treatFalseAsError: true, }, }), ]); await func(); ``` ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/utilities`](https://eridu-tech.github.io/eridu-tech-core/types/Utilities.ErrorPolicy.html) API docs. --- # FileSize The `eridu-tech/file-size` component provides an easy way for defining, manipulating, and comparing file size. Furthermore, it is designed for easy integration with external file-size libraries. ### Creating a FileSize[​](#creating-a-filesize "Direct link to Creating a FileSize") Creating `FileSize` from bytes: ./samples/creating-from-bytes.ts ``` import { FileSize } from "eridu-tech/file-size"; const fileSize = FileSize.fromBytes(100); ``` Creating `FileSize` from kilo bytes: ./samples/creating-from-kilo-bytes.ts ``` import { FileSize } from "eridu-tech/file-size"; const fileSize = FileSize.fromKiloBytes(100); ``` Creating `FileSize` from mega bytes: ./samples/creating-from-mega-bytes.ts ``` import { FileSize } from "eridu-tech/file-size"; const fileSize = FileSize.fromMegaBytes(100); ``` Creating `FileSize` from giga bytes: ./samples/creating-from-giga-bytes.ts ``` import { FileSize } from "eridu-tech/file-size"; const fileSize = FileSize.fromGigaBytes(100); ``` Creating `FileSize` from tera bytes: ./samples/creating-from-tera-bytes.ts ``` import { FileSize } from "eridu-tech/file-size"; const fileSize = FileSize.fromTeraBytes(1); ``` Creating `FileSize` from peta bytes: ./samples/creating-from-peta-bytes.ts ``` import { FileSize } from "eridu-tech/file-size"; const fileSize = FileSize.fromPetaBytes(1); ``` ### Comparing FileSize:s[​](#comparing-filesize "Direct link to comparing-filesize") Equals: ./samples/comparing-equal.ts ``` import { FileSize } from "eridu-tech/file-size"; // Returns false FileSize.fromBytes(20_000).equals(FileSize.fromBytes(40_000)); ``` Greater than: ./samples/comparing-gt.ts ``` import { FileSize } from "eridu-tech/file-size"; // Returns false FileSize.fromBytes(20_000).gt(FileSize.fromBytes(40_000)); ``` Greater than or equals: ./samples/comparing-gte.ts ``` import { FileSize } from "eridu-tech/file-size"; // Returns false FileSize.fromBytes(20_000).gte(FileSize.fromBytes(40_000)); ``` Less than: ./samples/comparing-lt.ts ``` import { FileSize } from "eridu-tech/file-size"; // Returns true FileSize.fromBytes(20_000).lt(FileSize.fromBytes(40_000)); ``` Less than or equals: ./samples/comparing-lte.ts ``` import { FileSize } from "eridu-tech/file-size"; // Returns true FileSize.fromBytes(20_000).lte(FileSize.fromBytes(40_000)); ``` ### Converting a FileSize[​](#converting-a-filesize "Direct link to Converting a FileSize") You can get amount of bytes contained in the `FileSize`: ./samples/converting-to-bytes.ts ``` import { FileSize } from "eridu-tech/file-size"; FileSize.fromKiloBytes(1).toBytes(); ``` You can get amount of kilo bytes contained in the `FileSize`: ./samples/converting-to-kilo-bytes.ts ``` import { FileSize } from "eridu-tech/file-size"; FileSize.fromMegaBytes(1).toKiloBytes(); ``` You can get amount of giga bytes contained in the `FileSize`: ./samples/converting-to-giga-bytes.ts ``` import { FileSize } from "eridu-tech/file-size"; FileSize.fromTeraBytes(1).toGigaBytes(); ``` You can get amount of tera bytes contained in the `FileSize`: ./samples/converting-to-tera-bytes.ts ``` import { FileSize } from "eridu-tech/file-size"; FileSize.fromPetaBytes(1).toTeraBytes(); ``` You can get amount of peta bytes contained in the `FileSize`: ./samples/converting-to-peta-bytes.ts ``` import { FileSize } from "eridu-tech/file-size"; FileSize.fromPetaBytes(1000).toPetaBytes(); ``` ### Serialization and deserialization of FileSize[​](#serialization-and-deserialization-of-filesize "Direct link to Serialization and deserialization of FileSize") The `FileSize` class supports serialization and deserialization, allowing you to easily convert instances to and from serialized formats. However, registration is required first: ./samples/serde-serialization.ts ``` import { SuperJsonSerde } from "eridu-tech/serde/super-json-serde"; import { FileSize } from "eridu-tech/file-size"; const serde = new SuperJsonSerde(); serde.registerCustom(FileSize.serdeTransformer); const fileSize = FileSize.fromBytes(12); const serializedFileSize = await serde.serialize(fileSize); const deserializedFileSize = await serde.deserialize(serializedFileSize); // logs false console.log(serializedFileSize === deserializedFileSize); ``` ## FileSize contract[​](#filesize-contract "Direct link to FileSize contract") The `IFileSize` contract provides a standardized way to express a file-size as bytes. Key components like `FileStorage`, rely on this contract, ensuring they are not tightly coupled to a specific file-size implementation. This decoupling is crucial for interoperability, allowing seamless integration with external file-size libraries. To integrate a new library, its file-size objects must simply implement the `IFileSize` contract. info Note `FileSize` class implements `IFileSize` contract. The `IFileSize` contract requires you to implement the `TO_MILLISECONDS` method on the file-size object, which must return the file-size in milliseconds. ./samples/implementing-ifile-size.ts ``` import { type IFileSize, TO_BYTES } from "eridu-tech/file-size/contracts"; export class MyFileSize implements IFileSize { constructor(private readonly fileSizeInBytes: number) {} [TO_BYTES](): number { return this.fileSizeInBytes; } } ``` ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/file-size`](https://eridu-tech.github.io/eridu-tech-core/modules/FileSize.html) API docs. --- # Generic pool The `eridu-tech/generic-pool` component reuses expensive resources instead of creating a new one for every call. The examples below pool SFTP connections created with `ssh2-sftp-client`, which are slow to open and cannot be shared by concurrent operations. ## Contracts[​](#contracts "Direct link to Contracts") The contracts are exported from `eridu-tech/generic-pool/contracts`: * `IResourceFactory` creates a resource, destroys it and optionally validates it before it is reused. * `IGenericPoolBase` borrows a client with `acquireOrFail()`, returns it with `release()` and runs a callback with a client through `run()`. * `IGenericPool` extends `IGenericPoolBase` with `close()`. ## QueuePool[​](#queuepool "Direct link to QueuePool") `QueuePool` creates resources on demand and queues callers in first-in-first-out order when no resource is available. An `SftpClient` is connected when the pool creates it and ended when the pool destroys it: ./samples/queue-pool.ts ``` import { QueuePool } from "eridu-tech/generic-pool"; import SftpClient from "ssh2-sftp-client"; const connectionFactory = { create: async (): Promise => { const client = new SftpClient("sftp-connection"); await client.connect({ host: "sftp.example.com", port: 22, username: "user", password: "password", }); return client; }, destroy: async (client: SftpClient): Promise => { await client.end(); }, }; const sftpPool = new QueuePool(connectionFactory); const fileNames = await sftpPool.run(async (client) => { const files = await client.list("/uploads"); return files.map((file) => file.name); }); console.log(fileNames); ``` The pool is configured with the following settings: * `maxResources` - Maximum number of resources the pool can create. Default `10`. * `maxWaitSize` - Maximum number of callers that can wait for a resource, `null` means no limit. Default `null`. ./samples/queue-pool-settings.ts ``` import { QueuePool } from "eridu-tech/generic-pool"; import SftpClient from "ssh2-sftp-client"; const sftpPool = new QueuePool( { create: async () => { const client = new SftpClient("sftp-connection"); await client.connect({ host: "sftp.example.com", port: 22, username: "user", password: "password", }); return client; }, destroy: async (client) => { await client.end(); }, }, { maxResources: 5, maxWaitSize: 100, }, ); ``` Instead of `run()`, you can borrow a client manually. Always release it when you are done, preferably in a `finally` block: ./samples/queue-pool-acquire-release.ts ``` import { QueuePool } from "eridu-tech/generic-pool"; import SftpClient from "ssh2-sftp-client"; const sftpPool = new QueuePool({ create: async () => { const client = new SftpClient("sftp-connection"); await client.connect({ host: "sftp.example.com", port: 22, username: "user", password: "password", }); return client; }, destroy: async (client) => { await client.end(); }, }); const client = await sftpPool.acquireOrFail(); try { await client.put("./report.pdf", "/uploads/report.pdf"); } finally { await sftpPool.release(client); } ``` A recycled connection is checked with the optional `IResourceFactory.validate()`. When it returns `false` or throws, the connection is ended and replaced, and the caller goes back to the front of the queue. Here `cwd()` reports whether the connection still responds: ./samples/queue-pool-validate.ts ``` import { QueuePool } from "eridu-tech/generic-pool"; import SftpClient from "ssh2-sftp-client"; const sftpPool = new QueuePool({ create: async () => { const client = new SftpClient("sftp-connection"); await client.connect({ host: "sftp.example.com", port: 22, username: "user", password: "password", }); return client; }, destroy: async (client) => { await client.end(); }, // A connection that no longer responds is ended and replaced. validate: async (client) => { try { await client.cwd(); return true; } catch { return false; } }, }); ``` Closing the pool ends all idle connections and rejects every waiting caller. Connections that are still borrowed are ended when they are released: ./samples/queue-pool-close.ts ``` import { QueuePool } from "eridu-tech/generic-pool"; import { UnableToAcquireError } from "eridu-tech/generic-pool/contracts"; import SftpClient from "ssh2-sftp-client"; const sftpPool = new QueuePool({ create: async () => { const client = new SftpClient("sftp-connection"); await client.connect({ host: "sftp.example.com", port: 22, username: "user", password: "password", }); return client; }, destroy: async (client) => { await client.end(); }, }); const client = await sftpPool.acquireOrFail(); // Ends the idle connections, waiting callers are rejected. await sftpPool.close(); // Later acquisitions are rejected too. try { await sftpPool.acquireOrFail(); } catch (error) { console.log(error instanceof UnableToAcquireError); } // The borrowed connection is ended when it is released. await sftpPool.release(client); ``` ## NoOpPool[​](#nooppool "Direct link to NoOpPool") `NoOpPool` gives the same client to every caller and ignores releases. Use it when the connection is already exclusive, for example when your application opens one connection itself: ./samples/no-op-pool.ts ``` import { NoOpPool } from "eridu-tech/generic-pool"; import SftpClient from "ssh2-sftp-client"; const client = new SftpClient("shared-sftp-connection"); await client.connect({ host: "sftp.example.com", port: 22, username: "user", password: "password", }); const sftpPool = new NoOpPool({ client, destroy: async (sftpClient) => { await sftpClient.end(); }, }); const first = await sftpPool.acquireOrFail(); const second = await sftpPool.acquireOrFail(); // logs true, every caller receives the same connection console.log(first === second); await sftpPool.close(); ``` ## Errors[​](#errors "Direct link to Errors") `acquireOrFail()` rejects with `UnableToAcquireError` when a client cannot be acquired, for example when the wait queue is full or the pool has been closed: ./samples/unable-to-acquire-error.ts ``` import { QueuePool } from "eridu-tech/generic-pool"; import { UnableToAcquireError } from "eridu-tech/generic-pool/contracts"; import SftpClient from "ssh2-sftp-client"; const sftpPool = new QueuePool( { create: async () => { const client = new SftpClient("sftp-connection"); await client.connect({ host: "sftp.example.com", port: 22, username: "user", password: "password", }); return client; }, destroy: async (client) => { await client.end(); }, }, { maxResources: 1, maxWaitSize: 0, }, ); const client = await sftpPool.acquireOrFail(); try { // The only connection is borrowed and no caller is allowed to wait. await sftpPool.acquireOrFail(); } catch (error) { // logs true console.log(error instanceof UnableToAcquireError); } await sftpPool.release(client); ``` ## Usage in the library[​](#usage-in-the-library "Direct link to Usage in the library") The FTP and SFTP file storage adapters use this component to pool their clients, so concurrent operations do not share one connection. For SFTP, `SftpFactory` is an `IResourceFactory` and `sftpPool()` combines it with a pool, both available from `eridu-tech/file-storage/sftp-file-storage-adapter`. ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/generic-pool`](https://eridu-tech.github.io/eridu-tech-core/modules/GenericPool.html) API docs. --- # Invocable An [`Invocable`](https://eridu-tech.github.io/eridu-tech-core/types/Utilities.Invocable.html) represents a callable entity, which can be either: 1. A function `InvocableFn` 2. An object with a specific invocation signature (`IInvocableObject`) ## Types[​](#types "Direct link to Types") * `Invocable` - Union type of `InvocableFn` and `IInvocableObject` * `InvocableFn` - Function signature * `IInvocableObject` - Object with `invoke` method ## Function Invocable (`InvocableFn`)[​](#function-invocable-invocablefn "Direct link to function-invocable-invocablefn") Represents a standard function with typed parameters and return value. ./samples/fn.ts ``` import type { InvocableFn } from "eridu-tech/utilities"; // Using InvocableFn type AddFunction = InvocableFn<[arg1: number, arg2: number], number>; // Equivalent to: type TraditionalFunction = (arg1: number, arg2: number) => number; ``` ## Object Invocable (`IInvocableObject`)[​](#object-invocable-iinvocableobject "Direct link to object-invocable-iinvocableobject") An object that implements a callable contract through an invoke method. This pattern is especially useful for dependency injection (DI) integration, as most DI frameworks are adapted for class-based resolution. ./samples/object.ts ``` import type { IInvocableObject } from "eridu-tech/utilities"; class InvocableObject implements IInvocableObject< [arg1: number, arg2: number], number > { invoke(arg1: number, arg2: number): number { throw new Error("Method not implemented."); } } const invocableObject: IInvocableObject<[arg1: number, arg2: number], number> = { invoke(arg1: number, arg2: number): number { throw new Error("Method not implemented."); }, }; ``` ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/utilities`](https://eridu-tech.github.io/eridu-tech-core/types/Utilities.Invocable.html) API docs. --- # Utilities Small, focused helpers shared across the ecosystem. * [Collection](/docs/utilities/collection/.md) — Type-safe collection utilities with powerful query, transform, and pagination primitives. * [TimeSpan](/docs/utilities/time_span/.md) — Define, manipulate, and compare durations with a typed, immutable API, integrates easily with time libraries like Luxon and Dayjs. * [FileSize](/docs/utilities/file_size/.md) — Define, manipulate, and compare file sizes with a typed API, from bytes to gigabytes, with easy unit conversion. * [Generic Pool](/docs/utilities/generic_pool/.md) — Reuses expensive clients such as FTP and SFTP connections, borrowing and releasing pooled resources with QueuePool or sharing an already exclusive client with NoOpPool. * [Backoff Policies](/docs/utilities/backoff_policies/.md) — Predefined retry backoff policies, constant and exponential, with configurable delay and jitter. * [ErrorPolicy Type](/docs/utilities/error_policy_type/.md) — Decide which errors are handled by resilience middlewares like retry and fallback, using a predicate function or one or more error classes. * [Invocable](/docs/utilities/invocable/.md) — A callable entity, either a plain function or an object exposing an invoke method, so functions and class-based services flow through one shared contract. --- # TimeSpan The `eridu-tech/time-span` component provides an easy way for defining, manipulating, and comparing durations. Furthermore, it is designed for easy integration with external time libraries like Luxon and Dayjs. ## TimeSpan class[​](#timespan-class "Direct link to TimeSpan class") The `TimeSpan` class is used for representing time interval. info Note `TimeSpan` cannot be negative. ### Creating a TimeSpan[​](#creating-a-timespan "Direct link to Creating a TimeSpan") Creating `TimeSpan` from milliseconds: ./samples/creating-from-milliseconds.ts ``` import { TimeSpan } from "eridu-tech/time-span"; export const timeSpan = TimeSpan.fromMilliseconds(100); ``` Creating `TimeSpan` from seconds: ./samples/creating-from-seconds.ts ``` import { TimeSpan } from "eridu-tech/time-span"; const timeSpan = TimeSpan.fromSeconds(30); ``` Creating `TimeSpan` from minutes: ./samples/creating-from-minutes.ts ``` import { TimeSpan } from "eridu-tech/time-span"; const timeSpan = TimeSpan.fromMinutes(15); ``` Creating `TimeSpan` from hours: ./samples/creating-from-hours.ts ``` import { TimeSpan } from "eridu-tech/time-span"; const timeSpan = TimeSpan.fromHours(1); ``` Creating `TimeSpan` from days: ./samples/creating-from-days.ts ``` import { TimeSpan } from "eridu-tech/time-span"; const timeSpan = TimeSpan.fromDays(1); ``` Creating `TimeSpan` from date range: ./samples/creating-from-date-range.ts ``` import { TimeSpan } from "eridu-tech/time-span"; const timeSpan = TimeSpan.fromDateRange({ start: new Date("2000-01-01"), end: new Date("2010-01-01"), }); ``` Creating `TimeSpan` from `string`: ./samples/creating-from-str.ts ``` import { TimeSpan } from "eridu-tech/time-span"; const timeSpan = TimeSpan.fromStr("5s"); ``` info Under the hood, this method leverages [@lukeed/ms](https://www.npmjs.com/package/@lukeed/ms) package to convert various time formats into milliseconds. Refer to its documentation for a complete list of supported time formats and units. ### Adding time to TimeSpan[​](#adding-time-to-timespan "Direct link to Adding time to TimeSpan") You can add milliseconds to a `TimeSpan`: ./samples/adding-milliseconds.ts ``` import { timeSpan } from "./creating-from-milliseconds.js"; timeSpan.addMilliseconds(200); ``` You can add seconds to a `TimeSpan`:\` ./samples/adding-seconds.ts ``` import { timeSpan } from "./creating-from-milliseconds.js"; timeSpan.addSeconds(30); ``` You can add minutes to a `TimeSpan`: ./samples/adding-minutes.ts ``` import { timeSpan } from "./creating-from-milliseconds.js"; timeSpan.addMinutes(20); ``` You can add hours to a `TimeSpan`: ./samples/adding-hours.ts ``` import { timeSpan } from "./creating-from-milliseconds.js"; timeSpan.addHours(2); ``` You can add days to a `TimeSpan`: ./samples/adding-days.ts ``` import { timeSpan } from "./creating-from-milliseconds.js"; timeSpan.addDays(14); ``` You can add 2 `TimeSpan` together: ./samples/adding-time-span.ts ``` import { timeSpan } from "./creating-from-milliseconds.js"; import { TimeSpan } from "eridu-tech/time-span"; timeSpan.addTimeSpan(TimeSpan.fromDays(14).addHours(20)); ``` ### Subtracting time from TimeSpan[​](#subtracting-time-from-timespan "Direct link to Subtracting time from TimeSpan") You can subtract milliseconds from a `TimeSpan`: ./samples/subtracting-milliseconds.ts ``` import { timeSpan } from "./creating-from-milliseconds.js"; timeSpan.subtractMilliseconds(200); ``` You can subtract seconds from a `TimeSpan`:\` ./samples/subtracting-seconds.ts ``` import { timeSpan } from "./creating-from-milliseconds.js"; timeSpan.subtractSeconds(30); ``` You can subtract minutes from a `TimeSpan`: ./samples/subtracting-minutes.ts ``` import { timeSpan } from "./creating-from-milliseconds.js"; timeSpan.subtractMinutes(20); ``` You can subtract hours from a `TimeSpan`: ./samples/subtracting-hours.ts ``` import { timeSpan } from "./creating-from-milliseconds.js"; timeSpan.subtractHours(2); ``` You can subtract days from a `TimeSpan`: ./samples/subtracting-days.ts ``` import { timeSpan } from "./creating-from-milliseconds.js"; timeSpan.subtractDays(14); ``` You can subtract 2 `TimeSpan` together: ./samples/subtracting-time-span.ts ``` import { timeSpan } from "./creating-from-milliseconds.js"; import { TimeSpan } from "eridu-tech/time-span"; timeSpan.subtractTimeSpan(TimeSpan.fromDays(14).addHours(20)); ``` ### Multiplying and dividing a TimeSpan[​](#multiplying-and-dividing-a-timespan "Direct link to Multiplying and dividing a TimeSpan") Dividing a timespan: ./samples/dividing.ts ``` import { TimeSpan } from "eridu-tech/time-span"; // Will be now 100 milliseconds TimeSpan.fromMilliseconds(200).divide(2); ``` Multiplying a timespan: ./samples/multiplying.ts ``` import { TimeSpan } from "eridu-tech/time-span"; // Will be now 400 milliseconds TimeSpan.fromMilliseconds(200).multiply(2); ``` ### Comparing TimeSpan:s[​](#comparing-timespan "Direct link to comparing-timespan") Equals: ./samples/comparing-equal.ts ``` import { TimeSpan } from "eridu-tech/time-span"; // Returns false TimeSpan.fromSeconds(1).equals(TimeSpan.fromSeconds(2)); ``` Greater than: ./samples/comparing-gt.ts ``` import { TimeSpan } from "eridu-tech/time-span"; // Returns false TimeSpan.fromSeconds(1).gt(TimeSpan.fromSeconds(2)); ``` Greater than or equals: ./samples/comparing-gte.ts ``` import { TimeSpan } from "eridu-tech/time-span"; // Returns false TimeSpan.fromSeconds(1).gte(TimeSpan.fromSeconds(2)); ``` Less than: ./samples/comparing-lt.ts ``` import { TimeSpan } from "eridu-tech/time-span"; // Returns true TimeSpan.fromSeconds(1).lt(TimeSpan.fromSeconds(2)); ``` Less than or equals: ./samples/comparing-lte.ts ``` import { TimeSpan } from "eridu-tech/time-span"; // Returns true TimeSpan.fromSeconds(1).lte(TimeSpan.fromSeconds(2)); ``` ### Converting a TimeSpan[​](#converting-a-timespan "Direct link to Converting a TimeSpan") You can get amount of milliseconds contained in the `TimeSpan`: ./samples/converting-to-milliseconds.ts ``` import { TimeSpan } from "eridu-tech/time-span"; TimeSpan.fromSeconds(1).toMilliseconds(); ``` You can get amount of seconds contained in the `TimeSpan`: ./samples/converting-to-seconds.ts ``` import { TimeSpan } from "eridu-tech/time-span"; TimeSpan.fromMinutes(1).toSeconds(); ``` You can get amount of minutes contained in the `TimeSpan`: ./samples/converting-to-minutes.ts ``` import { TimeSpan } from "eridu-tech/time-span"; TimeSpan.fromHours(1).toMinutes(); ``` You can get amount of hours contained in the `TimeSpan`: ./samples/converting-to-hours.ts ``` import { TimeSpan } from "eridu-tech/time-span"; TimeSpan.fromDays(1).toHours(); ``` You can get amount of days contained in the `TimeSpan`: ./samples/converting-to-days.ts ``` import { TimeSpan } from "eridu-tech/time-span"; TimeSpan.fromHours(48).toDays(); ``` You can get end date relative to a start date: ./samples/converting-to-end-date.ts ``` import { TimeSpan } from "eridu-tech/time-span"; // Will return date of "2002-01-01" TimeSpan.fromDays(365).toEndDate(new Date("2001-01-01")); ``` You can get start date relative to a end date: ./samples/converting-to-start-date.ts ``` import { TimeSpan } from "eridu-tech/time-span"; // Will return date of "2000-01-01" TimeSpan.fromDays(365).toStartDate(new Date("2001-01-01")); ``` ### Serialization and deserialization of TimeSpan[​](#serialization-and-deserialization-of-timespan "Direct link to Serialization and deserialization of TimeSpan") The `TimeSpan` class supports serialization and deserialization, allowing you to easily convert instances to and from serialized formats. However, registration is required first: ./samples/serde-serialization.ts ``` import { SuperJsonSerde } from "eridu-tech/serde/super-json-serde"; import { TimeSpan } from "eridu-tech/time-span"; const serde = new SuperJsonSerde(); serde.registerCustom(TimeSpan.serdeTransformer); const timeSpan = TimeSpan.fromSeconds(12); const serializedTimeSpan = await serde.serialize(timeSpan); const deserializedTimeSpan = await serde.deserialize(serializedTimeSpan); // logs false console.log(serializedTimeSpan === deserializedTimeSpan); ``` ## ITimeSpan contract[​](#itimespan-contract "Direct link to ITimeSpan contract") The `ITimeSpan` contract provides a standardized way to express a duration as milliseconds. Key components, including `Cache` and `Lock`, rely on this contract, ensuring they are not tightly coupled to a specific duration implementation. This decoupling is crucial for interoperability, allowing seamless integration with external time libraries like `Luxon` or `Dayjs`. To integrate a new library, its duration objects must simply implement the `ITimeSpan` contract. info Note `TimeSpan` class implements `ITimeSpan` contract. The `ITimeSpan` contract requires you to implement the `TO_MILLISECONDS` method on the duration object, which must return the duration in milliseconds. ./samples/implementing-itimespan.ts ``` import { type ITimeSpan, TO_MILLISECONDS, } from "eridu-tech/time-span/contracts"; export class Duration implements ITimeSpan { constructor(private readonly timeInMs: number) {} [TO_MILLISECONDS](): number { return this.timeInMs; } } ``` ## Further information[​](#further-information "Direct link to Further information") For further information refer to [`eridu-tech/time-span`](https://eridu-tech.github.io/eridu-tech-core/modules/TimeSpan.html) API docs. --- # Configuring HttpRouter adapters `HttpRouter` is adapter-based: it delegates URL pattern matching to a Hono `Router` instance. Any existing Hono-compatible adapter can be used interchangeably with the `HttpRouter` class. ## SmartRouter[​](#smartrouter "Direct link to SmartRouter") `SmartRouter` is the recommended adapter. It composes multiple routers and automatically selects the best matching router for each route. `eridu-tech/http-router` exports [`defaultHttpRouterAdapter`](https://eridu-tech.github.io/eridu-tech-core/variables/HttpRouter.defaultHttpRouterAdapter.html) to reduce boilerplate. It is equivalent to: ./samples/smart-router-default-equivalent.ts ``` import { SmartRouter } from "hono/router/smart-router"; import { RegExpRouter } from "hono/router/reg-exp-router"; import { TrieRouter } from "hono/router/trie-router"; new SmartRouter({ routers: [new RegExpRouter(), new TrieRouter()], }); ``` ./samples/smart-router-default.ts ``` import { HttpRouter, defaultHttpRouterAdapter } from "eridu-tech/http-router"; new HttpRouter({ router: defaultHttpRouterAdapter(), }); ``` You can also configure `SmartRouter` explicitly: ./samples/smart-router-explicit.ts ``` import { HttpRouter } from "eridu-tech/http-router"; import { SmartRouter } from "hono/router/smart-router"; import { RegExpRouter } from "hono/router/reg-exp-router"; import { TrieRouter } from "hono/router/trie-router"; new HttpRouter({ router: new SmartRouter({ routers: [new RegExpRouter(), new TrieRouter()], }), }); ``` info `SmartRouter` provides the best all-around performance by delegating each route to the optimal underlying router. Prefer it unless you have a specific reason to use a single router. ## RegExpRouter[​](#regexprouter "Direct link to RegExpRouter") `RegExpRouter` compiles all routes into a single regular expression for fast matching. It is best suited for applications with many static routes. ./samples/regexp-router.ts ``` import { HttpRouter } from "eridu-tech/http-router"; import { RegExpRouter } from "hono/router/reg-exp-router"; new HttpRouter({ router: new RegExpRouter() }); ``` ## TrieRouter[​](#trierouter "Direct link to TrieRouter") `TrieRouter` performs linear trie traversal for route matching. It is best suited for applications with many dynamic path parameters. ./samples/trie-router.ts ``` import { HttpRouter } from "eridu-tech/http-router"; import { TrieRouter } from "hono/router/trie-router"; new HttpRouter({ router: new TrieRouter() }); ``` ## LinearRouter[​](#linearrouter "Direct link to LinearRouter") `LinearRouter` registers routes very quickly, making it suitable for environments that initialize applications on every request. ./samples/linear-router.ts ``` import { HttpRouter } from "eridu-tech/http-router"; import { LinearRouter } from "hono/router/linear-router"; new HttpRouter({ router: new LinearRouter() }); ``` ## PatternRouter[​](#patternrouter "Direct link to PatternRouter") `PatternRouter` is the smallest router, simply adding and matching patterns. It is best suited for minimal footprint applications. ./samples/pattern-router.ts ``` import { HttpRouter } from "eridu-tech/http-router"; import { PatternRouter } from "hono/router/pattern-router"; new HttpRouter({ router: new PatternRouter() }); ``` ## Further information[​](#further-information "Direct link to Further information") For further information refer to: * [Hono Routing — official documentation](https://hono.dev/docs/concepts/routers) --- # Creating HttpRouter adapters `HttpRouter` relies on the **Hono router adapter interface** for URL pattern matching and request dispatching. It does **not** ship with a built-in router. Instead, you supply a Hono `Router` instance that satisfies the adapter interface. ## Implementing your custom Router adapter[​](#implementing-your-custom-router-adapter "Direct link to Implementing your custom Router adapter") In order to create an adapter you need to implement the [`Router`](https://github.com/honojs/hono/blob/main/src/router.ts) contract from Hono. The interface defines three members: ./samples/router-adapter-contract.ts ``` import type { Result } from "hono/router"; interface Router { name: string; add(method: string, path: string, handler: T): void; match(method: string, path: string): Result; } ``` ## Further information[​](#further-information "Direct link to Further information") For further information refer to: * [Hono GitHub repository](https://github.com/honojs/hono): source code for the Hono framework, including the router adapters (`hono/router`). * [Hono Routing official documentation](https://hono.dev/docs/concepts/routers): guide to Hono's routing system, including path patterns, route grouping, and router selection. --- # Analog.js Analog.js API routes are powered by [h3](https://h3.unjs.io/). Use [`fromWebHandler`](https://v1.h3.dev/guide/event-handler#converting-from-web-handlers) from `h3` to convert winter-tc request handler into h3 request handler. ### 1. Install[​](#1-install "Direct link to 1. Install") ``` npm install eridu-tech hono ``` ### 2. Create the handler[​](#2-create-the-handler "Direct link to 2. Create the handler") src/server/routes/api/\[...].ts ``` import { HttpRouter, defaultHttpRouterAdapter } from "eridu-tech/http-router"; import { fromWebHandler } from "h3"; const router = new HttpRouter({ router: defaultHttpRouterAdapter(), baseUrl: "/api", }); router.endpoint({ url: "/hello/:name?", method: ["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"], handler: (args) => { const { name } = args.req.params(); return args.text( [ `METHOD: ${args.req.method},`, "Hello from Next.js", name ?? "", "!", ] .filter((str) => str !== "") .join(" ") .trim(), ); }, }); export default fromWebHandler((req) => { return router.fetch(req); }); ``` **File structure** ``` . ├── src │ └── server │ └── routes │ └── api │ └── [...].ts ├── package.json ``` ### 3. Develop[​](#3-develop "Direct link to 3. Develop") ``` npm run dev ``` ### 4. Build[​](#4-build "Direct link to 4. Build") ``` npm run build ``` **Reference:** [Analog API Routes](https://analogjs.org/docs/features/api/overview) --- # Next.js App Router `HttpRouter` can be used directly with Next.js App Router route handlers. ### 1. Install[​](#1-install "Direct link to 1. Install") ``` npm install eridu-tech hono ``` ### 2. Create the handler[​](#2-create-the-handler "Direct link to 2. Create the handler") app/api/\[\[...route]]/route.ts ``` import { HttpRouter, defaultHttpRouterAdapter } from "eridu-tech/http-router"; const router = new HttpRouter({ router: defaultHttpRouterAdapter(), baseUrl: "/api", }); router.endpoint({ url: "/hello/:name?", method: ["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"], handler: (args) => { const { name } = args.req.params(); return args.text( [ `METHOD: ${args.req.method},`, "Hello from Next.js", name ?? "", "!", ] .filter((str) => str !== "") .join(" ") .trim(), ); }, }); export const GET = async (request: Request) => router.fetch(request); export const HEAD = async (request: Request) => router.fetch(request); export const POST = async (request: Request) => router.fetch(request); export const PUT = async (request: Request) => router.fetch(request); export const DELETE = async (request: Request) => router.fetch(request); export const PATCH = async (request: Request) => router.fetch(request); export const OPTIONS = async (request: Request) => router.fetch(request); ``` **File structure** ``` . ├── app │ └── api │ └── [[...route]] │ └── route.ts ├── package.json ``` ### 3. Develop[​](#3-develop "Direct link to 3. Develop") ``` npm run dev ``` ### 4. Build[​](#4-build "Direct link to 4. Build") ``` npm run build ``` ### 5. Start[​](#5-start "Direct link to 5. Start") ``` npm start ``` info Next.js automatically maps HTTP methods to the exported handler names (`GET`, `POST`, etc.). Route groups and prefixes defined via `HttpRouter.route()` map to the file-system route. **Reference:** [Next.js Route Handlers](https://nextjs.org/docs/app/building-your-application/routing/route-handlers) --- # Next.js Pages Router `HttpRouter` can be used directly with Next.js Pages Router route handlers. ### 1. Install[​](#1-install "Direct link to 1. Install") ``` npm install eridu-tech hono @hono/node-server ``` ### 2. Create the handler[​](#2-create-the-handler "Direct link to 2. Create the handler") pages/api/\[\[...route]].ts ``` import { HttpRouter, defaultHttpRouterAdapter } from "eridu-tech/http-router"; import { getRequestListener } from "@hono/node-server"; const router = new HttpRouter({ router: defaultHttpRouterAdapter(), baseUrl: "/api", }); router.endpoint({ url: "/hello/:name?", method: ["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"], handler: (args) => { const { name } = args.req.params(); return args.text( [ `METHOD: ${args.req.method},`, "Hello from Next.js", name ?? "", "!", ] .filter((str) => str !== "") .join(" ") .trim(), ); }, }); export default getRequestListener((request: Request) => router.fetch(request)); ``` **File structure** ``` . ├── pages │ └── api │ └── [[...route]] │ └── route.ts ├── package.json ``` ### 3. Develop[​](#3-develop "Direct link to 3. Develop") ``` npm run dev ``` ### 4. Build[​](#4-build "Direct link to 4. Build") ``` npm run build ``` ### 5. Start[​](#5-start "Direct link to 5. Start") ``` npm start ``` info In order for this to work with the Pages Router, disable Vercel Node.js helpers by setting an environment variable in your project dashboard or in your `.env` file: ``` NODEJS_HELPERS=0 ``` **Reference:** [Next.js Pages Router API Routes](https://nextjs.org/docs/pages/building-your-application/routing/api-routes) --- # Nuxt Nuxt API routes are powered by [h3](https://h3.unjs.io/). Use [`fromWebHandler`](https://v1.h3.dev/guide/event-handler#converting-from-web-handlers) from `h3` to convert winter-tc request handler into h3 request handler. ### 1. Install[​](#1-install "Direct link to 1. Install") ``` npm install eridu-tech hono ``` ### 2. Create the handler[​](#2-create-the-handler "Direct link to 2. Create the handler") server/api/\[...].ts ``` import { HttpRouter, defaultHttpRouterAdapter } from "eridu-tech/http-router"; import { fromWebHandler } from "h3"; const router = new HttpRouter({ router: defaultHttpRouterAdapter(), baseUrl: "/api", }); router.endpoint({ url: "/hello/:name?", method: ["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"], handler: (args) => { const { name } = args.req.params(); return args.text( [ `METHOD: ${args.req.method},`, "Hello from Next.js", name ?? "", "!", ] .filter((str) => str !== "") .join(" ") .trim(), ); }, }); export default fromWebHandler((req) => { return router.fetch(req); }); ``` **File structure** ``` . ├── server │ └── api │ └── [...].ts ├── package.json ``` ### 3. Develop[​](#3-develop "Direct link to 3. Develop") ``` npm run dev ``` ### 4. Build[​](#4-build "Direct link to 4. Build") ``` npm run build ``` **Reference:** [Nuxt Server Routes](https://nuxt.com/docs/guide/directory-structure/server) --- # SolidStart SolidStart supports fetch-based route handlers natively. ### 1. Install[​](#1-install "Direct link to 1. Install") ``` npm install eridu-tech hono ``` ### 2. Create the handler[​](#2-create-the-handler "Direct link to 2. Create the handler") src/routes/api/\[...route].ts ``` import { HttpRouter, defaultHttpRouterAdapter } from "eridu-tech/http-router"; import type { APIEvent } from "@solidjs/start/server"; const router = new HttpRouter({ router: defaultHttpRouterAdapter(), baseUrl: "/api", }); router.endpoint({ url: "/hello/:name?", method: ["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"], handler: (args) => { const { name } = args.req.params(); return args.text( [ `METHOD: ${args.req.method},`, "Hello from Next.js", name ?? "", "!", ] .filter((str) => str !== "") .join(" ") .trim(), ); }, }); export const GET = ({ request }: APIEvent) => router.fetch(request); export const HEAD = ({ request }: APIEvent) => router.fetch(request); export const POST = ({ request }: APIEvent) => router.fetch(request); export const PUT = ({ request }: APIEvent) => router.fetch(request); export const DELETE = ({ request }: APIEvent) => router.fetch(request); export const PATCH = ({ request }: APIEvent) => router.fetch(request); export const OPTIONS = ({ request }: APIEvent) => router.fetch(request); ``` **File structure** ``` . ├── src │ └── routes │ └── api │ └── [...route].ts ├── package.json ``` ### 3. Develop[​](#3-develop "Direct link to 3. Develop") ``` npm run dev ``` ### 4. Build[​](#4-build "Direct link to 4. Build") ``` npm run build ``` **Reference:** [SolidStart API Routes](https://start.solidjs.com/api-routes) --- # SvelteKit `HttpRouter` works with SvelteKit server endpoints via the standard `Request`/`Response` API. ### 1. Install[​](#1-install "Direct link to 1. Install") ``` npm install eridu-tech hono ``` ### 2. Create the handler[​](#2-create-the-handler "Direct link to 2. Create the handler") src/routes/api/\[...route]/+server.ts ``` import { HttpRouter, defaultHttpRouterAdapter } from "eridu-tech/http-router"; const router = new HttpRouter({ router: defaultHttpRouterAdapter(), baseUrl: "/api", }); router.endpoint({ url: "/hello/:name?", method: ["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"], handler: (args) => { const { name } = args.req.params(); return args.text( [ `METHOD: ${args.req.method},`, "Hello from Next.js", name ?? "", "!", ] .filter((str) => str !== "") .join(" ") .trim(), ); }, }); const handler = ({ request }: { request: Request }) => { return router.fetch(request); }; export const GET = handler; export const POST = handler; export const PUT = handler; export const DELETE = handler; export const PATCH = handler; ``` **File structure** ``` . ├── src │ └── routes │ └── api │ └── [...route] │ └── +server.ts ├── package.json ``` ### 3. Develop[​](#3-develop "Direct link to 3. Develop") ``` npm run dev ``` ### 4. Build[​](#4-build "Direct link to 4. Build") ``` npm run build ``` **Reference:** [SvelteKit Server Endpoints](https://kit.svelte.dev/docs/routing#server) --- # TanStack Start TanStack Start uses Vinxi under the hood and supports standard fetch handlers. ### 1. Install[​](#1-install "Direct link to 1. Install") ``` npm install eridu-tech hono ``` ### 2. Create the handler[​](#2-create-the-handler "Direct link to 2. Create the handler") app/routes/api.$.ts ``` import { HttpRouter, defaultHttpRouterAdapter } from "eridu-tech/http-router"; import { createFileRoute } from "@tanstack/react-router"; const router = new HttpRouter({ router: defaultHttpRouterAdapter(), baseUrl: "/api", }); router.endpoint({ url: "/hello/:name?", method: ["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"], handler: (args) => { const { name } = args.req.params(); return args.text( [ `METHOD: ${args.req.method},`, "Hello from Next.js", name ?? "", "!", ] .filter((str) => str !== "") .join(" ") .trim(), ); }, }); const handler = async ({ request }: { request: Request }) => { return router.fetch(request); }; export const Route = createFileRoute("/api/$")({ server: { handlers: { GET: handler, HEAD: handler, POST: handler, PUT: handler, DELETE: handler, PATCH: handler, OPTIONS: handler, }, }, }); ``` **File structure** ``` . ├── app │ └── routes │ └── api.$.ts ├── package.json ``` ### 3. Develop[​](#3-develop "Direct link to 3. Develop") ``` npm run dev ``` ### 4. Build[​](#4-build "Direct link to 4. Build") ``` npm run build ``` **Reference:** [TanStack Start API Routes](https://tanstack.com/start/latest/docs/framework/react/api-routes) --- # Node.js Use the `@hono/node-server` adapter to serve `HttpRouter` on Node.js. ### 1. Install[​](#1-install "Direct link to 1. Install") ``` npm install eridu-tech hono @hono/node-server ``` ### 2. Create the handler[​](#2-create-the-handler "Direct link to 2. Create the handler") ./samples/create-handler.ts ``` // src/index.ts import { HttpRouter, defaultHttpRouterAdapter } from "eridu-tech/http-router"; import { serve } from "@hono/node-server"; const router = new HttpRouter({ router: defaultHttpRouterAdapter(), baseUrl: "/api", }); router.endpoint({ url: "/hello/:name?", method: ["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"], handler: (args) => { const { name } = args.req.params(); return args.text( [ `METHOD: ${args.req.method},`, "Hello from Next.js", name ?? "", "!", ] .filter((str) => str !== "") .join(" ") .trim(), ); }, }); const server = serve({ fetch: (request: Request) => router.fetch(request), port: 3000, }); // Graceful shutdown process.on("SIGINT", () => { server.close(); process.exit(0); }); process.on("SIGTERM", () => { server.close(() => process.exit(0)); }); ``` **File structure** ``` . ├── src │ └── index.ts ├── package.json └── tsconfig.json ``` ### 3. Develop[​](#3-develop "Direct link to 3. Develop") ``` npx tsx watch src/index.ts ``` ### 4. Build[​](#4-build "Direct link to 4. Build") ``` npx tsc ``` ### 5. Deploy[​](#5-deploy "Direct link to 5. Deploy") ``` node dist/index.js ``` **Dockerfile** ``` FROM node:22-alpine AS builder WORKDIR /app COPY package*.json tsconfig.json ./ RUN npm ci COPY src ./src RUN npm run build && npm prune --production FROM node:22-alpine WORKDIR /app COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/dist ./dist COPY --from=builder /app/package.json ./ EXPOSE 3000 CMD ["node", "dist/index.js"] ``` **Reference:** [Hono on Node.js](https://hono.dev/docs/getting-started/nodejs) --- # Frameworks, Runtimes, and Platforms `HttpRouter` implements the WinterTC fetch standard, exposing a `fetch(request: Request): Response | Promise` method. This makes it compatible with any framework, runtime, or platform adapter that accepts a standard fetch handler. ## Frameworks[​](#frameworks "Direct link to Frameworks") * [Next.js App Router](/docs/web/http_router/frameworks_and_runtimes/frameworks/nextjs_app_router/.md) * [Next.js Pages Router](/docs/web/http_router/frameworks_and_runtimes/frameworks/nextjs_pages_router/.md) * [Nuxt](/docs/web/http_router/frameworks_and_runtimes/frameworks/nuxt/.md) * [Analog.js](/docs/web/http_router/frameworks_and_runtimes/frameworks/analogjs/.md) * [SvelteKit](/docs/web/http_router/frameworks_and_runtimes/frameworks/sveltekit/.md) * [SolidStart](/docs/web/http_router/frameworks_and_runtimes/frameworks/solidstart/.md) * [TanStack Start](/docs/web/http_router/frameworks_and_runtimes/frameworks/tanstack_start/.md) ## JavaScript Runtimes[​](#javascript-runtimes "Direct link to JavaScript Runtimes") * [Node.js](/docs/web/http_router/frameworks_and_runtimes/javascript_runtimes/nodejs/.md) ## Further information[​](#further-information "Direct link to Further information") * [Hono documentation](https://hono.dev/docs/) — Primary reference for runtime adapters and integration patterns. * [`HttpRouter` API docs](https://eridu-tech.github.io/eridu-tech-core/classes/HttpRouter.HttpRouter.html) * [WinterTC fetch standard](https://wintertc.org/work) --- # HTTP Router usage The `eridu-tech/http-router` component provides a framework-agnostic HTTP router built on top of the [Hono](https://hono.dev/) router engine. It implements the **Winter TC fetch object standard**, which means it exposes a standard `fetch(request): Response` signature. This allows it to be integrated directly into any runtime or framework that supports the Fetch API including Node.js, Bun, Deno, Cloudflare Workers, Next.js, Nuxt, SvelteKit, and more. The router provides typed path parameters, a middleware chain with shared context, response helpers, cookie management, file upload validation, and schema-based request validation. ## Initial configuration[​](#initial-configuration "Direct link to Initial configuration") To begin using the `HttpRouter` class, you'll need to create and configure an instance: ./samples/http-router-initial-config.ts ``` import { HttpRouter } from "eridu-tech/http-router"; import { RegExpRouter } from "hono/router/reg-exp-router"; import { SmartRouter } from "hono/router/smart-router"; import { TrieRouter } from "hono/router/trie-router"; export const router = new HttpRouter({ router: new SmartRouter({ routers: [new RegExpRouter(), new TrieRouter()], }), }); ``` The `router` setting accepts any Hono-compatible router instance. For most use cases, the pre-configured `SmartRouter` with `RegExpRouter` and `TrieRouter` provides the best balance of performance and feature support. You can also use the bundled `defaultHttpRouterAdapter`: ./samples/default-adapter-http-router-initial-config.ts ``` import { HttpRouter, defaultHttpRouterAdapter } from "eridu-tech/http-router"; const router = new HttpRouter({ router: defaultHttpRouterAdapter(), }); ``` info Here is a complete list of settings for the [`HttpRouter`](https://eridu-tech.github.io/eridu-tech-core/types/HttpRouter.HttpRouterSettings.html) class. ## HttpRouter basics[​](#httprouter-basics "Direct link to HttpRouter basics") ### Defining endpoints[​](#defining-endpoints "Direct link to Defining endpoints") #### Basic endpoints[​](#basic-endpoints "Direct link to Basic endpoints") You can register an endpoint using the `endpoint` method with a URL pattern and handler: ./samples/basic-endpoint.ts ``` import { HttpRouter, defaultHttpRouterAdapter } from "eridu-tech/http-router"; const router = new HttpRouter({ router: defaultHttpRouterAdapter(), }); router.endpoint({ url: "/hello", method: ["GET"], handler: async ({ text }) => { return text("Hello World"); }, }); ``` #### HTTP methods[​](#http-methods "Direct link to HTTP methods") You can specify one or more HTTP methods an endpoint responds to: ./samples/http-methods.ts ``` import { router } from "./http-router-initial-config.js"; router.endpoint({ url: "/resource", method: ["GET"], handler: async ({ text }) => text("GET /resource"), }); router.endpoint({ url: "/resource", method: ["POST"], handler: async ({ text }) => text("POST /resource"), }); router.endpoint({ url: "/resource", method: ["PUT"], handler: async ({ text }) => text("PUT /resource"), }); router.endpoint({ url: "/resource", method: ["DELETE"], handler: async ({ text }) => text("DELETE /resource"), }); ``` When no `method` is specified, the endpoint responds to **all** HTTP methods (GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD, CONNECT, TRACE). You can also use custom HTTP methods like `PURGE`: ./samples/custom-http-method.ts ``` import { router } from "./http-router-initial-config.js"; router.endpoint({ url: "/cache", method: ["PURGE"], handler: async ({ text }) => text("PURGE Method /cache"), }); ``` #### Multiple methods[​](#multiple-methods "Direct link to Multiple methods") You can register the same handler for multiple methods at once: ./samples/multiple-methods.ts ``` import { router } from "./http-router-initial-config.js"; router.endpoint({ url: "/post", method: ["PUT", "DELETE"], handler: async ({ req, text }) => { return text(`${req.method} /post`); }, }); ``` #### Path parameters[​](#path-parameters "Direct link to Path parameters") Define dynamic path segments with `:paramName` syntax. The router automatically extracts path parameters and makes them available via `req.params()`: ./samples/path-parameters.ts ``` import { router } from "./http-router-initial-config.js"; router.endpoint({ url: "/users/:id", method: ["GET"], handler: async ({ req, json }) => { const params = req.params(); return json({ userId: params.id }); }, }); ``` Multiple path parameters are also supported: ./samples/multiple-path-parameters.ts ``` import { router } from "./http-router-initial-config.js"; router.endpoint({ url: "/posts/:id/comment/:commentId", method: ["GET"], handler: async ({ req, json }) => { const { id, commentId } = req.params(); return json({ postId: id, commentId }); }, }); ``` #### Optional parameters[​](#optional-parameters "Direct link to Optional parameters") Parameters can be made optional with the `?` suffix. The route matches both with and without the parameter: ./samples/optional-parameters.ts ``` import { router } from "./http-router-initial-config.js"; // Will match `/api/animal` and `/api/animal/:type` router.endpoint({ url: "/api/animal/:type?", method: ["GET"], handler: async ({ text }) => text("Animal!"), }); ``` #### Wildcard patterns[​](#wildcard-patterns "Direct link to Wildcard patterns") Use `*` as a wildcard segment to match any value: ./samples/wildcard-pattern.ts ``` import { router } from "./http-router-initial-config.js"; // Matches /wild/anything/card router.endpoint({ url: "/wild/*/card", method: ["GET"], handler: async ({ text }) => text("GET /wild/*/card"), }); ``` Deep wildcards match across multiple path segments: ./samples/deep-wildcard.ts ``` import { router } from "./http-router-initial-config.js"; // Matches /static/js/app.js, /static/css/style.css, etc. router.endpoint({ url: "/static/*", method: ["GET"], handler: async ({ text }) => text("Static file"), }); ``` #### Regex-constrained parameters[​](#regex-constrained-parameters "Direct link to Regex-constrained parameters") You can constrain path parameters with regular expressions: ./samples/regex-constrained-parameters.ts ``` import { router } from "./http-router-initial-config.js"; router.endpoint({ url: "/post/:date{[0-9]+}/:title{[a-z]+}", method: ["GET"], handler: async ({ req, json }) => { const { date, title } = req.params(); return json({ date, title }); }, }); ``` You can also use regexp patterns that include slashes: ./samples/regex-parameters-with-slashes.ts ``` import { router } from "./http-router-initial-config.js"; router.endpoint({ url: "/posts/:filename{.+\\.png}", method: ["GET"], handler: async ({ req, json }) => { const { filename } = req.params(); return json({ filename }); }, }); ``` #### Method matching behaviour[​](#method-matching-behaviour "Direct link to Method matching behaviour") If a request arrives for a path that exists but with a method that is not registered, the router returns a `404 Not Found` response: ./samples/method-matching-behaviour.ts ``` import { router } from "./http-router-initial-config.js"; router.endpoint({ url: "/get-only", method: ["GET"], handler: async ({ text }) => text("Only GET"), }); // POST /get-only → 404 Not Found // GET /get-only → 200 "Only GET" ``` ### Route grouping[​](#route-grouping "Direct link to Route grouping") You can group routes under a common prefix using the `group` method: ./samples/route-grouping.ts ``` import { router } from "./http-router-initial-config.js"; router.group("/api", (api) => { api.endpoint({ url: "/users", method: ["GET"], handler: async ({ json }) => json({ users: [] }), }); api.endpoint({ url: "/users/:id", method: ["GET"], handler: async ({ req, json }) => { const { id } = req.params(); return json({ userId: id }); }, }); }); ``` Routes defined inside the group are automatically prefixed. For example, `/users` becomes `/api/users`. Groups can also be nested without a prefix: ./samples/nested-group.ts ``` import { router } from "./http-router-initial-config.js"; router.group((sub) => { sub.endpoint({ url: "/nested", method: ["GET"], handler: async ({ text }) => text("Nested route"), }); }); ``` ### Handler arguments[​](#handler-arguments "Direct link to Handler arguments") Route handlers receive an object with the following properties: #### `req` The incoming request[​](#req-the-incoming-request "Direct link to req-the-incoming-request") The `req` object provides access to all request data: ./samples/handler-req-access.ts ``` import { router } from "./http-router-initial-config.js"; router.endpoint({ url: "/data", method: ["POST"], handler: async ({ req, json }) => { // JSON body const jsonBody = await req.json(); // Form fields (text only) const fields = await req.fields(); // Uploaded files const files = await req.files(); // Raw form data (fields and files) const formData = await req.formData(); // Path parameters const params = req.params(); // Query string parameters const searchParams = req.searchParams(); // Headers const headers = req.headers(); // Cookies const cookies = req.cookies(); // Raw body as text const text = await req.text(); // Raw body as bytes const bytes = await req.bytes(); // Underlying Web API Request const webReq = req.webReq; // AbortSignal for cancellation const signal = req.signal; // You can read the req as an AsyncIterable stream for await (const chunk of req) { console.log("CHUNK:", chunk); } return json({ ok: true }); }, }); ``` #### `res` The response builder[​](#res-the-response-builder "Direct link to res-the-response-builder") The `res` object allows building the response using a fluent API: ./samples/handler-res-builder.ts ``` import { router } from "./http-router-initial-config.js"; router.endpoint({ url: "/response", method: ["GET"], handler: async ({ res }) => { return res .setStatus(201) .setHeader("X-Custom", "value") .setBody("Created"); }, }); ``` #### `context`[​](#context "Direct link to context") The `context` object is a shared key-value store that lives for the duration of a single request. It persists across the middleware chain and the final handler, making it ideal for passing data between middleware and handlers: ./samples/handler-context.ts ``` import { contextToken } from "eridu-tech/execution-context/contracts"; import { router } from "./http-router-initial-config.js"; type IUser = { id: string; firstName: string; lastName: string; }; async function loadUser(): Promise { const res = await fetch("/api/user"); return res.json(); } const token = contextToken("USER"); router.use(async ({ context, next }) => { context.put(token, await loadUser()); return await next(); }); router.endpoint({ url: "/profile", method: ["GET"], handler: async ({ context, json }) => { const user = context.getOrFail(token); return json(user); }, }); ``` ### Response helpers[​](#response-helpers "Direct link to Response helpers") Handler arguments include response helper methods for creating common responses. These are destructured directly from the handler args: #### text[​](#text "Direct link to text") ./samples/helper-text.ts ``` import type { HttpHandlerArgs, IHttpRes, } from "eridu-tech/http-router/contracts"; async ({ text }: HttpHandlerArgs): Promise => text("Hello World"); // Content-Type: text/plain ``` #### html[​](#html "Direct link to html") ./samples/helper-html.ts ``` import type { HttpHandlerArgs, IHttpRes, } from "eridu-tech/http-router/contracts"; async ({ html }: HttpHandlerArgs): Promise => html("

Title

"); // Content-Type: text/html ``` #### json[​](#json "Direct link to json") ./samples/helper-json.ts ``` import type { HttpHandlerArgs, IHttpRes, } from "eridu-tech/http-router/contracts"; async ({ json }: HttpHandlerArgs): Promise => json({ message: "success" }); // Content-Type: application/json ``` The `json` helper also accepts an optional Standard Schema for runtime validation: ./samples/helper-json-with-schema.ts ``` import { z } from "zod"; import type { HttpHandlerArgs, IHttpRes, } from "eridu-tech/http-router/contracts"; const responseSchema = z.object({ name: z.string() }); async ({ json }: HttpHandlerArgs): Promise => json({ name: "John" }, responseSchema); ``` #### notFound[​](#notfound "Direct link to notFound") ./samples/helper-not-found.ts ``` import type { HttpHandlerArgs, IHttpRes, } from "eridu-tech/http-router/contracts"; async ({ notFound }: HttpHandlerArgs): Promise => notFound(); // Status: 404, Content-Type: text/html ``` #### redirect[​](#redirect "Direct link to redirect") ./samples/helper-redirect.ts ``` import type { HttpHandlerArgs, IHttpRes, } from "eridu-tech/http-router/contracts"; async ({ redirect }: HttpHandlerArgs): Promise => redirect("/new-location"); // Status: 302, Location: /new-location ``` #### permanentRedirect[​](#permanentredirect "Direct link to permanentRedirect") ./samples/helper-permanent-redirect.ts ``` import type { HttpHandlerArgs, IHttpRes, } from "eridu-tech/http-router/contracts"; async ({ permanentRedirect }: HttpHandlerArgs): Promise => permanentRedirect("/new-permanent"); // Status: 301, Location: /new-permanent ``` ### Cookie management[​](#cookie-management "Direct link to Cookie management") The response builder provides full cookie management through the fluent API. #### Setting cookies[​](#setting-cookies "Direct link to Setting cookies") ./samples/cookie-set.ts ``` import { TimeSpan } from "eridu-tech/time-span"; import type { HttpHandlerArgs, IHttpRes, } from "eridu-tech/http-router/contracts"; async ({ res }: HttpHandlerArgs): Promise => { return res .putCookie("session", "abc123", { httpOnly: true, secure: true, maxAge: TimeSpan.fromHours(1), path: "/", sameSite: "Lax", }) .setBody("Cookie set"); }; ``` Cookie settings include: * `expires` Absolute `Date` or relative `ITimeSpan` * `maxAge` Lifetime in seconds (number or `ITimeSpan`) * `httpOnly` Restrict access to HTTP-only * `secure` Only send over HTTPS * `sameSite` `"Strict"`, `"Lax"` (default), or `"None"` * `domain` The domain scope * `path` The path scope * `priority` `"Low"`, `"Medium"`, or `"High"` * `prefix` `"secure"` (adds `__Secure-`) or `"host"` (adds `__Host-`) * `partitioned` Enable CHIPS partitioned storage #### Removing cookies[​](#removing-cookies "Direct link to Removing cookies") ./samples/cookie-remove.ts ``` import type { HttpHandlerArgs, IHttpRes, } from "eridu-tech/http-router/contracts"; async ({ res }: HttpHandlerArgs): Promise => { return res.removeCookie("session").setBody("Cookie removed"); }; ``` #### Checking if response has set a cookie[​](#checking-if-response-has-set-a-cookie "Direct link to Checking if response has set a cookie") ./samples/cookie-has.ts ``` import type { HttpHandlerArgs, IHttpRes, } from "eridu-tech/http-router/contracts"; async ({ res }: HttpHandlerArgs): Promise => { if (res.hasCookies("session")) { res.removeCookie("session"); } return res.setBody("Checked"); }; ``` #### Stripping cookies from response[​](#stripping-cookies-from-response "Direct link to Stripping cookies from response") You can remove all cookies or a specific cookie from the response: ./samples/cookie-strip.ts ``` import type { HttpHandlerArgs, IHttpRes, } from "eridu-tech/http-router/contracts"; async ({ res }: HttpHandlerArgs): Promise => { return res.withoutCookies().setBody("All cookies stripped"); }; ``` ### Middleware[​](#middleware "Direct link to Middleware") #### Shared middleware[​](#shared-middleware "Direct link to Shared middleware") Use the `use` method to register middleware that applies to **multiple routes** registered on the same router instance: ./samples/shared-middleware.ts ``` import { router } from "./http-router-initial-config.js"; router.use(async ({ req, next }) => { const start = Date.now(); const response = await next(); const duration = Date.now() - start; response.setHeader("X-Response-Time", String(duration)); return response; }); router.endpoint({ url: "/api/data", method: ["GET"], handler: async ({ json }) => json({ data: "test" }), }); ``` #### Endpoint-specific middleware[​](#endpoint-specific-middleware "Direct link to Endpoint-specific middleware") Use the `middlewares` property on an endpoint definition to register middleware that runs **only for that specific endpoint**. This keeps middleware scoped and prevents it from affecting other routes: ./samples/endpoint-middleware.ts ``` import { router } from "./http-router-initial-config.js"; router.endpoint({ url: "/admin", method: ["GET"], handler: async ({ text }) => text("Admin panel"), middlewares: (builder) => builder.use(async ({ req, res, next }) => { const authHeader = req.headers()["authorization"]; if (!authHeader) { return res.setStatus(401).setBody("Unauthorized"); } return await next(); }), }); ``` #### Middleware execution order[​](#middleware-execution-order "Direct link to Middleware execution order") Middleware executes in the following order: 1. **Shared middlewares** (from `router.use()`) registered in order 2. **Endpoint-specific middlewares** (from `endpoint.middlewares`) registered in order 3. **Handler** innermost Each middleware receives a `next` function. Calling `await next()` passes control to the next middleware in the chain. A middleware can short-circuit the chain by returning a response without calling `next()`. ## Patterns[​](#patterns "Direct link to Patterns") ### Handling file uploads[​](#handling-file-uploads "Direct link to Handling file uploads") Uploaded files are accessed through the `files()` method, which returns a record mapping each file field name to an `IHttpFileCollection`: ./samples/file-upload.ts ``` import { router } from "./http-router-initial-config.js"; router.endpoint({ url: "/upload", method: ["POST"], handler: async ({ req, json }) => { const files = await req.files(); // Single file: get the first file, or a 400 if none was uploaded. const avatar = files["avatar"].firstOrFail(); // Multiple files: iterate the collection directly. for (const document of files["documents"]) { console.log(document.name); } // Inspect the file... const content = await avatar.asText(); const name = avatar.name; const type = avatar.contentType; const size = avatar.fileSize; return json({ name, type, sizeInBytes: size.toBytes(), content, }); }, }); ``` An `IHttpFileCollection` handles zero, one, or many files with the same API: | Method | Description | | ------------------ | --------------------------------------------------------------- | | `size()` | Returns the number of files in the collection | | `get(index)` | Returns the file at a 0-based index, or `null` if out of bounds | | `getOrFail(index)` | Returns the file at a 0-based index, throws a 400 if missing | | `first()` | Returns the first file, or `null` if the collection is empty | | `firstOrFail()` | Returns the first file, throws a 400 if the collection is empty | | `isEmpty()` | Returns whether the collection has no files | #### File access methods[​](#file-access-methods "Direct link to File access methods") | Method | Description | | -------------------- | ---------------------------------------------------- | | `asText()` | Reads the file content as a UTF-8 string | | `asBytes()` | Reads the file content as `Uint8Array` | | `asArrayBuffer()` | Reads the file content as `ArrayBuffer` | | `asReadableStream()` | Returns a `ReadableStream` for streaming | | `asFile()` | Returns the underlying Web API `File` object | #### File properties[​](#file-properties "Direct link to File properties") | Property | Type | Description | | -------------- | ---------- | ------------------------------------------- | | `name` | `string` | The original file name | | `contentType` | `string` | The MIME type | | `lastModified` | `Date` | The last modified timestamp | | `fileSize` | `FileSize` | The file size (from `eridu-tech/file-size`) | ### Validating request data[​](#validating-request-data "Direct link to Validating request data") You can enforce runtime and compile-time type safety by passing [Standard Schema](https://standardschema.dev/) schemas directly to the request methods. #### Validating cookies, params, and headers[​](#validating-cookies-params-and-headers "Direct link to Validating cookies, params, and headers") The `cookies()`, `params()`, and `headers()` methods return a record of string values and accept a schema synchronously: ./samples/validate-cookies-params-headers.ts ``` import { z } from "zod"; import { router } from "./http-router-initial-config.js"; const cookiesSchema = z.object({ session: z.string().optional() }); const paramsSchema = z.object({ id: z.string() }); const headersSchema = z.object({ authorization: z.string() }); router.endpoint({ url: "/users/:id", method: ["GET"], handler: async ({ req, json }) => { const cookies = req.cookies(cookiesSchema); const params = req.params(paramsSchema); const headers = req.headers(headersSchema); return json({ userId: params.id, session: cookies.session, auth: headers.authorization, }); }, }); ``` #### Validating search params and fields[​](#validating-search-params-and-fields "Direct link to Validating search params and fields") The `searchParams()` and `fields()` methods return a record where each value can be a single string or an array of strings, and accept a schema for validation: ./samples/validate-search-params-fields.ts ``` import { z } from "zod"; import { router } from "./http-router-initial-config.js"; const searchParamsSchema = z.object({ include: z.string().optional(), tags: z.array(z.string()).optional(), }); const fieldsSchema = z.object({ name: z.string(), age: z.string().pipe(z.coerce.number()), }); router.endpoint({ url: "/signup", method: ["POST"], handler: async ({ req, json }) => { const searchParams = req.searchParams(searchParamsSchema); const fields = await req.fields(fieldsSchema); return json({ include: searchParams.include, tags: searchParams.tags, name: fields.name, age: fields.age, }); }, }); ``` #### Validating the JSON body[​](#validating-the-json-body "Direct link to Validating the JSON body") The `json()` method parses the request body and validates it asynchronously: ./samples/validate-json-body.ts ``` import { z } from "zod"; import { router } from "./http-router-initial-config.js"; const jsonSchema = z.object({ name: z.string(), age: z.number(), }); router.endpoint({ url: "/users", method: ["POST"], handler: async ({ req, json }) => { const body = await req.json(jsonSchema); return json({ name: body.name, age: body.age }); }, }); ``` #### Validating uploaded files[​](#validating-uploaded-files "Direct link to Validating uploaded files") You can define file validation rules by passing a record of file definitions directly to `req.files()`. Each file field accepts a `FileDef`, which is the union of a `StaticFileDef` (rules known ahead of time) and a `DynamicFileDef` (a function that inspects the uploaded files at runtime): ./samples/validate-uploaded-files.ts ``` import { FileSize } from "eridu-tech/file-size"; import { type FileInputs } from "eridu-tech/http-router/contracts"; import { router } from "./http-router-initial-config.js"; const fileInputs = { // Static validation rules known ahead of time. avatar: { contentType: "image/png", fileSize: FileSize.fromMegaBytes(5), name: /\.png$/, max: 1, optional: false, }, // Dynamic validation rules not known ahead of time. // returns an error message string, or `null` when the files pass. docs: (collection) => (collection.size() > 2 ? "Too many documents" : null), } satisfies FileInputs; router.endpoint({ url: "/upload-avatar", method: ["POST"], handler: async ({ req, json }) => { const files = await req.files(fileInputs); const avatarFiles = files.avatar; const file = avatarFiles.firstOrFail(); const content = await file.asBytes(); // Process the avatar... return json({ name: file.name }); }, }); ``` info All validation throw an `HttpError` with status code `400` if constraints are not met. ### Error handling[​](#error-handling "Direct link to Error handling") Errors thrown inside handlers or middleware propagate as a generic `500 Internal Server Error` response. To return structured HTTP errors with proper status codes and messages, use the `HttpError` class: ./samples/http-error-handling.ts ``` import { HttpError } from "eridu-tech/http-router/contracts"; import { router } from "./http-router-initial-config.js"; router.endpoint({ url: "/secure", method: ["GET"], handler: async () => { throw HttpError.create({ status: "403", message: "Forbidden", cause: null, }); }, }); ``` ### Testing[​](#testing "Direct link to Testing") You can test the code by creating a standard web `Request` object and passing it to the `fetch` method of the `HttpRouter` class: ./samples/testing-basic.ts ``` import { HttpRouter, defaultHttpRouterAdapter } from "eridu-tech/http-router"; import { describe, expect, test } from "vitest"; describe("My router", () => { test("should respond to GET /hello", async () => { const router = new HttpRouter({ router: defaultHttpRouterAdapter(), }); router.endpoint({ url: "/hello", method: ["GET"], handler: async ({ text }) => text("Hello World"), }); const request = new Request("https://test.local/hello"); const response = await router.fetch(request); expect(response.status).toBe(200); expect(await response.text()).toBe("Hello World"); }); }); ``` You can also use `HttpReq.test()` to easily create a standard web `Request`: ./samples/testing-http-req-test.ts ``` import { HttpReq } from "eridu-tech/http-router"; const httpReq = HttpReq.test({ method: "POST", url: "/api/data", params: { id: "42" }, searchParams: { include: "profile" }, headers: { authorization: "Bearer token" }, cookies: { session: "abc123" }, body: { type: "application/json", data: { name: "John" }, }, }); ``` #### `TestReqJsonBody`[​](#testreqjsonbody "Direct link to testreqjsonbody") Simulates an `application/json` payload: ./samples/testing-json-body.ts ``` import { HttpRouter, defaultHttpRouterAdapter, HttpReq, } from "eridu-tech/http-router"; import { describe, expect, test } from "vitest"; describe("JSON body", () => { test("should send JSON data", async () => { const router = new HttpRouter({ router: defaultHttpRouterAdapter(), }); router.endpoint({ url: "/users/:id", method: ["POST"], handler: async ({ req, json }) => json({ params: req.params(), body: await req.json(), }), }); const httpReq = HttpReq.test({ method: "POST", url: "/users/:id", hostname: "https://api.example.com", params: { id: "42" }, searchParams: { include: "profile", tags: ["a", "b"] }, headers: { authorization: "Bearer token" }, cookies: { session: "abc123" }, body: { type: "application/json", data: { name: "John" }, }, }); const response = await router.fetch(httpReq.webReq); expect(response.status).toBe(200); }); }); ``` #### `TestReqUrlEncodedBody`[​](#testrequrlencodedbody "Direct link to testrequrlencodedbody") Simulates an `application/x-www-form-urlencoded` form: ./samples/testing-url-encoded-body.ts ``` import { HttpRouter, defaultHttpRouterAdapter, HttpReq, } from "eridu-tech/http-router"; import { describe, expect, test } from "vitest"; describe("URL-encoded body", () => { test("should send form data", async () => { const router = new HttpRouter({ router: defaultHttpRouterAdapter(), }); router.endpoint({ url: "/submit", method: ["POST"], handler: async ({ req, json }) => json(await req.formData()), }); const httpReq = HttpReq.test({ method: "POST", url: "/submit", body: { type: "application/x-www-form-urlencoded", data: { username: "john", role: "admin" }, }, }); const response = await router.fetch(httpReq.webReq); expect(response.status).toBe(200); }); }); ``` #### `TestReqMultipartFormDataBody`[​](#testreqmultipartformdatabody "Direct link to testreqmultipartformdatabody") Simulates a `multipart/form-data` payload with optional text fields and file uploads: ./samples/testing-multipart-body.ts ``` import { HttpRouter, defaultHttpRouterAdapter, HttpReq, } from "eridu-tech/http-router"; import { describe, expect, test } from "vitest"; describe("Multipart body", () => { test("should send multipart form with file uploads", async () => { const router = new HttpRouter({ router: defaultHttpRouterAdapter(), }); router.endpoint({ url: "/upload", method: ["POST"], handler: async ({ req, json }) => { const formData = await req.formData(); const file = formData["avatar"]; const content = file && !Array.isArray(file) && typeof file !== "string" ? await file.asText() : null; return json({ uploaded: !!content }); }, }); const httpReq = HttpReq.test({ method: "POST", url: "/upload", body: { type: "multipart/form-data", data: { fields: { description: "my file" }, files: { avatar: new TextEncoder().encode("file content").buffer, }, }, }, }); const response = await router.fetch(httpReq.webReq); expect(response.status).toBe(200); }); }); ``` #### `TestReqCustom`[​](#testreqcustom "Direct link to testreqcustom") Passes `data` through as-is for arbitrary payloads: ./samples/testing-custom-body.ts ``` import { HttpRouter, defaultHttpRouterAdapter, HttpReq, } from "eridu-tech/http-router"; import { describe, expect, test } from "vitest"; describe("Custom body", () => { test("should send raw data", async () => { const router = new HttpRouter({ router: defaultHttpRouterAdapter(), }); router.endpoint({ url: "/raw", method: ["POST"], handler: async ({ req, text }) => text(await req.text()), }); const httpReq = HttpReq.test({ method: "POST", url: "/raw", body: { type: "custom", data: new Blob(["raw data"]), }, }); const response = await router.fetch(httpReq.webReq); expect(response.status).toBe(200); }); }); ``` ### Using the context for request-scoped data[​](#using-the-context-for-request-scoped-data "Direct link to Using the context for request-scoped data") The shared `context` object is useful for passing data between middleware and handlers: ./samples/request-scoped-context.ts ``` import { contextToken } from "eridu-tech/execution-context/contracts"; import { router } from "./http-router-initial-config.js"; const REQ_ID = contextToken("REQ_ID"); const CURRENT_DATE = contextToken("CURRENT_DATE"); router.use(async ({ context, next }) => { context.put(REQ_ID, crypto.randomUUID()); context.put(CURRENT_DATE, new Date().toISOString()); return await next(); }); router.endpoint({ url: "/track", method: ["GET"], handler: async ({ context, text }) => { const requestId = context.getOrFail(REQ_ID); const startTime = context.getOrFail(CURRENT_DATE); return text(`Request ${String(requestId)} processed`); }, }); ``` ### Using invocable objects as handlers and middleware[​](#using-invocable-objects-as-handlers-and-middleware "Direct link to Using invocable objects as handlers and middleware") Both handlers and middleware can be invocable objects (classes with an `invoke` method), which allows them to encapsulate state. This pattern is designed for seamless integration with dependency injection libraries, as most DI frameworks have first-class support for classes. **Handler example** using `IHttpHandlerObject`: ./samples/invocable-handler.ts ``` import { type IHttpHandlerObject, type HttpHandlerArgs, type IHttpRes, } from "eridu-tech/http-router/contracts"; import { router } from "./http-router-initial-config.js"; class GreetingHandler implements IHttpHandlerObject { constructor(private readonly greeting: string) {} invoke(args: HttpHandlerArgs): IHttpRes { const { text } = args; return text(this.greeting); } } router.endpoint({ url: "/greet", method: ["GET"], handler: new GreetingHandler("Hello from a class handler!"), }); ``` **Middleware example** using `IHttpMiddlewareObject`: ./samples/invocable-middleware.ts ``` import { type IHttpMiddlewareObject, type HttpMiddlewareArgs, type IHttpRes, } from "eridu-tech/http-router/contracts"; import { router } from "./http-router-initial-config.js"; class AuthMiddleware implements IHttpMiddlewareObject { constructor(private readonly apiKey: string) {} async invoke(args: HttpMiddlewareArgs): Promise { const { req, res, next } = args; const authHeader = req.headers()["authorization"]; if (authHeader !== `Bearer ${this.apiKey}`) { return res.setStatus(401).setBody("Unauthorized"); } return await next(); } } router.use(new AuthMiddleware("sk-1234")); ``` info For further information about invocable objects, refer to the [`Invocable`](/docs/utilities/invocable/.md) documentation. ### Interoperability with Winter TC standard web request handlers[​](#interoperability-with-winter-tc-standard-web-request-handlers "Direct link to Interoperability with Winter TC standard web request handlers") A Winter TC handler is a function with the signature `(request: Request) => Promise | Response`. Since `HttpRouter` endpoints expect the richer `HttpHandlerArgs` interface, you can use the `HttpRouter.fromWinterTcHandler()` static method to bridge the two seamlessly: ./samples/winter-tc-handler.ts ``` import { HttpRouter } from "eridu-tech/http-router"; import { router } from "./http-router-initial-config.js"; // A standard Winter TC handler async function healthHandler(request: Request): Promise { if (request.method.toLowerCase() !== "get") { return new Response("Not found", { status: 404 }); } const url = new URL(request.url); if (url.pathname === "/proxy/health") { return new Response("OK", { status: 200 }); } return fetch(request); } // Adapted to work with HttpRouter endpoint via the static method router.endpoint({ url: "/proxy/*", handler: HttpRouter.fromWinterTcHandler(healthHandler), }); ``` The method internally passes `req.webReq` (the underlying Web API `Request`) to the Winter TC handler and converts the returned `Response` into an `IHttpRes` via `fromWebRes()`. --- # DI Integration The `bindHttpFactory` function turns a controller method into an [`HttpHandlerFn`](https://eridu-tech.github.io/eridu-tech-core/types/HttpRouter.HttpHandlerFn.html). This lets you keep HTTP handlers on injectable controller classes and resolve them from a [DI container](/docs/foundation/di/.md) for every request, instead of wiring handler functions to service instances manually. `bindHttpFactory(container)` returns a binder with the signature `(token, method) => HttpHandlerFn`: * `token` — the token of the controller to resolve. * `method` — the name of the handler method to invoke. For each request, the binder: 1. Resolves the controller with [`container.resolveOrFail(token)`](/docs/foundation/di/.md#resolve_or_fail), so the controller follows the lifetime of its registration. 2. Invokes the bound method with the [handler arguments](/docs/web/http_router/http_router_usage/.md#handler-arguments). 3. Throws `UnexpectedError` when the resolved member is not invocable. info Declare handler methods as **arrow function properties** so `this` stays bound to the controller instance when the binder invokes them. ## Initial configuration[​](#initial-configuration "Direct link to Initial configuration") Set up the container, the router, and the binder created with `bindHttpFactory`: ./samples/container.ts ``` import { Container } from "eridu-tech/di"; import { ExecutionContext } from "eridu-tech/execution-context"; import { AlsExecutionContextAdapter } from "eridu-tech/execution-context/als-execution-context-adapter"; import { HttpRouter, defaultHttpRouterAdapter } from "eridu-tech/http-router"; import { bindHttpFactory } from "eridu-tech/http-router/di"; const executionContext = new ExecutionContext(new AlsExecutionContextAdapter()); export const container = new Container({ executionContext }); export const router = new HttpRouter({ router: defaultHttpRouterAdapter() }); export const bindHttp = bindHttpFactory(container); ``` Then define the controller and register it in the container: ./samples/users-controller.ts ``` import type { HttpHandlerFn } from "eridu-tech/http-router/contracts"; import { container } from "./container.js"; export class UsersController { getUser: HttpHandlerFn = ({ req, json }) => { const { id } = req.params(); return json({ id }); }; } container.registerValue({ token: UsersController, value: new UsersController(), }); await container.init(); ``` ## Usage[​](#usage "Direct link to Usage") ### Registering endpoints[​](#registering-endpoints "Direct link to Registering endpoints") Pass `bindHttp(Controller, "methodName")` wherever a handler is accepted: ./samples/bind-http-factory-endpoint.ts ``` import { bindHttp, router } from "./container.js"; import { UsersController } from "./users-controller.js"; router.endpoint({ url: "/users/:id", method: ["GET"], handler: bindHttp(UsersController, "getUser"), }); ``` ### Inside route groups[​](#inside-route-groups "Direct link to Inside route groups") The bound handler is a regular handler, so it can be registered inside route groups: ./samples/bind-http-factory-with-group.ts ``` import { bindHttp, router } from "./container.js"; import { UsersController } from "./users-controller.js"; router.group("/api", (api) => { api.endpoint({ url: "/users/:id", method: ["GET"], handler: bindHttp(UsersController, "getUser"), }); }); ``` ## Further information[​](#further-information "Direct link to Further information") For further information refer to: * [`eridu-tech/http-router`](https://eridu-tech.github.io/eridu-tech-core/modules/HttpRouter.html) API docs. * [DI Container usage](/docs/foundation/di/.md) for the container lifecycle and registration methods. --- # Web Serve HTTP and build framework-agnostic APIs. * [HTTP Router](/docs/web/http_router/http_router_usage/.md) — Framework-agnostic HTTP router built on the Hono router engine, implements the Winter TC fetch standard with middleware chains and typed path parameters. ---