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.
What's inside Eridu-tech
- 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.
HttpRouter compatibility and standards
HttpRouteris compatible with any framework, runtime, or platform adapter that accepts a standard fetch handler because it implements the WinterTC fetch standard, exposing afetch(request: Request): Response | Promise<Response>method.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 anHttpRouterendpoint, use theHttpRouter.fromWinterTcHandler()static method. This method bridges the standard Web APIRequestandResponseobjects with theHttpHandlerArgsandIHttpResinterfaces used by the router by passingreq.webReqto the handler and converting the resultingResponseviafromWebRes().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), });Configure MongodbSharedLockAdapter
The
MongodbSharedLockAdapterprovides a distributed lock mechanism using MongoDB. It requires themongodbpackage.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
databasesetting accepts aTransactionContextto 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();- You must call
Implement time-windowed retries using the retryInterval middleware
TheretryIntervalmiddleware 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, aRetryIntervalResilienceErroris 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();Retrieve file content as text or bytes
Files can be retrieved in different formats using the methods on the file object returned byfileStorage.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();Group routes with common prefixes
Use thegroupmethod 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"), }); });Use dynamic path parameters and constraints
The router supports dynamic path segments using the:paramNamesyntax, which are accessible viareq.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 }); }, });Configure RedisCacheAdapter
To useRedisCacheAdapter, install theioredispackage 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, });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'sfetchmethod.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);
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
ISerderRegistercontract (such as theSerdeclass) to theSemaphoreFactory.Critical Requirement: You must use the same
Serdeinstances for both serialization and deserialization that were provided to theSemaphoreFactory, as the factory injects custom serialization logic forISemaphoreinstances into those specificSerdeinstances.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);Write streamed files using FileStorage
Streamed files are written using
addStream,updateStream, orputStream. These methods acceptAsyncIterabletypes (typicallyAsyncIterable<Uint8Array>).It is best practice to provide the
fileSizeusingFileSize.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), });