Eridu-tech Documentation

website·Indexed 30 days ago

https://www.eridu-tech.io/

An adapter-first backend foundation tool and modular TypeScript library for building production-ready applications. It provides components for concurrency management (Lock, Semaphore, SharedLock), messaging (EventBus), reliability (CircuitBreaker, RateLimiter), dependency injection, execution context middleware, and transaction management via TransactionContext.

Tokens
85.1K
Snippets
407
Records
447
Agent score
50%

What's inside Eridu-tech

  1. Overview of eridu-tech backend toolkit

    eridu-tech is an adapter-first backend foundation toolkit for TypeScript designed for developing modular monoliths. It provides maintained components for caching, locking, rate limiting, event buses, HTTP routing, and resilience, and is compatible with frameworks such as Next.js, Nuxt, Express, and NestJS.
  2. Adapt Winter TC standard handlers for HttpRouter

    To use a standard Winter TC handler (a function with the signature (request: Request) => Promise<Response> | Response) within an HttpRouter endpoint, use the HttpRouter.fromWinterTcHandler() static method. This method bridges the standard Web API Request and Response objects with the HttpHandlerArgs and IHttpRes interfaces used by the router by passing req.webReq to the handler and converting the resulting Response via fromWebRes().
    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<Response> {
        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),
    });
  3. Configure MongodbSharedLockAdapter

    The MongodbSharedLockAdapter provides a distributed lock mechanism using MongoDB. It requires the mongodb package.

    Key Requirements:

    • You must call .init() once before use to create necessary indexes.
    • Ensure a single, consistent database is used across all server instances/processes.
    • The database setting accepts a TransactionContext to make the adapter transaction-aware.
    import { MongoClient } from "mongodb";
    import { MongodbSharedLockAdapter } from "eridu-tech/shared-lock/mongodb-shared-lock-adapter";
    
    const client = await MongoClient.connect("YOUR_MONGODB_CONNECTION_STRING");
    const database = client.db("database");
    
    const mongodbSharedLockAdapter = new MongodbSharedLockAdapter({
        database,
        collectionName: "my-shared-lock", // Optional: defaults to "shared-lock"
        collectionSettings: {},
    });
    
    await mongodbSharedLockAdapter.init();
    
    // To remove the collection and all data:
    await mongodbSharedLockAdapter.deInit();
  4. Implement time-windowed retries using the retryInterval middleware

    The retryInterval middleware retries a function repeatedly within a specified time window, using a fixed interval between attempts. If the time window expires and all attempts have failed, a RetryIntervalResilienceError is thrown.
    import { retryInterval } from "eridu-tech/resilience";
    import { use } from "eridu-tech/middleware";
    import { TimeSpan } from "eridu-tech/time-span";
    
    async function unstableFn(): Promise<number> {
        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({
            // Total time window to keep retrying
            time: TimeSpan.fromSeconds(10),
            // Fixed delay between each attempt
            interval: TimeSpan.fromMilliseconds(500),
        }),
    ]);
    
    await fn();
  5. Retrieve file content as text or bytes

    Files can be retrieved in different formats using the methods on the file object returned by fileStorage.create(filename).
    import { fileStorage } from "./file-storage-initial-config.js";
    
    // Read as utf8 text
    const text = await fileStorage.create("file.txt").getText();
    
    // Read as Uint8Array
    const bytes = await fileStorage.create("file.txt").getBytes();
  6. Group routes with common prefixes

    Use the group method to organize routes under a common URL prefix. All endpoints defined within the group callback are automatically prefixed. Groups can also be created without a prefix for logical organization.
    import { router } from "./http-router-initial-config.js";
    
    // Group with prefix: /users becomes /api/users
    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 });
            },
        });
    });
    
    // Nested group without prefix
    router.group((sub) => {
        sub.endpoint({
            url: "/nested",
            method: ["GET"],
            handler: async ({ text }) => text("Nested route"),
        });
    });
  7. Use dynamic path parameters and constraints

    The router supports dynamic path segments using the :paramName syntax, which are accessible via req.params(). Parameters can be made optional with a ? suffix. You can also constrain parameters using regular expressions inside curly braces {} immediately following the parameter name.
    import { router } from "./http-router-initial-config.js";
    
    // Basic path parameters
    router.endpoint({
        url: "/users/:id",
        method: ["GET"],
        handler: async ({ req, json }) => {
            const params = req.params();
            return json({ userId: params.id });
        },
    });
    
    // Optional parameter (matches /api/animal and /api/animal/cat)
    router.endpoint({
        url: "/api/animal/:type?",
        method: ["GET"],
        handler: async ({ text }) => text("Animal!"),
    });
    
    // Regex-constrained parameters
    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 });
        },
    });
    
    // Regex with slashes (e.g., matching filenames)
    router.endpoint({
        url: "/posts/:filename{.+\.png}",
        method: ["GET"],
        handler: async ({ req, json }) => {
            const { filename } = req.params();
            return json({ filename });
        },
    });
  8. Configure RedisCacheAdapter

    To use RedisCacheAdapter, install the ioredis package and provide a string serializer (e.g., SuperJsonSerdeAdapter).
    import { RedisCacheAdapter } from "eridu-tech/cache/redis-cache-adapter";
    import { Serde } from "eridu-tech/serde";
    import { SuperJsonSerdeAdapter } from "eridu-tech/serde/super-json-serde-adapter";
    import { Redis } from "ioredis";
    
    const serde = new Serde(new SuperJsonSerdeAdapter());
    const database = new Redis("YOUR_REDIS_CONNECTION_STRING");
    
    const redisCacheAdapter = new RedisCacheAdapter({
        database,
        serde,
    });
  9. Integrate eridu-tech HttpRouter with SolidStart

    SolidStart supports fetch-based route handlers natively. To integrate the eridu-tech HttpRouter, create a catch-all route file and export named functions for each HTTP method (GET, POST, etc.) that pass the request to the router's fetch method.
    npm install eridu-tech hono

    // 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);

  10. Serialize and deserialize semaphores for distributed use

    Semaphores can be serialized to be transmitted over a network (e.g., from a main server to a queue worker) and deserialized for reuse. To enable this, you must pass an object implementing the ISerderRegister contract (such as the Serde class) to the SemaphoreFactory.

    Critical Requirement: You must use the same Serde instances for both serialization and deserialization that were provided to the SemaphoreFactory, as the factory injects custom serialization logic for ISemaphore instances into those specific Serde instances.

    import { Redis } from "ioredis";
    import { RedisSemaphoreAdapter } from "eridu-tech/semaphore/redis-semaphore-adapter";
    import { SemaphoreFactory } from "eridu-tech/semaphore";
    import { Serde } from "eridu-tech/serde";
    import { SuperJsonSerdeAdapter } from "eridu-tech/serde/super-json-serde-adapter";
    
    const serde = new Serde(new SuperJsonSerdeAdapter());
    const redisClient = new Redis("YOUR_REDIS_CONNECTION");
    
    const semaphoreFactory = new SemaphoreFactory({
        serde,
        adapter: new RedisSemaphoreAdapter(redisClient),
    });
    
    const semaphore = semaphoreFactory.create("resource", {
        limit: 2,
    });
    
    // Manual serialization
    const serializedSemaphore = serde.serialize(semaphore);
    const deserializedSemaphore = serde.deserialize(serializedSemaphore);
  11. Write streamed files using FileStorage

    Streamed files are written using addStream, updateStream, or putStream. These methods accept AsyncIterable types (typically AsyncIterable<Uint8Array>).

    It is best practice to provide the fileSize using FileSize.fromBytes() to allow adapters to perform optimizations.

    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 fileStream = createReadStream("./file.txt");
    const { size } = await stat("./file.txt");
    
    const hasAdded = await fileStorage.create("file.txt").addStream({
        data: fileStream,
        fileSize: FileSize.fromBytes(size),
    });