--- url: /core-api/application.md description: >- API reference for the StrataService class, the top-level object that manages backends, contexts, and middleware. --- # Application (StrataService) The `StrataService` class is the top-level object for a Strata service. It holds configuration, manages the backend connection, coordinates contexts, runs global middleware, and listens for incoming requests. For conceptual background, see [Architecture](/concepts/architecture). ```typescript import { StrataService } from '@strata-js/strata'; ``` ## Constructor ```typescript new StrataService(config : StrataServiceConfig) ``` Creates a new service instance. The config is the only argument. ```typescript const service = new StrataService({ service: { serviceGroup: 'UserService', concurrency: 32, }, backend: { type: 'redis-streams', redis: { host: 'localhost', port: 6379 }, }, }); ``` See [StrataServiceConfig](#strataserviceconfig) for the full configuration shape. ## Properties | Property | Type | Description | |-----------------------|-------------------------------|--------------------------------------------------------------| | `id` | `string` | Auto-generated unique ID for this service instance. | | `name` | `string` | Application name (from `package.json` or `APP_NAME` env). | | `displayName` | `string` | Human-friendly version of `name` (via lodash `startCase`). | | `serviceGroup` | `string` | The service group this instance belongs to. | | `version` | `string` | Application version (from `package.json` or `APP_VERSION`). | | `concurrency` | `number` | Max concurrent requests this instance will process. | | `initialized` | `boolean` | `true` after `start()` has been called. | | `config` | `StrataServiceConfig` | The config object passed to the constructor. | | `info` | `ServiceInfo` | Snapshot of the service's current state. See below. | | `contexts` | `Record` | Map of registered context names to their operation names. | | `backends` | `string[]` | List of registered backend names. | | `middleware` | `Set` | Set of globally registered middleware. | | `outstandingRequests` | `Map` | Currently in-flight requests. | | `lastBusy` | `{ time: Date, lag: number }` | Last time `node-toobusy` detected event loop lag. | ## Methods ### `start()` ```typescript service.start() : Promise ``` Initializes the backend, enables service discovery (unless disabled in config), and starts listening for incoming requests and commands. Call this **after** registering all contexts and middleware. Throws `AlreadyInitializedError` if called more than once. ```typescript // Register everything first service.registerContext(usersContext); service.useMiddleware(authMiddleware); // Then start await service.start(); ``` ### `registerContext()` Registers a context with the service. Has two overloads: ```typescript // Use the context's own name (set in constructor) service.registerContext(context : StrataContext) : void // Override the name service.registerContext(name : string, context : StrataContext) : void ``` If the context doesn't have a name and none is provided, it throws. If the name is already registered, it throws `AlreadyRegisteredError`. ```typescript import { StrataContext } from '@strata-js/strata'; const users = new StrataContext('users'); users.registerOperation('get', async (request) => { return { user: await db.findUser(request.payload.userId) }; }); // These are equivalent: service.registerContext(users); service.registerContext('users', users); ``` ### `getContext()` ```typescript service.getContext(contextName : string) : StrataContext ``` Retrieves a registered context by name. Throws `UnknownContextError` if not found. ### `useMiddleware()` ```typescript service.useMiddleware(middleware : OperationMiddleware | OperationMiddleware[]) : void ``` Registers global middleware that runs on every request across all contexts. Accepts a single middleware or an array. For details on how middleware ordering works, see [Middleware Model](/concepts/middleware-model). ```typescript service.useMiddleware(new AuthMiddleware()); service.useMiddleware([ new LoggingMiddleware(), new TimingMiddleware() ]); ``` ### `registerBackend()` ```typescript service.registerBackend(name : string, backendClass : BackendConstructor) : void ``` Registers a custom backend constructor under the given name. Once registered, the name can be used as the `type` in the backend config. Throws if the name is already registered. See [Using a Custom Backend](/guides/using-custom-backends) for a full walkthrough. ```typescript import { MyCustomBackend } from './backends/custom.js'; service.registerBackend('custom', MyCustomBackend); ``` ### `getBackend()` ```typescript service.getBackend(name : string) : T ``` Returns the backend instance registered under the given name. ### `setConcurrency()` ```typescript service.setConcurrency(concurrency : number) : void ``` Updates the max concurrent requests at runtime. ### `setTooBusy()` ```typescript service.setTooBusy(options : TooBusyConfig) : void ``` Updates `node-toobusy` options at runtime. See [TooBusyConfig](#toobusyconfig). ### `registerTeardown()` ```typescript service.registerTeardown(fn : () => Promise) : void ``` Registers a cleanup function that will be called during service shutdown. Use this for closing database connections, flushing buffers, or any other teardown logic. ```typescript service.registerTeardown(async () => { await database.disconnect(); }); ``` ## Built-in `service` Context Every `StrataService` automatically registers a `service` context with a single `info` operation. This is useful for health checks, debugging, and service discovery. ### `info` Operation Send a request to `service`/`info` with an empty payload. The response is a `ServiceInfo` object: ```typescript // From a client const response = await client.request('UserService', 'service', 'info', {}); ``` Example response payload: ```json { "id": "l7aNdfrRDZW8nt0FBaSS", "serviceName": "User Service", "serviceGroup": "UserService", "version": "1.2.0", "strataVersion": "2.0.0", "environment": "production", "concurrency": 32, "outstanding": 3, "hostname": "worker-01", "lastBusy": { "time": "2024-06-25T16:16:40.794Z", "lag": 0 }, "contexts": { "service": [ "info" ], "users": [ "get", "create", "delete" ] }, "backend": { "type": "redis-streams" } } ``` ::: info The `info` operation responds from whichever service instance picks up the request. If you need info from a *specific* instance, use the `info` command via `client.command()` instead. ::: ## Configuration Reference ### `StrataServiceConfig` ```typescript interface StrataServiceConfig { service : ServiceConfig; backend : BackendConfig; logging ?: LoggerConfig; client ?: ClientConfig; aliases ?: Record; interruptsToForceShutdown ?: number; shutdownTimeout ?: number; } ``` ### `ServiceConfig` ```typescript interface ServiceConfig { serviceGroup : string; concurrency ?: number; // Default: 32 defaultRequestTimeout ?: number; toobusy ?: TooBusyConfig; } ``` ### `TooBusyConfig` Options for `node-toobusy` dynamic concurrency control: ```typescript interface TooBusyConfig { maxLag ?: number; interval ?: number; smoothingFactorOnRise ?: number; smoothingFactorOnFall ?: number; } ``` ### `BackendConfig` ```typescript interface BackendConfig { type : string; discovery ?: DiscoveryConfig; validateEnvelopes ?: boolean; [key : string] : unknown; } ``` ### `ServiceInfo` The shape returned by the `info` operation and `info` command: ```typescript interface ServiceInfo { id : string; serviceName : string; serviceGroup : string; version : string; strataVersion : string; environment : string; concurrency : number; outstanding : number; hostname : string; lastBusy : { time : Date, lag : number }; contexts : Record; backend : BackendInfo; } ``` --- --- url: /concepts/architecture.md description: >- How Strata organizes services into a hierarchy of services, contexts, and operations with pluggable backends. --- # Architecture Strata organizes services around a simple hierarchy: a **service** contains **contexts**, and each context exposes **operations**. Communication flows through a pluggable **backend**, and scaling happens through **service groups**. ## Service Structure A Strata application has three levels: ### StrataService The top-level object. It holds configuration, manages the backend connection, coordinates contexts, and runs global middleware. You create one per process. ### StrataContext A logical grouping of related operations -- analogous to an Express Router. A `users` context might handle `get`, `create`, and `delete`. Contexts are registered with the service under a name, and incoming requests are routed to the matching context. ### Operations The individual units of work within a context. Each operation is an async function that receives a request and returns a response (or calls `request.succeed()` / `request.fail()` directly). This is where your business logic lives. ```typescript import { StrataService, StrataContext } from '@strata-js/strata'; // Create the service const service = new StrataService({ service: { serviceGroup: 'UserService' }, backend: { type: 'redis', redis: { host: 'localhost', port: 6379 } }, }); // Create a context const users = new StrataContext('users'); // Register operations users.registerOperation('get', async (request) => { const { userId } = request.payload; const user = await db.findUser(userId); return { user }; }); users.registerOperation('create', async (request) => { const user = await db.createUser(request.payload); return { user }; }); // Register the context with the service service.registerContext(users); // Start listening for requests await service.start(); ``` That's it. The service listens on its queue, routes incoming requests to the `users` context, and dispatches to the matching operation. ## Pluggable Backends The backend is the transport layer. It handles sending and receiving messages, managing queues, and coordinating service discovery. Critically, your service code never touches the backend directly -- it's configured once and forgotten. ### Built-in Backends | Backend | Transport | Use Case | |------------------|-----------------|-------------------------------------| | `redis` | Redis lists | Simple, reliable, well-understood | | `redis-streams` | Redis Streams | Most tested, consumer group support | | `null` | Nothing | Unit testing | ### Swapping Backends Changing the backend is a configuration change. Your contexts, operations, and middleware don't change at all: ```typescript // Redis lists const service = new StrataService({ service: { serviceGroup: 'MyService' }, backend: { type: 'redis', redis: { host: 'localhost' } }, }); // Redis Streams -- same service code, different config const service = new StrataService({ service: { serviceGroup: 'MyService' }, backend: { type: 'redis-streams', redis: { host: 'localhost' } }, }); ``` ### Custom Backends You can implement your own backend by extending `BaseStrataBackend` and registering it with the service. As long as your backend can send and receive JSON messages, it works. See the [Backends](/core-api/backends) reference for built-in options, or the [Writing a Custom Backend](/guides/writing-custom-backends) guide. ## Client / Service Model Strata has two sides: **services** that process requests and **clients** that send them. * A `StrataService` listens on a queue for incoming requests, routes them through middleware and contexts, and sends responses back. * A `StrataClient` sends requests to a service group's queue and listens for responses on its own dedicated queue. A service can also have an embedded client for making calls to other service groups. This is how services communicate with each other -- service A's client sends a request to service B's queue. ```typescript import { StrataClient } from '@strata-js/strata'; const client = new StrataClient({ client: { name: 'MyApp' }, backend: { type: 'redis', redis: { host: 'localhost' } }, }); await client.start(); // Send a request to the UserService const response = await client.request('UserService', 'users', 'get', { userId: '12345', }); ``` ## Service Groups A **service group** is one or more instances of the same service, all reading from the same request queue. This is how Strata handles horizontal scaling. When you start three instances of `UserService`, they all listen on the `Requests:UserService` queue. Each incoming request goes to exactly one instance -- whichever pops it first. No load balancer, no coordinator, no configuration. ``` /-> UserService (instance 1) Client -> RPUSH Requests:UserService ----------> UserService (instance 2) \-> UserService (instance 3) ``` Each instance has its own unique ID but shares the service group name. The backend handles the distribution transparently. To scale up, start another instance. To scale down, stop one. The queue absorbs the difference. ## Layered Architecture (Optional) Strata itself doesn't enforce how you organize code inside your operations. However, the framework was designed with [iDesign](https://www.idesign.net/) principles in mind, and the recommended architecture follows a layered pattern: | Layer | Responsibility | Example | |-------------------|------------------------------------------|------------------------------| | **Clients** | External consumers (contexts/operations) | Route requests to managers | | **Managers** | Business logic orchestration | Coordinate engines and RA | | **Engines** | Pure business logic, no I/O | Validation, transformation | | **Resource Access**| Data persistence, external APIs | Database queries, HTTP calls | | **Utils** | Cross-cutting concerns | Logging, config, helpers | Each layer only calls the layer directly below it. Managers coordinate between engines and resource access. Engines are pure functions -- no I/O, no state, easily testable. This isn't required, but it works well with Strata's structure. --- --- url: /core-api/backends.md description: >- Built-in backend implementations (Redis Lists, Redis Streams, In-Memory) and how to configure them. --- # Backends Strata uses a pluggable backend system so the same service code can run on different message queue implementations. All services that need to communicate with each other must share the same backend, but you can have multiple service or client instances connected to different backends in the same process. Strata ships with three built-in backends and supports [using](/guides/using-custom-backends) or [writing](/guides/writing-custom-backends) custom ones. ## Built-in Backends ### Redis Lists (`redis`) The default and recommended backend. Uses [Redis lists](https://redis.io/docs/data-types/lists/) for queuing, Redis pub/sub for commands, and Redis sets for service discovery. **How it works:** * **Sending a request:** `RPUSH Requests: "{ ... }"` * **Listening for requests:** `BLPOP Requests: ` * **Sending a response:** `RPUSH "{ ... }"` * **Listening for responses:** `BLPOP ` Simple, reliable, and well-understood. Messages are delivered to exactly one consumer (whichever pops first), giving you natural load balancing across service instances. **Configuration:** ```typescript const service = new StrataService({ service: { serviceGroup: 'UserService' }, backend: { type: 'redis', redis: { host: 'localhost', port: 6379, }, }, }); ``` **When to use:** This is the default for most deployments. Simple, low overhead, and covers the vast majority of use cases. ### Redis Streams (`redis-streams`) An alternative backend that uses [Redis Streams](https://redis.io/docs/data-types/streams/) instead of lists. **How it works:** * **Sending a request:** `XADD Requests: * msg "{ ... }"` * **Listening for requests:** `XREADGROUP GROUP COUNT BLOCK 0 STREAMS Requests: >` * **Sending a response:** Pipelines `XADD` (the response) and `XACK` (the request) as a single transaction. * **Listening for responses:** `XREAD BLOCK 0 STREAMS Responses::` Redis Streams provide consumer groups, message acknowledgment, and crash recovery semantics. However, Strata does not currently expose these capabilities at the framework level, so in practice the streams backend adds overhead without additional benefit over the lists backend. **Configuration:** ```typescript const service = new StrataService({ service: { serviceGroup: 'UserService' }, backend: { type: 'redis-streams', redis: { host: 'localhost', port: 6379, db: 0, }, }, }); ``` **When to use:** If you have a specific reason to use Redis Streams (existing infrastructure, future plans for stream-level features). Otherwise, prefer the Redis lists backend. ### Null (`null`) A backend that does nothing. It accepts all method calls but doesn't send or receive any messages. Intended for testing. **Configuration:** ```typescript const service = new StrataService({ service: { serviceGroup: 'TestService' }, backend: { type: 'null' }, }); ``` **When to use:** Unit tests where you want to test your operation handlers and middleware without needing a running Redis instance. The null backend lets you instantiate a full `StrataService` with no external dependencies. ## Common Backend Configuration All backends share a common set of configuration options: ```typescript interface BackendConfig { type : string; // Which backend to use discovery ?: DiscoveryConfig; // Service discovery settings validateEnvelopes ?: boolean; // Envelope validation (default: true) } ``` ### Envelope Validation All backends include built-in envelope validation that checks message structure before processing. Enabled by default with near-zero performance overhead (0-2%). ```typescript backend: { type: 'redis', validateEnvelopes: true, // Default -- leave it on } ``` See [Envelope Validation](/guides/envelope-validation) for details on what gets validated and how errors are handled. ### Service Discovery The Redis-based backends support service discovery. When enabled, each service instance periodically registers itself, and clients can query for available services. ```typescript backend: { type: 'redis', discovery: { enabled: true, interval: 30, // Registration interval in seconds ttl: 90, // Key expiration in seconds db: 0, // Redis DB for discovery data }, } ``` ### Queue Naming Both Redis backends use the convention `Requests:` for request queues. In environments where multiple developers share the same Redis instance, append the hostname to the service group in your config: ```yaml service: serviceGroup: "UserService.$HOSTNAME" ``` This gives each developer an isolated queue. ## Custom Backends Need a different transport? You can plug in any messaging system -- AMQP, Kafka, gRPC, in-memory, whatever fits your infrastructure. * [Using a Custom Backend](/guides/using-custom-backends) -- registering and configuring a third-party backend. * [Writing a Custom Backend](/guides/writing-custom-backends) -- building your own from scratch. --- --- url: /middleware/cache.md description: >- Response caching middleware with memory, Redis, and custom store support for skipping repeated operation handler calls. --- # Cache Middleware Response caching middleware for Strata services. Caches successful operation responses and serves them on subsequent requests with matching payloads, skipping the operation handler entirely. Supports three built-in cache stores (memory, Redis, null) and custom stores via the `ICacheStore` interface. ## Installation ```bash npm install @strata-js/middleware-cache ``` Peer dependency: `@strata-js/strata >= 1.4.0 || ^2.0.0` ## Usage Create a `CacheMiddleware` instance with a configuration object, then register it on an operation (or context/service): ```typescript import { StrataContext } from '@strata-js/strata'; import { CacheMiddleware } from '@strata-js/middleware-cache'; const ctx = new StrataContext('products'); const cache = new CacheMiddleware({ kind: 'memory', key: 'products' }); ctx.registerOperation('getProduct', async (request) => { return { product: await db.findProduct(request.payload.productId) }; }, [], [ cache ]); ``` On the first request, the handler runs and the response is stored. On subsequent requests with an identical payload, the cached response is returned immediately without calling the handler. ## Configuration Pass a configuration object to the `CacheMiddleware` constructor. The `kind` field determines which store is used. ### Memory Cache Uses [node-cache](https://www.npmjs.com/package/node-cache) for in-process caching. Good for development and single-instance services. ```typescript const cache = new CacheMiddleware({ kind: 'memory', key: 'my-service', options: { stdTTL: 3600, // TTL in seconds (default: 86400 = 24 hours) checkperiod: 120, // Cleanup interval in seconds }, }); ``` | Option | Type | Default | Description | |--------|------|---------|-------------| | `kind` | `'memory'` | -- | Selects the in-memory store. | | `key` | `string` | `'cache'` | Prefix for cache keys. | | `options` | `NodeCacheOptions` | `{ stdTTL: 86400 }` | Passed directly to `node-cache`. See [node-cache options](https://www.npmjs.com/package/node-cache#options). | ### Redis Cache Uses [ioredis](https://www.npmjs.com/package/ioredis) for distributed caching across multiple service instances. ```typescript const cache = new CacheMiddleware({ kind: 'redis', key: 'my-service', options: { host: 'localhost', port: 6379, db: 1, }, ttl: 60 * 60 * 24, // TTL in seconds }); ``` | Option | Type | Default | Description | |--------|------|---------|-------------| | `kind` | `'redis'` | -- | Selects the Redis store. | | `key` | `string` | `'cache'` | Prefix for cache keys. | | `options` | `RedisOptions` | -- | Passed directly to `ioredis`. See [ioredis options](https://github.com/redis/ioredis#connect-to-redis). | | `ttl` | `number` | -- | Time-to-live in seconds. If omitted, entries never expire. | The Redis store includes automatic reconnection with exponential backoff and keepalive (3-minute idle delay). ### Null Cache (Disabled) Disables caching entirely. Useful for turning off caching via configuration without changing code. ```typescript const cache = new CacheMiddleware({ kind: 'none' }); ``` | Option | Type | Description | |--------|------|-------------| | `kind` | `'none'` | Selects the null store (all gets return `null`, all sets are no-ops). | ## Custom Cache Stores Implement the `ICacheStore` interface to use any backing store: ```typescript import { CacheMiddleware } from '@strata-js/middleware-cache'; import type { ICacheStore } from '@strata-js/middleware-cache'; class DynamoStore implements ICacheStore { async get(key : string) : Promise { const item = await dynamo.getItem({ Key: { pk: key } }); return item?.value ?? null; } async set(key : string, payload : unknown) : Promise { await dynamo.putItem({ Key: { pk: key }, value: payload }); return key; } // Optional -- called during service shutdown async teardown() : Promise { await dynamo.close(); } } const cache = new CacheMiddleware(new DynamoStore(), { kind: 'custom', key: 'my-service' }); ``` ### `ICacheStore` Interface ```typescript interface ICacheStore { get(key : string) : Promise; set(key : string, payload : unknown) : Promise; teardown ?: () => Promise; } ``` | Method | Description | |--------|-------------| | `get(key)` | Retrieve a cached value by key. Return `null` or `undefined` for a cache miss. | | `set(key, payload)` | Store a value under the given key. | | `teardown()` | Optional. Called when the service shuts down -- close connections, flush buffers, etc. | When using a custom store, pass the store instance as the first argument and a `{ kind: 'custom' }` configuration as the second. ## How Caching Works ### Cache Key Generation Cache keys are built from four components, joined by `:`: ``` ::: ``` For example, a request to `products/getProduct` with payload `{ productId: '42' }` and key `'my-service'` produces: ``` my-service:products:getProduct:{"productId":"42"} ``` Payload keys are sorted alphabetically before serialization, so `{ b: 2, a: 1 }` and `{ a: 1, b: 2 }` produce identical cache keys. ### Request Lifecycle 1. **`beforeRequest`** -- Checks the cache for a matching key. On a hit, calls `request.succeed()` with the cached value (short-circuiting the handler). Sets `request.cacheHit` to `true` or `false`. 2. **`success`** -- If the request was **not** a cache hit, stores the response in the cache. 3. **`failure`** -- No-op. Failed responses are never cached. 4. **`teardown`** -- Calls `store.teardown()` if the store implements it (e.g., disconnects Redis). ## Examples ### Operation-Level Caching The most common pattern -- cache responses for a single read operation: ```typescript import { StrataContext } from '@strata-js/strata'; import { CacheMiddleware } from '@strata-js/middleware-cache'; const products = new StrataContext('products'); const cache = new CacheMiddleware({ kind: 'redis', key: 'product-service', options: { host: 'redis.internal', port: 6379 }, ttl: 300, // 5 minutes }); products.registerOperation('getProduct', async (request) => { return { product: await db.findProduct(request.payload.productId) }; }, [], [ cache ]); ``` ### Switching Stores by Environment Use the `kind` field to swap stores without changing any other code: ```typescript import { CacheMiddleware } from '@strata-js/middleware-cache'; import type { CacheMiddlewareConfiguration } from '@strata-js/middleware-cache'; function buildCacheConfig() : CacheMiddlewareConfiguration { if(process.env.NODE_ENV === 'production') { return { kind: 'redis', key: 'my-service', options: { host: process.env.REDIS_HOST!, port: 6379 }, ttl: 3600, }; } if(process.env.DISABLE_CACHE === 'true') { return { kind: 'none' }; } return { kind: 'memory', key: 'my-service' }; } const cache = new CacheMiddleware(buildCacheConfig()); ``` ### Context-Level Caching Cache every operation in a context: ```typescript const lookups = new StrataContext('lookups'); const cache = new CacheMiddleware({ kind: 'memory', key: 'lookups' }); lookups.useMiddleware(cache); lookups.registerOperation('getStates', async () => fetchStates()); lookups.registerOperation('getCountries', async () => fetchCountries()); ``` --- --- url: /core-api/client.md description: >- API reference for the StrataClient class, which sends requests, posts, and service commands to Strata services. --- # Client (StrataClient) The `StrataClient` class sends requests to services and receives responses. It also supports fire-and-forget posts, service commands, and service discovery. For conceptual background, see [Architecture](/concepts/architecture). ```typescript import { StrataClient } from '@strata-js/strata'; ``` ## Constructor ```typescript new StrataClient(config : StrataClientConfig) ``` Creates a new client instance. ```typescript const client = new StrataClient({ client: { name: 'MyApp' }, backend: { type: 'redis-streams', redis: { host: 'localhost', port: 6379 }, }, }); ``` The client name defaults to `Client:` if `service.serviceGroup` is set in config, or `StrataClient.` otherwise. ## Properties | Property | Type | Description | |-----------------------|------------------------------|----------------------------------------------------------------| | `id` | `string` | Auto-generated unique ID (or override via `config.client.id`). | | `name` | `string` | Client name for identification on the wire. | | `version` | `string` | Application version (from `package.json` or `APP_VERSION`). | | `config` | `StrataClientConfig` | The config object passed to the constructor. | | `initialized` | `boolean` | `true` after `start()` has been called. | | `outstandingRequests` | `Map` | Currently in-flight requests waiting for responses. | ## Methods ### `start()` ```typescript client.start() : Promise ``` Initializes the backend and starts listening for responses. You **must** call this before making any requests or sending commands. Throws `AlreadyInitializedError` if called more than once. ```typescript const client = new StrataClient(config); await client.start(); ``` ### `request()` ```typescript client.request( queueOrService : string, context : string, operation : string, payload : Record, metadata ?: MetadataType, auth ?: string, timeout ?: number ) : Promise> ``` Sends a request and waits for a response. This is the primary way to call operations on a service. **Parameters:** | Parameter | Type | Required | Description | |------------------|---------------------------|----------|-----------------------------------------------------------------------| | `queueOrService` | `string` | yes | Service group name or queue name. The `Requests:` prefix is added automatically if missing. | | `context` | `string` | yes | Target context name. | | `operation` | `string` | yes | Target operation name. | | `payload` | `Record` | yes | Request payload. | | `metadata` | `Record` | no | Request metadata (tracking, stats, etc.). | | `auth` | `string` | no | Authentication token (typically a JWT). | | `timeout` | `number` | no | Request timeout in milliseconds. `0` or omitted uses the service default. | **Returns:** `Promise>` -- the full response envelope. ```typescript const response = await client.request('UserService', 'users', 'get', { userId: '12345', }); console.log(response.status); // 'succeeded' console.log(response.payload); // { user: { ... } } ``` Throws `UninitializedError` if the client hasn't been started. ### `post()` ```typescript client.post( queueOrService : string, context : string, operation : string, payload : Record, metadata ?: MetadataType, auth ?: string ) : Promise ``` Sends a fire-and-forget message. The service processes it but sends no response. Useful for events, notifications, or any case where the sender doesn't need confirmation. ```typescript await client.post('NotificationService', 'email', 'send', { to: 'user@example.com', subject: 'Welcome!', body: 'Thanks for signing up.', }); ``` Throws `UninitializedError` if the client hasn't been started. ### `command()` ```typescript client.command( command : ValidCommandName, target ?: string, payload ?: Record ) : Promise ``` Sends a command to one or more running services. Commands control service behavior at runtime (shutdown, concurrency changes, etc.). **Parameters:** | Parameter | Type | Default | Description | |-----------|--------------------|--------------|-----------------------------------------------------------------------------| | `command` | `ValidCommandName` | -- | One of `'info'`, `'concurrency'`, `'shutdown'`, `'toobusy'`. | | `target` | `string` | `'Services'` | Target scope for the command. | | `payload` | `object` | -- | Command-specific payload. | **Target format:** | Target | Scope | |------------------------------------|--------------------------------------------| | `'Services'` | All running services (all groups). | | `'Services:'` | All instances in a specific service group. | | `'Services::'` | A specific service instance by ID. | **Returns:** A `ServiceCommandResponse` for commands that have replies (currently only `info`), or `undefined`. ```typescript // Get info from all services const info = await client.command('info'); // Shut down a specific service group await client.command('shutdown', 'Services:UserService', { graceful: true, }); // Update concurrency for all services await client.command('concurrency', 'Services', { concurrency: 64, }); ``` #### Built-in Commands | Command | Payload | Has Response | Description | |----------------|--------------------------------|:------------:|----------------------------------------------| | `info` | -- | yes | Returns `ServiceInfo` for each instance. | | `shutdown` | `{ graceful?, exitCode? }` | no | Initiates service shutdown. | | `concurrency` | `{ concurrency }` | no | Updates max concurrent requests. | | `toobusy` | `TooBusyConfig` | no | Updates `node-toobusy` options. | ### `discoverServices()` ```typescript client.discoverServices() : Promise ``` Returns all discovered service groups, their instances, and what contexts/operations each supports. The return type is a nested record: service group name -> service instance ID -> `DiscoveredService`. ```typescript const services = await client.discoverServices(); for(const [ group, instances ] of Object.entries(services)) { console.log(`Service group: ${ group }`); for(const [ id, info ] of Object.entries(instances)) { console.log(` Instance ${ id }: v${ info.version }, ${ info.outstanding } outstanding`); console.log(` Contexts:`, Object.keys(info.contexts)); } } ``` ::: info Service discovery requires the backend to support it and discovery to be enabled in the service config (enabled by default). ::: ### `registerBackend()` ```typescript client.registerBackend(name : string, backendClass : BackendConstructor) : void ``` Registers a custom backend constructor, just like `service.registerBackend()`. Use this when the client needs a backend that isn't built in. ### `getBackend()` ```typescript client.getBackend(name : string) : T ``` Returns the backend instance registered under the given name. ## Configuration Reference ### `StrataClientConfig` ```typescript type StrataClientConfig = Omit & { service ?: Partial; }; ``` In practice, a client config looks like: ```typescript { client: { name: 'MyApp', // Optional. Defaults based on serviceGroup or hostname. id: 'custom-id', // Optional. Auto-generated if omitted. }, backend: { type: 'redis-streams', redis: { host: 'localhost', port: 6379 }, }, aliases: { // Optional. Map friendly names to queue names. 'users': 'UserService', }, } ``` ### `ClientConfig` ```typescript interface ClientConfig { name ?: string; id ?: string; } ``` --- --- url: /utilities/config.md description: >- Configuration loader utility supporting YAML/JSON files, environment variable substitution, and file includes. --- # Config `@strata-js/util-config` is a configuration loading utility that supports YAML and JSON files with environment variable substitution and file includes. It stores named config objects and retrieves them with full TypeScript generics support. ## Installation ```bash npm install @strata-js/util-config ``` ## Usage ```typescript import configUtil from '@strata-js/util-config'; interface AppConfig { server : { host : string; port : number; }; database : { connectionString : string; }; } // Load a config file (typically at startup) configUtil.load('./config.yml'); // Retrieve the parsed config anywhere in your application const config = configUtil.get(); console.log(config.server.port); // 8080 ``` The default export is a singleton instance of `ConfigUtil`. You can also import the class directly if you need multiple independent instances: ```typescript import { ConfigUtil } from '@strata-js/util-config'; const myConfig = new ConfigUtil(); myConfig.load('./my-config.yml'); ``` ## API ### `load(filePath, name?, options?)` ```typescript configUtil.load(filePath : string, name ?: string, options ?: ConfigLoaderOptions) : void ``` Reads and parses a configuration file, storing the result under `name`. | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `filePath` | `string` | — | Path to the config file. If not provided, uses the `CONFIG_FILE` environment variable. | | `name` | `string` | `'default'` | Name to store the config under. Allows loading multiple config files. | | `options` | `ConfigLoaderOptions` | See below | Options controlling parsing behavior. | Supported file formats are determined by extension: | Extension | Format | |-----------|--------| | `.yml`, `.yaml` | YAML | | `.json` | JSON | ### `ConfigLoaderOptions` | Option | Type | Default | Description | |--------|------|---------|-------------| | `substituteEnvironmentVariables` | `boolean` | `true` | Replace environment variable references in the file content before parsing. | | `mergeIncludes` | `boolean` | `true` | Process `include` directives and merge included files into the config. | ### `get(name?)` ```typescript configUtil.get(name ?: string) : T ``` Returns the config stored under `name`. Throws an error if no config has been loaded under that name. | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `name` | `string` | `'default'` | The name the config was stored under during `load()` or `set()`. | ### `set(config, name?)` ```typescript configUtil.set>(config : T, name ?: string) : void ``` Stores a config object directly, bypassing file loading. Useful for testing or when config comes from an external source. | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `config` | `T` | — | The config object to store. | | `name` | `string` | `'default'` | The name to store the config under. | ### `delete(name?)` ```typescript configUtil.delete(name ?: string) : void ``` Removes the config stored under `name`. | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `name` | `string` | `'default'` | The name of the config to remove. | ### `clear()` ```typescript configUtil.clear() : void ``` Removes all stored configs. ## Environment Variable Substitution When `substituteEnvironmentVariables` is enabled (the default), the file content is scanned for environment variable references before parsing. Three syntaxes are supported: | Syntax | Example | |--------|---------| | `$VAR` | `$HOSTNAME` | | `${VAR}` | `${REDIS_HOST}` | | `{{VAR}}` | `{{REDIS_PASSWORD}}` | If a referenced variable is not set, the reference is left as-is in the string. ```yaml # config.yml server: host: "$HOSTNAME" port: 8080 database: connectionString: "postgres://${DB_USER}:${DB_PASS}@${DB_HOST}/mydb" ``` ## File Includes Config files can include other files using the `include` key. Included files are loaded first, then the base file's values are merged on top (base values win). Single include: ```yaml # config/local.yml include: "base.yml" service: serviceGroup: "MyService" logging: level: debug prettyPrint: true ``` Multiple includes: ```yaml include: - "base.yml" - "logging-defaults.yml" service: serviceGroup: "MyService" ``` Include paths are resolved relative to the file that contains the `include` directive. Includes can be nested up to 10 levels deep by default. Set the `INCLUDE_DEPTH` environment variable to change this limit. ## Examples ### Named Configs Load separate configs for different parts of your application: ```typescript import configUtil from '@strata-js/util-config'; configUtil.load('./config/service.yml', 'service'); configUtil.load('./config/database.yml', 'database'); const serviceConfig = configUtil.get('service'); const dbConfig = configUtil.get('database'); ``` ### Environment-based Config Files A common pattern is to load different files based on the current environment: ```typescript import configUtil from '@strata-js/util-config'; const env = process.env.ENVIRONMENT ?? 'local'; configUtil.load(`./config/${ env }.yml`); const config = configUtil.get(); ``` ### Testing Use `set()` to inject config directly in tests without touching the filesystem: ```typescript import configUtil from '@strata-js/util-config'; beforeEach(() => { configUtil.set({ server: { host: 'localhost', port: 0 }, database: { connectionString: 'sqlite::memory:' }, }); }); afterEach(() => { configUtil.clear(); }); ``` ### Base Config with Overrides Use file includes to share common configuration across environments: ```yaml # config/base.yml backend: type: "redis-streams" redis: host: "localhost" port: 6379 discovery: enabled: true ``` ```yaml # config/production.yml include: "base.yml" backend: redis: host: "$REDIS_HOST" password: "$REDIS_PASSWORD" logging: level: info prettyPrint: false ``` --- --- url: /getting-started/configuration.md description: >- How to configure StrataService and StrataClient using config objects, environment variables, or the config utility. --- # Configuration Strata v2 removed the built-in configuration system. Instead, the `StrataService` and `StrataClient` constructors accept plain config objects. Where you get that object is up to you -- hardcode it, load it from a file, pull it from environment variables, or use the `@strata-js/util-config` utility. ## Config Interfaces All configuration flows through two TypeScript interfaces: `StrataServiceConfig` for services and `StrataClientConfig` for clients. Both are built from the same base `StrataConfig` type. ### StrataServiceConfig Used when constructing a `StrataService`. Requires `service` and `backend`. ```typescript import type { StrataServiceConfig } from '@strata-js/strata'; const config : StrataServiceConfig = { // Required: identifies this service group service: { serviceGroup: 'MyService', }, // Required: which backend to use backend: { type: 'redis', redis: { host: 'localhost', port: 6379 }, }, }; ``` Full interface: | Property | Type | Required | Default | Description | |----------|------|----------|---------|-------------| | `service.serviceGroup` | `string` | Yes | — | Name shared by all instances of this service. Determines which queue the service reads from. | | `service.concurrency` | `number` | No | `32` | Maximum number of requests processed simultaneously. | | `service.defaultRequestTimeout` | `number` | No | — | Default timeout (ms) for requests. | | `service.toobusy` | `TooBusyConfig` | No | — | Dynamic concurrency control via [node-toobusy](https://github.com/STRML/node-toobusy). | | `backend` | `BackendConfig` | Yes | — | Backend configuration. See [Backend Configuration](#backend-configuration). | | `client` | `ClientConfig` | No | — | Optional embedded client config. | | `logging` | `LoggerConfig` | No | — | Logging config (`level`, `prettyPrint`). | | `aliases` | `Record` | No | — | Friendly names that map to service group names. | | `shutdownTimeout` | `number` | No | — | How long (ms) to wait for graceful shutdown. | | `interruptsToForceShutdown` | `number` | No | — | Number of SIGINT signals before forcing shutdown. | #### TooBusyConfig Controls the [node-toobusy](https://github.com/STRML/node-toobusy) event loop lag detection: | Property | Type | Default | Description | |----------|------|---------|-------------| | `maxLag` | `number` | `70` | Maximum event loop lag (ms) before refusing work. | | `interval` | `number` | `500` | How often (ms) to check event loop lag. | | `smoothingFactorOnRise` | `number` | `1/3` | Smoothing factor when lag is increasing. | | `smoothingFactorOnFall` | `number` | `1/3` | Smoothing factor when lag is decreasing. | ### StrataClientConfig Used when constructing a `StrataClient`. The `service` property is optional (and all its fields are optional if present). `backend` is required. ```typescript import type { StrataClientConfig } from '@strata-js/strata'; const config : StrataClientConfig = { // Optional: names this client client: { name: 'MyClient', }, // Required: which backend to use backend: { type: 'redis', redis: { host: 'localhost', port: 6379 }, }, }; ``` Full interface: | Property | Type | Required | Default | Description | |----------|------|----------|---------|-------------| | `client.name` | `string` | No | `StrataClient.` | Name for this client. Used to identify the client in logs and the response queue. | | `client.id` | `string` | No | auto-generated | Unique client ID. Usually you let Strata generate this. | | `backend` | `BackendConfig` | Yes | — | Backend configuration. Same as service. | | `service` | `Partial` | No | — | Optional. If present, `service.serviceGroup` is used to derive a default client name. | | `logging` | `LoggerConfig` | No | — | Logging config. | | `aliases` | `Record` | No | — | Friendly names that map to service group names. | | `shutdownTimeout` | `number` | No | — | Graceful shutdown timeout. | | `interruptsToForceShutdown` | `number` | No | — | Interrupts before force shutdown. | ::: tip Shared Config `StrataServiceConfig` is a superset of `StrataClientConfig`. If your service needs an embedded client, you can pass the same config object to both constructors. The service uses `service.serviceGroup`, the client uses `client.name`. ::: ## Backend Configuration The `backend` object tells Strata which transport to use. The `type` field selects the backend; all other fields are backend-specific. For detailed descriptions of each built-in backend (Redis Streams, Redis Lists, Null), see the [Backends](/core-api/backends) reference. ### Backend Config Reference | Property | Type | Default | Description | |----------|------|---------|-------------| | `type` | `string` | — | Backend name: `'redis'`, `'redis-streams'`, `'null'`, or a custom registered name. | | `redis` | `RedisOptions` | — | [ioredis](https://github.com/redis/ioredis) connection options. Required for Redis backends. | | `validateEnvelopes` | `boolean` | `true` | Validate message envelope structure at runtime. See [Envelope Validation](/guides/envelope-validation). | | `isolateCommands` | `boolean` | `false` | Use a separate Redis connection for commands (Redis Streams only). | | `discovery.enabled` | `boolean` | `true` | Enable service discovery. | | `discovery.interval` | `number` | `30` | How often (seconds) to refresh the discovery key (Redis only). | | `discovery.ttl` | `number` | `90` | TTL (seconds) for the discovery key (Redis only). | | `discovery.db` | `number` | `0` | Redis DB number for discovery data (Redis only). | ## YAML Config Files with `@strata-js/util-config` The recommended way to manage configuration is with `@strata-js/util-config`, a simple file loader that supports YAML, JSON, and JSON5. ```bash npm install @strata-js/util-config ``` ### Loading a Config File ```typescript import configUtil from '@strata-js/util-config'; // Load config from a YAML file configUtil.load('./config/local.yml'); // Retrieve the parsed config object const config = configUtil.get(); ``` `load()` reads the file, performs environment variable substitution, and stores the result under a name (defaults to `'default'`). `get()` retrieves it. You can store multiple configs under different names: ```typescript configUtil.load('./config/service.yml', 'service'); configUtil.load('./config/client.yml', 'client'); const serviceConfig = configUtil.get('service'); const clientConfig = configUtil.get('client'); ``` ### Complete YAML Example ```yaml # config/local.yml # Logging logging: level: debug prettyPrint: true # Service service: serviceGroup: "MyService.$HOSTNAME" concurrency: 16 # Client (optional, for embedded clients) client: name: "MyServiceClient" # Aliases aliases: orders: "OrderService.$HOSTNAME" users: "UserService.$HOSTNAME" # Backend backend: type: "redis-streams" validateEnvelopes: true redis: host: "localhost" port: 6379 discovery: enabled: true interval: 30 ttl: 90 ``` ### Production Example ```yaml # config/production.yml logging: level: info prettyPrint: false service: serviceGroup: "MyService" concurrency: 64 aliases: orders: "OrderService" users: "UserService" backend: type: "redis-streams" redis: host: "$REDIS_HOST" port: 6379 password: "$REDIS_PASSWORD" discovery: enabled: true ``` ## Environment Variable Substitution When `@strata-js/util-config` loads a file, it replaces environment variable references with their runtime values. Three syntaxes are supported: | Syntax | Example | |--------|---------| | `$VAR` | `$HOSTNAME` | | `${VAR}` | `${REDIS_HOST}` | | `{{VAR}}` | `{{REDIS_PASSWORD}}` | If an environment variable is not set, the reference is left as-is in the string. This means you should set all referenced variables before calling `configUtil.load()`. A common pattern is to set defaults before loading config: ```typescript import { hostname } from 'node:os'; process.env.HOSTNAME = process.env.HOSTNAME ?? hostname(); process.env.ENVIRONMENT = process.env.ENVIRONMENT ?? 'local'; const env = process.env.ENVIRONMENT; configUtil.load(`./config/${ env }.yml`); ``` ### File Includes Config files can include other config files using the `include` key. Included files are merged into the base config (included values are overridden by the base file's values): ```yaml # config/base.yml backend: type: "redis-streams" redis: host: "localhost" port: 6379 ``` ```yaml # config/local.yml include: "base.yml" service: serviceGroup: "MyService.$HOSTNAME" logging: level: debug prettyPrint: true ``` Multiple includes are supported as an array: ```yaml include: - "base.yml" - "logging-defaults.yml" ``` Include resolution goes up to 10 levels deep by default. Set the `INCLUDE_DEPTH` environment variable to change this limit. ## Envelope Validation All backends validate message envelopes by default. This catches malformed messages before they reach your handlers. The performance overhead is negligible (under 2% in benchmarks). To disable validation: ```typescript backend: { type: 'redis', validateEnvelopes: false, redis: { host: 'localhost', port: 6379 }, } ``` For details on what gets validated and how to handle validation errors, see the [Envelope Validation guide](/guides/envelope-validation). --- --- url: /core-api/context.md description: >- API reference for the StrataContext class, which groups related operations like an Express Router. --- # Context (StrataContext) The `StrataContext` class is a logical grouping of related operations -- analogous to an Express Router. You create contexts, register operation handlers on them, then register the context with the service. For conceptual background, see [Architecture](/concepts/architecture). ```typescript import { StrataContext } from '@strata-js/strata'; // Also exported as `Context` for convenience import { Context } from '@strata-js/strata'; ``` ## Constructor ```typescript new StrataContext(contextName ?: string) ``` Creates a new context. The name is optional here -- you can provide it when registering with the service instead. ```typescript const users = new StrataContext('users'); const orders = new StrataContext(); // name provided at registration time ``` ## Properties | Property | Type | Description | |----------------------------|--------------------------|----------------------------------------------------| | `contextName` | `string \| undefined` | The name set in the constructor, if any. | | `contextMiddlewareList` | `OperationMiddleware[]` | All middleware registered at the context level. | | `operationsMiddlewareList` | `OperationMiddleware[]` | All middleware registered at the operation level (deduplicated). | ## Methods ### `registerOperation()` ```typescript context.registerOperation( opName : string, opFunc : OperationHandler, opFuncParams : string[] = [], middleware : OperationMiddleware[] = [] ) : void ``` Registers a function as a named operation on this context. Throws `AlreadyRegisteredError` if the name is taken. **Parameters:** | Parameter | Type | Default | Description | |----------------|----------------------------------------|---------|--------------------------------------------------------------------------| | `opName` | `string` | -- | The operation name clients will use to call this handler. | | `opFunc` | `(...args) => Promise` | -- | The async handler function. | | `opFuncParams` | `string[]` | `[]` | Property paths to extract from the request and pass as arguments. | | `middleware` | `OperationMiddleware[]` | `[]` | Middleware to apply to this operation only. | ::: warning Parameter Order The third argument is `opFuncParams` (request property extraction), **not** middleware. Middleware is the **fourth** argument. ::: #### Basic Usage By default, the handler receives the full `StrataRequest` object: ```typescript context.registerOperation('get', async (request) => { const { userId } = request.payload; const user = await db.findUser(userId); return { user }; }); ``` If the handler returns a value and hasn't called `request.succeed()` or `request.fail()`, the return value is automatically used as the success payload. #### With `opFuncParams` The `opFuncParams` array lets you extract specific properties from the request and pass them as individual arguments. This is useful for keeping your handlers clean and decoupled from the request object. Each string is a lodash-style property path evaluated against the request. ```typescript // Handler receives (payload, auth) instead of (request) context.registerOperation('get', async (payload, auth) => { const user = await db.findUser(payload.userId); return { user }; }, [ 'payload', 'auth' ]); ``` ```typescript // Handler receives individual payload fields context.registerOperation('update', async (userId, updates) => { const user = await db.updateUser(userId, updates); return { user }; }, [ 'payload.userId', 'payload.updates' ]); ``` When `opFuncParams` is empty (the default), the full `StrataRequest` is passed as the only argument. #### With Operation-Level Middleware ```typescript context.registerOperation('get', async (request) => { return { user: await db.findUser(request.payload.userId) }; }, [], [ new CacheMiddleware({ ttl: 60000 }) ]); ``` ### `registerOperationsForInstance()` ```typescript context.registerOperationsForInstance( instance : T, opFuncParams : string[] = [], middleware : OperationMiddleware[] = [], opModifiers : { include ?: string[], exclude ?: string[] } = {} ) : void ``` Convenience method that registers all public methods of an object as operations. Each method becomes an operation with a matching name. **Excluded automatically:** the constructor, and any methods starting with `_` or `$`. **Parameters:** | Parameter | Type | Default | Description | |----------------|------------------------------------------------|---------|----------------------------------------------------------------| | `instance` | `T` | -- | The object whose methods become operations. | | `opFuncParams` | `string[]` | `[]` | Property paths extracted from the request, applied to all methods. | | `middleware` | `OperationMiddleware[]` | `[]` | Middleware applied to all registered operations. | | `opModifiers` | `{ include?: string[], exclude?: string[] }` | `{}` | Filter which methods to register. `exclude` takes precedence. | ```typescript class UserManager { async get(payload : { userId : string }) { return { user: await db.findUser(payload.userId) }; } async list() { return { users: await db.listUsers() }; } // Excluded automatically (starts with _) async _internalMethod() { /* ... */ } } const manager = new UserManager(); const users = new StrataContext('users'); // Register all public methods as operations, passing `payload` to each users.registerOperationsForInstance(manager, [ 'payload' ]); // Cherry-pick methods users.registerOperationsForInstance(manager, [ 'payload' ], [], { include: [ 'get' ], }); // Exclude specific methods users.registerOperationsForInstance(manager, [ 'payload' ], [], { exclude: [ 'list' ], }); ``` ### `useMiddleware()` ```typescript context.useMiddleware(middleware : OperationMiddleware | OperationMiddleware[]) : void ``` Registers context-level middleware that runs on every operation in this context. Accepts a single middleware or an array. For how middleware ordering works across service, context, and operation levels, see [Middleware Model](/concepts/middleware-model). ```typescript const users = new StrataContext('users'); users.useMiddleware(new RateLimitMiddleware()); ``` ### `getOperationNames()` ```typescript context.getOperationNames() : string[] ``` Returns a list of all registered operation names. ```typescript const names = users.getOperationNames(); // [ 'get', 'create', 'delete' ] ``` ## Registering with the Service A context does nothing until it's registered with a `StrataService`. Registration must happen **before** calling `service.start()`. ```typescript import { StrataService, StrataContext } from '@strata-js/strata'; const service = new StrataService(config); const users = new StrataContext('users'); users.registerOperation('get', async (request) => { return { user: await db.findUser(request.payload.userId) }; }); // Register with service (uses context's name) service.registerContext(users); // Or register under a different name service.registerContext('accounts', users); await service.start(); ``` ::: tip You can register the same context instance under multiple names. Both names will route to the same context. This is valid, but be aware they share the same state and middleware. ::: --- --- url: /core-api/envelope.md description: >- TypeScript interfaces for MessageEnvelope, RequestEnvelope, PostEnvelope, and ResponseEnvelope wire types. --- # Envelope Types These are the TypeScript interfaces for the message envelopes that make up Strata's wire protocol. For conceptual background on the protocol, see [The Protocol](/concepts/the-protocol). All envelope types are exported from the main package: ```typescript import type { MessageEnvelope, RequestEnvelope, PostEnvelope, ResponseEnvelope, ResponseMessage, } from '@strata-js/strata'; ``` ## MessageEnvelope The base interface shared by all envelope types. ```typescript interface MessageEnvelope> { id : string; messageType : 'request' | 'response' | 'post'; context : string; operation : string; timestamp : string; payload : PayloadType; } ``` | Field | Type | Description | |---------------|----------|--------------------------------------------------------------| | `id` | `string` | Unique message identifier (nanoid, 20 chars). | | `messageType` | `string` | Discriminator: `'request'`, `'response'`, or `'post'`. | | `context` | `string` | Target context name. | | `operation` | `string` | Target operation name. | | `timestamp` | `string` | ISO 8601 datetime. | | `payload` | `object` | Application-specific data. Opaque to the framework. | ## RequestEnvelope Sent by a client when it expects a response. Extends `MessageEnvelope` with `messageType: 'request'`. ```typescript interface RequestEnvelope< PayloadType = Record, MetadataType = Record, > extends MessageEnvelope { messageType : 'request'; responseQueue ?: string; timeout ?: number; metadata : MetadataType; auth ?: string; priorRequest ?: string; client ?: string; requestChain ?: string[]; } ``` | Field | Type | Required | Description | |-----------------|------------|:--------:|-------------------------------------------------------------------------| | `responseQueue` | `string` | no | Queue where the service should send the response. Set by the backend. | | `timeout` | `number` | no | Milliseconds the client will wait. `0` or omitted uses service default. | | `metadata` | `object` | yes | Additional metadata. Opaque to the framework. | | `auth` | `string` | no | Authentication data (typically a JWT). Passed through, never inspected. | | `priorRequest` | `string` | no | ID of the request that triggered this one (for tracing). | | `client` | `string` | no | Human-readable client identifier. | | `requestChain` | `string[]` | no | Chain of request IDs for distributed tracing. | ## PostEnvelope Sent by a client when no response is needed. Extends `MessageEnvelope` with `messageType: 'post'`. ```typescript interface PostEnvelope< PayloadType = Record, MetadataType = Record, > extends MessageEnvelope { messageType : 'post'; metadata : MetadataType; auth ?: string; priorRequest ?: string; client ?: string; requestChain ?: string[]; } ``` Same fields as `RequestEnvelope`, minus `responseQueue` and `timeout`. ## RequestOrPostEnvelope Union type used internally when handling either type of incoming message. ```typescript type RequestOrPostEnvelope< P = Record, M = Record > = RequestEnvelope | PostEnvelope; ``` ## ResponseEnvelope Sent by a service back to the client after processing a request. Extends `MessageEnvelope` with `messageType: 'response'`. ```typescript interface ResponseEnvelope> extends MessageEnvelope { id : string; messageType : 'response'; context : string; operation : string; timestamp : string; payload : PayloadType; status : 'succeeded' | 'failed' | 'pending'; messages : ResponseMessage[]; service : string; } ``` | Field | Type | Description | |------------|---------------------|----------------------------------------------------------------| | `status` | `string` | `'succeeded'`, `'failed'`, or `'pending'`. | | `messages` | `ResponseMessage[]` | Array of structured messages generated during processing. | | `service` | `string` | Human-readable service identifier (e.g. `'UserService v1.0'`). | ## ResponseMessage Structured messages generated during request processing. Useful for returning warnings, informational messages, or detailed error information alongside the response payload. ```typescript interface ResponseMessage> { severity : ResponseMessageSeverity; message : string; code ?: string; type ?: string; details ?: DetailsType; stack ?: string; } ``` | Field | Type | Required | Description | |------------|----------|:--------:|--------------------------------------------------------| | `severity` | `string` | yes | `'error'`, `'warning'`, `'info'`, or `'debug'`. | | `message` | `string` | yes | Human-readable message text. | | `code` | `string` | no | Machine-readable message code. | | `type` | `string` | no | Message type name (typically the error class name). | | `details` | `object` | no | Additional structured data. | | `stack` | `string` | no | Stack trace, if applicable. Never expose to end users. | ### ResponseMessageSeverity ```typescript const ResponseMessageSeverity = [ 'error', 'warning', 'info', 'debug' ] as const; type ResponseMessageSeverity = typeof ResponseMessageSeverity[number]; ``` ## Command Types Commands are a separate messaging mechanism from requests. They control service behavior at runtime. ### ServiceCommand ```typescript interface ServiceCommand> { id : string; command : ValidCommandName; payload ?: Payload; } ``` ### ValidCommandName ```typescript type ValidCommandName = 'info' | 'concurrency' | 'shutdown' | 'toobusy'; ``` ### ServiceCommandResponse ```typescript interface ServiceCommandResponse> { id : string; payload ?: Payload; } ``` ### Specific Command Types ```typescript // Info command -- the only command with a response interface StrataInfoCommand extends ServiceCommand { command : 'info'; responseChannel : string; } // Concurrency command interface StrataConcurrencyCommand extends ServiceCommand { command : 'concurrency'; payload : { concurrency : number }; } // Shutdown command interface StrataShutdownCommand extends ServiceCommand { command : 'shutdown'; payload ?: { graceful ?: boolean; exitCode ?: number }; } // TooBusy command interface StrataToobusyCommand extends ServiceCommand { command : 'toobusy'; payload : TooBusyConfig; } ``` --- --- url: /guides/envelope-validation.md description: >- How Strata's built-in envelope validation catches malformed messages at the transport layer with near-zero overhead. --- # Envelope Validation Strata includes built-in envelope validation that checks the structure of every message before it is processed. This catches malformed requests, bad field types, and missing required fields at the transport layer -- before your operation handlers or middleware ever see the message. Validation is **enabled by default** and has near-zero performance overhead. ## Why Validate Envelopes * **Runtime type safety.** TypeScript types disappear at runtime. A service can receive a payload that is `null` or an array instead of the expected object. Envelope validation catches these at the boundary. * **Better error messages.** Invalid requests get a clear `failed` response explaining what's wrong, instead of an opaque crash somewhere in your handler. * **Cross-implementation compatibility.** If you have Strata services written in different languages or different versions, validation ensures they all conform to the same wire format. * **Bug prevention.** Catches structural errors during development and testing before they reach production. ## Performance Validation uses hand-rolled checks with simple JavaScript primitives (`typeof`, equality comparisons, `Array.isArray()`). No schema libraries, no runtime compilation. **Benchmark results** (Apple M1 Max, Node.js v20.x, 100,000 iterations): | Method | Time per Operation | Overhead | |--------|-------------------|----------| | No validation | 1.09 us/op | baseline | | Hand-rolled validation | 1.09 us/op | 0% | | Zod schema validation | 43.17 us/op | 3,860% | The validation checks are so fast they're lost in the noise of `JSON.parse()` itself. At 10,000 requests per second, hand-rolled validation adds less than 0.1% additional CPU time compared to no validation at all. | Scenario (10K req/sec) | CPU Time per Second | % of CPU Core | |------------------------|--------------------:|---------------| | No validation | ~11ms | ~1.1% | | Hand-rolled validation | ~11ms | ~1.1% | | Zod library | ~455ms | ~45.5% | ## Configuration Validation is configured per-backend using the `validateEnvelopes` option. ### Enabled (Default) ```typescript const service = new StrataService({ service: { serviceGroup: 'MyService' }, backend: { type: 'redis-streams', redis: { host: 'localhost', port: 6379 }, // validateEnvelopes defaults to true -- no need to set it }, }); ``` ### Disabled ```typescript const service = new StrataService({ service: { serviceGroup: 'TrustedService' }, backend: { type: 'redis-streams', redis: { host: 'localhost', port: 6379 }, validateEnvelopes: false, }, }); ``` ::: warning Disabling validation removes a safety net for negligible performance gain. Only do this if you fully control every service on the bus and have verified envelope correctness through other means. ::: ## What Gets Validated Validation checks the structure and types of message envelopes. It does **not** inspect payload contents -- that's your handler's responsibility. ### Request Envelopes | Field | Type | Required | Notes | |-------|------|----------|-------| | `id` | `string` | yes | Must be non-empty | | `messageType` | `'request'` | yes | Literal value | | `context` | `string` | yes | Must be non-empty | | `operation` | `string` | yes | Must be non-empty | | `timestamp` | `string` | yes | Must be non-empty | | `payload` | `object` | yes | Not `null`, not an array | | `metadata` | `object` | yes | Not `null`, not an array | | `responseQueue` | `string` | no | | | `timeout` | `number` | no | | | `auth` | `string` | no | | | `client` | `string` | no | | | `priorRequest` | `string` | no | | | `requestChain` | `string[]` | no | Must be an array if present | ### Response Envelopes | Field | Type | Required | Notes | |-------|------|----------|-------| | `id` | `string` | yes | Must be non-empty | | `messageType` | `'response'` | yes | Literal value | | `context` | `string` | yes | Must be non-empty | | `operation` | `string` | yes | Must be non-empty | | `timestamp` | `string` | yes | Must be non-empty | | `payload` | `object` | yes | Not `null`, not an array | | `status` | `'succeeded'` | `'failed'` | `'pending'` | yes | | | `messages` | `ResponseMessage[]` | yes | Each message is validated | | `service` | `string` | yes | | Each `ResponseMessage` in the `messages` array must have: | Field | Type | Required | Notes | |-------|------|----------|-------| | `severity` | `'error'` | `'warning'` | `'info'` | `'debug'` | yes | | | `message` | `string` | yes | | | `code` | `string` | no | | | `type` | `string` | no | | | `stack` | `string` | no | | | `details` | `object` | no | Not an array if present | ### Post Envelopes | Field | Type | Required | Notes | |-------|------|----------|-------| | `id` | `string` | yes | Must be non-empty | | `messageType` | `'post'` | yes | Literal value | | `context` | `string` | yes | Must be non-empty | | `operation` | `string` | yes | Must be non-empty | | `timestamp` | `string` | yes | Must be non-empty | | `payload` | `object` | yes | Not `null`, not an array | | `metadata` | `object` | yes | Not `null`, not an array | | `auth` | `string` | no | | | `client` | `string` | no | | | `priorRequest` | `string` | no | | | `requestChain` | `string[]` | no | Must be an array if present | ### Service Commands All commands require `id` (non-empty string) and `command` (string). Additional fields vary by command: | Command | Additional Required Fields | |---------|--------------------------| | `info` | `responseChannel` (string) | | `concurrency` | `payload.concurrency` (number) | | `shutdown` | None -- payload is optional (`graceful`: boolean, `exitCode`: number) | | `toobusy` | `payload` (object, all fields optional: `maxLag`, `interval`, `smoothingFactorOnRise`, `smoothingFactorOnFall` -- all numbers) | ### Command Responses | Field | Type | Required | Notes | |-------|------|----------|-------| | `id` | `string` | yes | Must be non-empty | | `payload` | `object` | no | Not an array if present | ### What Is NOT Validated **Payload contents** are not inspected. Validation only checks that `payload` is a non-null, non-array object. Your handlers and middleware are responsible for validating the actual contents: ```typescript ctx.registerOperation('update', async (request) => { // Envelope validation guarantees payload is an object. // You must validate its contents: if(!request.payload.userId || typeof request.payload.userId !== 'string') { throw new Error('userId is required and must be a string'); } // Process the request... }); ``` ## Error Handling What happens when validation fails depends on the message type: ### Invalid Requests (Service Side) 1. The backend validates the envelope structure. 2. If invalid: extract minimal fields (`id`, `responseQueue`, `context`, `operation`) if possible. 3. Send a `failed` response with message: "Envelope validation failed: The request envelope structure is invalid." 4. If `responseQueue` can't be extracted, log a critical error and drop the message. 5. The `incomingRequest` event is **not** emitted -- your handlers never see the request. ### Invalid Responses (Client Side) 1. The backend validates the envelope structure. 2. If invalid: log a warning and drop the message. 3. The `incomingResponse` event is **not** emitted. ### Invalid Posts 1. The backend validates the envelope structure. 2. If invalid: log a warning and drop the message. ### Invalid Commands 1. The backend validates the command structure. 2. If invalid: log a warning and drop the message. ## When to Disable Validation **Almost never.** The performance cost is effectively zero, and the safety benefits are real. That said, there are narrow cases where disabling makes sense: **Disable when:** * All services are written and controlled by your team * All services use the same Strata version * Services communicate on a fully trusted network * You have verified envelope correctness through other means (comprehensive integration tests) **Keep enabled when:** * Services communicate across team or organization boundaries * Multiple Strata implementations are in use (different languages, different versions) * Third-party services connect to your bus * Development or staging environments (where bugs are most common) ## Troubleshooting ### "Envelope validation failed" Errors Your client is receiving a `failed` response with a validation error message. **Check:** * All required fields are present (see tables above) * `payload` is an object (`{}`) -- not `null`, not an array, not a primitive * `metadata` is an object if present * `messageType` matches the expected value (`'request'`, `'response'`, or `'post'`) * String fields are non-empty * For responses: `status` is one of `'succeeded'`, `'failed'`, `'pending'` * For responses: `messages` is an array with valid `severity` values ### Validation Passes But Handler Fails This means the envelope structure is correct, but the payload contents are wrong. Envelope validation only checks structure -- not business logic. Validate payload contents in your handlers or use the [Payload Validation Middleware](/middleware/payload-validation). ## Best Practices 1. **Leave validation enabled.** The 0-2% overhead is negligible. The safety is not. 2. **Validate payload contents yourself.** Envelope validation checks structure, not business data. Use middleware or handler-level checks for payload validation. 3. **Handle validation errors in clients.** Check for `failed` responses with validation error messages and handle them appropriately. 4. **Monitor validation failures.** Log and alert on validation errors -- they indicate a bug in a service or client. 5. **Test with validation on.** Always run tests with validation enabled to catch structural issues early. ## Next Steps * [Backends](/core-api/backends) -- backend-specific configuration, including `validateEnvelopes`. * [Configuration](/getting-started/configuration) -- full configuration reference. * [The Protocol](/concepts/the-protocol) -- the envelope format that validation enforces. --- --- url: /ai/llms-txt.md description: >- Auto-generated llms.txt and llms-full.txt files that provide LLM-friendly documentation for AI coding tools. --- # LLM Documentation This site automatically generates LLM-friendly documentation files following the [llms.txt](https://llmstxt.org/) standard. These files are designed to be consumed by AI tools, coding assistants, and language models that need to understand the Strata.js API. ## Available Files | File | Description | |------|-------------| | [`/llms.txt`](/llms.txt) | Index file with section links — a table of contents for LLMs. | | [`/llms-full.txt`](/llms-full.txt) | The entire documentation site concatenated into a single file. | Individual pages are also available as clean markdown files optimized for LLM consumption. ## Usage Point your AI tool at the `llms.txt` or `llms-full.txt` URL to give it full context on Strata.js: ``` https://strata-js.gitlab.io/llms.txt https://strata-js.gitlab.io/llms-full.txt ``` For tools that support the `llms.txt` standard (like Context7, Cursor, or similar), these files are discovered automatically. ## How It Works The files are generated at build time by [vitepress-plugin-llms](https://github.com/okineadev/vitepress-plugin-llms). Every page in the documentation is processed into a clean, LLM-optimized markdown format with navigation links and structural metadata stripped out. --- --- url: /utilities/logging.md description: >- Structured logging utility wrapping pino with a console.*-style API for multi-argument log messages. --- # Logging `@strata-js/util-logging` is a structured logging utility that wraps [pino](https://getpino.io). It changes pino's default behavior to match the `console.*` API -- when you pass multiple arguments, they are serialized and appended to the log message instead of being added as separate JSON fields. This package is also re-exported from `@strata-js/strata`, so if you're already using Strata you don't need to install it separately. ## Installation ```bash npm install @strata-js/util-logging ``` For pretty-printed output during development, also install `pino-pretty` as a dev dependency: ```bash npm install -D pino-pretty ``` ## Usage ```typescript import { logging } from '@strata-js/util-logging'; const logger = logging.getLogger('myService'); logger.info('Server started on port', 8080); // [12:00:00 PM] INFO (myService): Server started on port 8080 logger.error('Request failed', { status: 500, path: '/api/users' }); // [12:00:01 PM] ERROR (myService): Request failed {"status":500,"path":"/api/users"} ``` If you're using Strata, the logging module is available directly: ```typescript import { logging } from '@strata-js/strata'; const logger = logging.getLogger('myService'); ``` ## API ### `getLogger(name)` ```typescript logging.getLogger(name : string) : BasicLogger ``` Returns a logger instance for the given name. If a logger with that name already exists, the same instance is returned. | Parameter | Type | Description | |-----------|------|-------------| | `name` | `string` | A name for the logger, typically your module or service name. Appears in log output. | ### `setConfig(config)` ```typescript logging.setConfig(config : LoggerConfig) : void ``` Updates the logging configuration. The new config applies to loggers created after this call. Loggers that have already produced output are not affected. ::: tip Call `setConfig()` early in your application startup, before creating any loggers, to ensure all loggers pick up the configuration. ::: ### `LoggerConfig` | Option | Type | Default | Description | |--------|------|---------|-------------| | `level` | `string` | `'debug'` | Minimum log level. Messages below this level are suppressed. | | `prettyPrint` | `boolean` | `true` | Enable pretty-printed output via [pino-pretty](https://github.com/pinojs/pino-pretty). Only takes effect if `pino-pretty` is installed. | ### `BasicLogger` The logger object returned by `getLogger()`. Each method accepts any number of arguments -- objects are serialized to JSON and all arguments are joined with spaces into the final message string. ```typescript interface BasicLogger { trace(...args : unknown[]) : void; debug(...args : unknown[]) : void; info(...args : unknown[]) : void; warn(...args : unknown[]) : void; error(...args : unknown[]) : void; fatal(...args : unknown[]) : void; silent(...args : unknown[]) : void; } ``` ## Log Levels Levels are listed from most verbose to least verbose. Setting a level means that level and everything above it will be logged. | Level | Description | |-------|-------------| | `trace` | Fine-grained diagnostic information. | | `debug` | General debugging information. The default level. | | `info` | Normal operational messages. | | `warn` | Something unexpected, but the application can continue. | | `error` | An error occurred. The operation failed but the application continues. | | `fatal` | A critical error. The application is likely about to crash. | | `silent` | Suppresses all output. Calling `logger.silent()` is a no-op. | ## Examples ### Configuration ```typescript import { logging } from '@strata-js/util-logging'; // Production: JSON output, info level logging.setConfig({ level: 'info', prettyPrint: false }); const logger = logging.getLogger('myService'); // {"level":30,"time":1622349751673,"name":"myService","msg":"Request processed in 42ms"} logger.info('Request processed in 42ms'); ``` ### Disabling Logging Set the level to `silent` to suppress all log output: ```typescript import { logging } from '@strata-js/util-logging'; logging.setConfig({ level: 'silent' }); const logger = logging.getLogger('myService'); logger.error('This will not appear'); ``` ### Multiple Loggers Create separate loggers for different parts of your application: ```typescript import { logging } from '@strata-js/util-logging'; const dbLogger = logging.getLogger('database'); const httpLogger = logging.getLogger('http'); dbLogger.info('Connected to PostgreSQL'); httpLogger.info('Listening on port', 8080); ``` ### Logging Objects and Errors Arguments are automatically serialized. Errors include their stack trace. ```typescript const logger = logging.getLogger('myService'); // Objects are JSON-serialized logger.info('User created', { id: 42, name: 'Alice' }); // [12:00:00 PM] INFO (myService): User created {"id":42,"name":"Alice"} // Errors include the stack trace try { await riskyOperation(); } catch(err) { logger.error('Operation failed', err); // [12:00:01 PM] ERROR (myService): Operation failed Error: something broke // at riskyOperation (/app/src/index.ts:42:11) // ... } ``` ### Pretty Print vs JSON With `prettyPrint: true` (the default, requires `pino-pretty` installed): ``` [12:00:00 PM] INFO (myService): Server started on port 8080 ``` With `prettyPrint: false`: ```json {"level":30,"time":1622349751673,"name":"myService","msg":"Server started on port 8080"} ``` Use pretty printing during development and JSON output in production for structured log aggregation. --- --- url: /ai/mcp-server.md description: >- MCP server that lets AI coding tools discover, inspect, and call running Strata services via Redis for local development. --- # MCP Server [`@strata-js/mcp`](https://gitlab.com/strata-js/tools/mcp-server) is an MCP (Model Context Protocol) server that bridges AI coding tools with your running Strata.js services via Redis. It lets tools like Claude Code, Cursor, and Windsurf discover, inspect, and call your services directly. ::: danger This is a lightsaber. You will cut your arm off. The MCP server gives an AI tool direct access to your running services. It can discover every operation and send requests with arbitrary payloads. **Do not use this in production.** It is a development tool. Things to consider before enabling it: * **Data exposure.** Every request and response flows through your LLM provider. If your services handle sensitive data (PII, credentials, financial data), that data will be sent to the LLM. * **Destructive operations.** An AI tool can call any operation it discovers. While the server blocks common destructive patterns (`delete`, `remove`, `purge`) by default, it cannot guarantee safety. A poorly worded prompt could trigger unintended writes or state changes. * **No auth boundary.** The MCP server connects to Redis with whatever credentials you give it. There's no additional auth layer between the AI tool and your services. Use this for local development and debugging. Never point it at a production Redis instance. ::: ## What It Does The MCP server exposes three tools to your AI assistant: | Tool | Description | |------|-------------| | `strata-discover` | Lists all registered services and their operations. | | `strata-service-info` | Gets detailed info about a specific service group (operations, instance count, metadata). | | `strata-request` | Sends a request to a Strata service with a custom payload and returns the response. | This means your AI tool can: * Ask "what services are running?" and get a real answer * Inspect a service's available operations before calling them * Make actual requests to services and see the responses * Debug service behavior interactively ## Setup No global install needed. Add the server to your AI tool's MCP configuration: ### Claude Code Add to `.mcp.json` in your project root (or `~/.claude/mcp.json` for global access): ```json { "mcpServers": { "strata": { "command": "npx", "args": ["@strata-js/mcp"], "env": { "STRATA_REDIS_HOST": "localhost", "STRATA_REDIS_PORT": "6379" } } } } ``` ### Cursor Add to `.cursor/mcp.json`: ```json { "mcpServers": { "strata": { "command": "npx", "args": ["@strata-js/mcp"], "env": { "STRATA_REDIS_HOST": "localhost", "STRATA_REDIS_PORT": "6379" } } } } ``` ## Configuration All configuration is via environment variables: | Variable | Default | Description | |----------|---------|-------------| | `STRATA_REDIS_HOST` | `localhost` | Redis host. | | `STRATA_REDIS_PORT` | `6379` | Redis port. | | `STRATA_REDIS_DB` | `0` | Redis database number. | | `STRATA_REDIS_USER` | -- | Redis username (if using ACLs). | | `STRATA_REDIS_PASS` | -- | Redis password. | | `STRATA_CLIENT_NAME` | `strata-mcp` | Client name on the message bus. | | `STRATA_BLOCK_RULES` | *(see below)* | Comma-separated wildcard patterns for blocked operations. | ### Block Rules By default, the server blocks operations matching common destructive patterns: ``` *.save, *.delete, *.remove, *.purge, *.drop, *.truncate, *.destroy ``` You can customize this with the `STRATA_BLOCK_RULES` environment variable: ```json "env": { "STRATA_BLOCK_RULES": "*.delete,*.remove,admin.*" } ``` Set to an empty string to disable blocking entirely (not recommended). ## Requirements * Node.js >= 22.0.0 * A running Redis instance with Strata services connected * An AI tool that supports MCP (Claude Code, Cursor, Windsurf, etc.) --- --- url: /middleware/message-logging.md description: >- Middleware that logs request and response envelopes to a monitoring service group for debugging and observability. --- # Message Logging Middleware Logs request and response envelopes to a separate service group for monitoring and debugging. Designed as a companion to the [Strata Queue Monitor](https://gitlab.com/strata-js/tools/strata-queue-monitor) (SQM), but works with any service that accepts the logged payload format. ## Installation ```bash npm install @strata-js/middleware-message-logging ``` Peer dependency: `@strata-js/strata ^2.0.0` ## Usage Create a `MessageLoggingMiddleware` instance with a `StrataClient` and configuration, then register it -- typically as global middleware so all requests are logged. ```typescript import { StrataService, StrataClient } from '@strata-js/strata'; import { MessageLoggingMiddleware } from '@strata-js/middleware-message-logging'; const service = new StrataService(serviceConfig); const client = new StrataClient(clientConfig); const logging = new MessageLoggingMiddleware(client, { serviceGroup: { toMonitor: 'UserService', toLogTo: 'StrataQueueMonitor', }, }); service.useMiddleware(logging); await client.start(); await service.start(); ``` The middleware uses the provided `StrataClient` to fire-and-forget (post) each logged envelope to the target service group. Logging is non-blocking -- it does not wait for the monitoring service to acknowledge receipt. ## Configuration ### `MessageLoggingConfig` ```typescript interface MessageLoggingConfig { serviceGroup : { toMonitor : string; toLogTo : string; }; endpoint ?: { context ?: string; operation ?: string; }; exclusions ?: string[]; logOnFailure ?: string[]; shouldLogRequestFn ?: (request : Request) => boolean; transformRequestFn ?: (request : RequestEnvelope) => void; transformResponseFn ?: (response : ResponseEnvelope) => void; } ``` | Option | Type | Default | Description | |--------|------|---------|-------------| | `serviceGroup.toMonitor` | `string` | -- | The service group being monitored (used as the `queue` field in the logged payload). | | `serviceGroup.toLogTo` | `string` | -- | The service group to send logged messages to (e.g., `'StrataQueueMonitor'`). | | `endpoint.context` | `string` | `'monitor'` | The context on the logging service to post to. | | `endpoint.operation` | `string` | `'logMessage'` | The operation on the logging service to post to. | | `exclusions` | `string[]` | `[]` | Endpoints to skip entirely (no request or response logging). | | `logOnFailure` | `string[]` | `[]` | Endpoints that are only logged when the request fails. | | `shouldLogRequestFn` | `(request) => boolean` | -- | Custom filter function. Return `false` to skip logging for a request. | | `transformRequestFn` | `(envelope) => void` | -- | Mutate the request envelope before it is logged (operates on a deep clone). | | `transformResponseFn` | `(envelope) => void` | -- | Mutate the response envelope before it is logged (operates on a deep clone). | ## Exclusions Skip logging for specific endpoints by listing them in `exclusions`. Each entry is a string in one of two forms: * `'context.operation'` -- exclude a specific operation * `'context.*'` -- exclude an entire context ```typescript const logging = new MessageLoggingMiddleware(client, { serviceGroup: { toMonitor: 'UserService', toLogTo: 'StrataQueueMonitor' }, exclusions: [ 'health.*', // Skip all health-check operations 'internal.refreshCache', // Skip a specific noisy operation ], }); ``` Excluded endpoints are completely skipped -- neither request nor response envelopes are logged, even on failure. ## Log on Failure The `logOnFailure` list uses the same format as `exclusions`, but instead of silencing logging it defers it: the request envelope is only logged when the operation fails. The response envelope is always logged on failure. ```typescript const logging = new MessageLoggingMiddleware(client, { serviceGroup: { toMonitor: 'UserService', toLogTo: 'StrataQueueMonitor' }, logOnFailure: [ 'batch.*', // Only log batch operations when they fail 'reports.generate', // Only log report generation on failure ], }); ``` This is useful for high-volume endpoints where you only care about failures. ## Custom Filtering For filtering logic beyond simple string matching, provide a `shouldLogRequestFn`. It receives the full `Request` object and returns a boolean: ```typescript const logging = new MessageLoggingMiddleware(client, { serviceGroup: { toMonitor: 'UserService', toLogTo: 'StrataQueueMonitor' }, shouldLogRequestFn: (request) => { // Only log requests from external clients return !request.metadata?.internal; }, }); ``` ::: info `shouldLogRequestFn` is checked in addition to `exclusions` and `logOnFailure`. If an endpoint is in `exclusions`, it is always skipped regardless of `shouldLogRequestFn`. ::: ## Transform Functions The `transformRequestFn` and `transformResponseFn` options let you modify envelopes before they are logged. The middleware deep-clones the envelope first, so your mutations won't affect the actual request or response flowing through the service. ```typescript const logging = new MessageLoggingMiddleware(client, { serviceGroup: { toMonitor: 'UserService', toLogTo: 'StrataQueueMonitor' }, transformRequestFn: (envelope) => { // Redact sensitive fields before logging if(envelope.payload?.password) { envelope.payload.password = '[REDACTED]'; } }, transformResponseFn: (envelope) => { // Strip large payloads from logged responses if(envelope.payload && JSON.stringify(envelope.payload).length > 10000) { envelope.payload = { _truncated: true }; } }, }); ``` ## Logged Payload Format Each logged message is posted with the following payload structure: ```typescript interface LoggedPayload { queue : string; envelope : RequestEnvelope | ResponseEnvelope; } ``` * **`queue`** -- For request envelopes, this is `serviceGroup.toMonitor`. For response envelopes, this is the request's `responseQueue`. * **`envelope`** -- The full serialized request or response envelope. ## Middleware Lifecycle | Hook | Behavior | |------|----------| | `beforeRequest` | If the request passes all filters, posts the request envelope to the logging service. | | `success` | If the request passes all filters, posts the response envelope. | | `failure` | If not excluded, posts the response envelope. Also posts the request envelope if the endpoint is in `logOnFailure`. | ## Examples ### Global Logging with SQM The most common setup -- log everything to the Strata Queue Monitor: ```typescript import { StrataService, StrataClient } from '@strata-js/strata'; import { MessageLoggingMiddleware } from '@strata-js/middleware-message-logging'; const service = new StrataService({ service: { serviceGroup: 'OrderService' }, backend: { type: 'redis-streams', redis: { host: 'localhost', port: 6379 } }, }); const client = new StrataClient({ backend: { type: 'redis-streams', redis: { host: 'localhost', port: 6379 } }, }); const logging = new MessageLoggingMiddleware(client, { serviceGroup: { toMonitor: 'OrderService', toLogTo: 'StrataQueueMonitor', }, }); service.useMiddleware(logging); await client.start(); await service.start(); ``` ### Custom Logging Endpoint Route logs to a custom monitoring service: ```typescript const logging = new MessageLoggingMiddleware(client, { serviceGroup: { toMonitor: 'OrderService', toLogTo: 'CustomMonitor', }, endpoint: { context: 'audit', operation: 'recordMessage', }, }); ``` ### Selective Logging with Exclusions Log everything except health checks and an internal cache refresh operation: ```typescript const logging = new MessageLoggingMiddleware(client, { serviceGroup: { toMonitor: 'OrderService', toLogTo: 'StrataQueueMonitor', }, exclusions: [ 'service.*', 'internal.refreshCache', ], logOnFailure: [ 'batch.*', ], }); ``` --- --- url: /concepts/middleware-model.md description: >- How Strata's three-hook middleware system (beforeRequest, success, failure) intercepts and modifies requests and responses. --- # Middleware Model Strata's middleware system lets you intercept and modify requests and responses at three distinct points in the processing lifecycle. Unlike Express-style middleware (a single function chain), Strata middleware uses three explicit hooks, making it clear exactly when your code runs. ## The Three Hooks Every middleware can implement up to three hooks: ### `beforeRequest(request)` Called before the operation handler runs. This is the only hook that can modify the incoming request. It can also short-circuit processing by calling `request.succeed()` or `request.fail()` directly -- if it does, the remaining `beforeRequest` hooks and the operation handler are skipped, and execution jumps straight to `success()` or `failure()`. ### `success(request)` Called after the operation handler succeeds. Can modify the outgoing response payload and messages. Cannot modify the original request. ### `failure(request)` Called after the operation handler fails (either by calling `request.fail()` or throwing an exception). Can modify the error response. Cannot modify the original request. ## Request Flow ```mermaid flowchart TD A[Incoming Request] --> B["beforeRequest()"] B --> C{Short-circuited?} C -->|No| D[Operation Handler] C -->|Yes, succeeded| F C -->|Yes, failed| G D --> E{Result} E -->|Succeeded| F["success()"] E -->|Failed| G["failure()"] F --> H[Send Response] G --> H ``` The `beforeRequest` hook runs first. If it doesn't short-circuit, the operation handler executes. Based on the result, either `success()` or `failure()` runs. Then the response is sent. The `success()` and `failure()` hooks always run -- they cannot be skipped. ## Registration Levels Middleware can be registered at three levels, each with a different scope: ### Global (Service-level) Applies to every request the service processes, across all contexts and operations. ```typescript import { StrataService } from '@strata-js/strata'; const service = new StrataService(config); service.useMiddleware(new AuthMiddleware()); ``` ### Context-level Applies to every operation within a specific context. ```typescript import { StrataContext } from '@strata-js/strata'; const users = new StrataContext('users'); users.useMiddleware(new RateLimitMiddleware()); ``` ### Operation-level Applies to a single operation only. Passed as an array in the fourth argument to `registerOperation()`. ```typescript users.registerOperation('get', async (request) => { return { user: await db.findUser(request.payload.userId) }; }, [], [ new CacheMiddleware({ ttl: 60000 }) ]); ``` ## Calling Order Middleware executes outside-in for `beforeRequest()` and inside-out for `success()` / `failure()`: ```mermaid flowchart LR G["Global Middleware"] -->|"beforeRequest()"| C["Context Middleware"] C -->|"beforeRequest()"| O["Operation Middleware"] O -->|"Call Handler"| H["Operation Handler"] H -->|"Return Result"| O2["Operation Middleware"] O2 -->|"success() / failure()"| C2["Context Middleware"] C2 -->|"success() / failure()"| G2["Global Middleware"] ``` **`beforeRequest()` order:** Global -> Context -> Operation **`success()` / `failure()` order:** Operation -> Context -> Global This means global middleware wraps everything. It sees the request first and the response last -- useful for cross-cutting concerns like logging, authentication, or tracing. ## Writing a Middleware A middleware is any object that implements `beforeRequest()` and optionally `success()` and `failure()`. The recommended approach is a class: ```typescript import type { OperationMiddleware } from '@strata-js/strata'; import type { StrataRequest } from '@strata-js/strata'; class TimingMiddleware implements OperationMiddleware { #startTimes = new Map(); async beforeRequest(request : StrataRequest) : Promise { this.#startTimes.set(request.id, Date.now()); return request; } async success(request : StrataRequest) : Promise { const start = this.#startTimes.get(request.id); if(start) { const duration = Date.now() - start; console.log(`[${ request.context }/${ request.operation }] completed in ${ duration }ms`); this.#startTimes.delete(request.id); } return request; } async failure(request : StrataRequest) : Promise { const start = this.#startTimes.get(request.id); if(start) { const duration = Date.now() - start; console.error(`[${ request.context }/${ request.operation }] failed after ${ duration }ms`); this.#startTimes.delete(request.id); } return request; } } ``` Key points: * **All hooks are async.** Even if the work is synchronous, the interface expects promises. * **Hooks run serially.** A slow middleware delays the entire chain. Keep hooks fast. * **You must instantiate the class.** Register `new TimingMiddleware()`, not the class itself. * **Return the request.** Each hook receives a `StrataRequest` and should return it (potentially modified). If you return `undefined`, the original request is used. --- --- url: /middleware.md description: >- Overview of first-party Strata middleware packages for caching, message logging, and payload validation. --- # Middleware Packages Strata publishes first-party middleware as separate npm packages under the `@strata-js` scope. Each package provides a middleware class that you instantiate with options and register on your service, context, or operation. For how to register and order middleware, see the [Using Middleware](/guides/using-middleware) guide. For building your own, see [Writing Middleware](/guides/writing-middleware). ## Available Packages | Package | Description | npm | |---------|-------------|-----| | [Cache](./cache) | Caches operation responses using memory, Redis, or a custom store. | `@strata-js/middleware-cache` | | [Message Logging](./message-logging) | Logs request/response envelopes to a queue for monitoring. | `@strata-js/middleware-message-logging` | | [Payload Validation](./payload-validation) | Validates request payloads using JSON Schema (AJV) or Zod. | `@strata-js/middleware-payload-validation` | ## Quick Example ```typescript import { StrataService, StrataContext } from '@strata-js/strata'; import { CacheMiddleware } from '@strata-js/middleware-cache'; import { MessageLoggingMiddleware } from '@strata-js/middleware-message-logging'; import { AjvPayloadValidationMiddleware } from '@strata-js/middleware-payload-validation/ajv'; const service = new StrataService(config); const users = new StrataContext('users'); // Global -- logs every request/response service.useMiddleware(new MessageLoggingMiddleware(client, { serviceGroup: { toMonitor: 'UserService', toLogTo: 'MonitorService' }, })); // Context -- validates every operation in 'users' users.useMiddleware(new AjvPayloadValidationMiddleware(userSchemas)); // Operation -- caches get responses users.registerOperation('get', async (request) => { return { user: await db.findUser(request.payload.userId) }; }, [], [ new CacheMiddleware({ kind: 'memory', key: 'users' }) ]); service.registerContext(users); await service.start(); ``` ## Writing Custom Middleware Need something these packages don't cover? Strata's middleware interface is simple to implement. See the [Writing Middleware](/guides/writing-middleware) guide for a complete walkthrough. --- --- url: /guides/migration-v1-to-v2.md description: >- Step-by-step guide to migrating from Strata v1 to v2, covering all breaking changes and their replacements. --- # Migrating from v1 to v2 Strata v2 refines the API based on real-world usage of v1. While v1 is battle-tested, its API had some rough edges -- a singleton service, a built-in configuration system, and tightly coupled Redis usage. v2 cleans all of that up. Most of the internals stayed the same, but the external API has changed to be more explicit and easier to understand. The migration is straightforward. Go through each breaking change below and apply the corresponding action. ## Breaking Changes ### `service` Singleton Replaced with `StrataService` Class The exported `service` singleton is gone. You now construct your own `StrataService` instances. This makes the code easier to understand, easier to test (no singleton state leaking between tests), and allows multiple service instances in the same process. **Before:** ```typescript import { service } from '@strata-js/strata'; import config from './config'; service.parseConfig(config); await service.init('ExampleService'); ``` **After:** ```typescript import { StrataService } from '@strata-js/strata'; import configUtil from '@strata-js/util-config'; const config = configUtil.loadConfig('./config.yaml'); const service = new StrataService(config); await service.start(); ``` ### `service.id` is Read-only and Auto-generated Previously you could set the service ID in configuration. Now it's automatically generated and always unique. **Action:** Remove any `id` settings from your service configuration. ### `service.name` Renamed to `service.serviceGroup` What was called `name` in v1 was actually used as the service group identifier for message routing. v2 renames it to `serviceGroup` to reflect this. There is now a separate read-only `name` property pulled from `package.json` for logging and informational purposes. **Before:** ```yaml service: name: MyService ``` **After:** ```yaml service: serviceGroup: MyService ``` **In code:** ```typescript // Before console.log(service.name); // 'MyService' (used for routing) // After console.log(service.serviceGroup); // 'MyService' (used for routing) console.log(service.name); // 'my-app' (from package.json, informational only) ``` ### `service.queue` Removed The `queue` property is gone. Queue names are now an implementation detail of the backend. Use `serviceGroup` for identifying your service. The `responseQueue` field on messages is still present but should be treated as an opaque value. **Action:** Replace any usage of `service.queue` with `service.serviceGroup`. ### `service.init()` Renamed to `service.start()` The method name changed to better reflect what it does. Additionally, `start()` takes no arguments -- configuration is passed to the constructor. **Before:** ```typescript service.init('ExampleService'); // or service.init(config); ``` **After:** ```typescript const service = new StrataService(config); await service.start(); ``` ### `service.parseConfig()` Removed The internal configuration parsing is gone. Configuration is passed directly to the `StrataService` constructor. Use your own configuration library, or use `@strata-js/util-config`. **Before:** ```typescript import { service } from '@strata-js/strata'; import config from './config'; service.parseConfig(config); await service.init('ExampleService'); ``` **After:** ```typescript import { StrataService } from '@strata-js/strata'; import configUtil from '@strata-js/util-config'; const env = process.env.ENVIRONMENT ?? 'local'; const config = configUtil.loadConfig(`./config/${ env }.yaml`); const service = new StrataService(config); await service.start(); ``` ### `service.setToobusy()` Renamed to `service.setTooBusy()` Capital B. That's it. **Action:** Find and replace `setToobusy` with `setTooBusy`. ### `service.client` Removed The built-in client instance on the service is gone. If your service needs to make requests to other services, create your own `StrataClient`: **Before:** ```typescript const response = await service.client.request('OtherService', 'ctx', 'op', {}); ``` **After:** ```typescript import { StrataClient } from '@strata-js/strata'; const client = new StrataClient({ client: { name: 'MyServiceClient' }, backend: { type: 'redis-streams', redis: { host: 'localhost', port: 6379 } }, }); await client.start(); const response = await client.request('OtherService', 'ctx', 'op', {}); ``` ### Convenience Methods Removed (`service.command`, `service.post`, `service.request`) These were wrappers around the internal client. Now that the client is separate, use it directly. **Action:** Replace `service.command(...)`, `service.post(...)`, and `service.request(...)` with calls on your own `StrataClient` instance. ### Configuration Utility Removed The `@strata-js/util-env-config` library is deprecated. The old system required a JavaScript/TypeScript config file that could execute arbitrary code and used a tiered environment system that confused people. v2 has no built-in configuration system. You pass a config object to the constructor, however you want to build it. We recommend `@strata-js/util-config`, which supports YAML, JSON, and JSON5: **Before:** ```typescript import { service } from '@strata-js/strata'; import config from './config'; // JS/TS config file service.parseConfig(config); ``` **After:** ```typescript import { StrataService } from '@strata-js/strata'; import configUtil from '@strata-js/util-config'; const env = process.env.ENVIRONMENT ?? 'local'; const config = configUtil.loadConfig(`./config/${ env }.yaml`); const service = new StrataService(config); ``` The configuration is now exposed on both service and client instances via the `config` property: ```typescript console.log(service.config); console.log(client.config); ``` ### Configuration Key Changes Several config keys have been renamed or removed: | v1 Key | v2 Key | Notes | |--------|--------|-------| | `service.name` | `service.serviceGroup` | Reflects actual usage | | `services` | `aliases` | Renamed -- these are shorthands for service groups | | `redis` | *(removed)* | Moved under `backend.redis` | | `service.queues` | *(removed)* | Moved under backend-specific config | ### `backend` Configuration is Required v1 only supported Redis Streams. v2 supports multiple backends and does not have a default -- you must specify one. **Before (implicit Redis Streams):** ```yaml redis: host: localhost port: 6379 ``` **After (explicit backend):** ```yaml backend: type: redis-streams discovery: enabled: false redis: host: localhost port: 6379 db: 0 ``` The same config as a TypeScript object: ```typescript const config = { backend: { type: 'redis-streams', discovery: { enabled: false }, redis: { host: 'localhost', port: 6379, db: 0 }, }, }; ``` This is as close to v1 behavior as possible. The `redis-streams` backend uses the same Redis Streams implementation as v1. ### `StrataClient` Requires Configuration in Constructor Previously `StrataClient` had its own config loading methods (`setConfig`, `parseConfig`). Now configuration is passed to the constructor: **Before:** ```typescript const client = new StrataClient(); client.parseConfig(config); ``` **After:** ```typescript const client = new StrataClient({ client: { name: 'MyClient' }, backend: { type: 'redis-streams', redis: { host: 'localhost', port: 6379 } }, }); ``` ### `client` is No Longer an `EventEmitter` The client previously emitted a `commandResponse` event. This has been replaced by `client.command()` returning the response directly. **Before:** ```typescript client.on('commandResponse', (response) => { console.log(response); }); client.command('Services:MyService', 'info'); ``` **After:** ```typescript const response = await client.command('info', 'Services:MyService'); console.log(response); ``` Note that the argument order for `client.command()` has also changed: the command name is now the first argument, and the target is the second. ### `client.command()` Signature Changed The method signature changed to put the command name first: **Before:** ```typescript client.command('Services:MyServiceGroup', 'concurrency', { concurrency: 5 }); ``` **After:** ```typescript client.command('concurrency', 'Services:MyServiceGroup', { concurrency: 5 }); ``` And it now returns the response directly (a `Promise`) instead of requiring an event listener. ## Quick Reference | What Changed | v1 | v2 | |---|---|---| | Service creation | `import { service }` (singleton) | `new StrataService(config)` | | Starting | `service.init('Name')` | `service.start()` | | Service name config | `service.name` | `service.serviceGroup` | | Config loading | `service.parseConfig(config)` | Pass to constructor | | Config utility | `@strata-js/util-env-config` | `@strata-js/util-config` | | Config files | JS/TS exports | YAML, JSON, or JSON5 | | Client access | `service.client` | Create your own `StrataClient` | | Backend config | Implicit Redis Streams | Explicit `backend.type` required | | Redis config | Top-level `redis` key | Under `backend.redis` | | Service aliases | `services` key | `aliases` key | | Queue config | `service.queues` | Backend-specific config | | Command sending | `client.command(target, cmd, payload)` | `client.command(cmd, target, payload)` | | Command responses | Event-based (`commandResponse`) | Return value from `command()` | | TooBusy setter | `service.setToobusy()` | `service.setTooBusy()` | ## Next Steps * [Quick Start](/getting-started/quick-start) -- get a v2 service running from scratch. * [Configuration](/getting-started/configuration) -- full v2 configuration reference. * [Backends](/core-api/backends) -- the new pluggable backend system. --- --- url: /middleware/payload-validation.md description: >- Middleware that validates request payloads using AJV (JSON Schema) or Zod before they reach operation handlers. --- # Payload Validation Middleware Validates request payloads before they reach your operation handlers. Supports two validation engines: [AJV](https://ajv.js.org/) (JSON Schema) and [Zod](https://zod.dev/). Pick whichever you prefer -- they're separate imports so you only bundle what you use. ## Installation ```bash npm install @strata-js/middleware-payload-validation ``` Then install the validation library you intend to use: **For AJV (JSON Schema):** ```bash npm install ajv ajv-formats ajv-errors ajv-keywords ``` **For Zod (supports v3 and v4):** ```bash npm install zod ``` Peer dependency: `@strata-js/strata ^2.0.0` ## Usage ### AJV (JSON Schema) Import from `@strata-js/middleware-payload-validation/ajv`: ```typescript import { StrataContext } from '@strata-js/strata'; import { AjvPayloadValidationMiddleware } from '@strata-js/middleware-payload-validation/ajv'; const users = new StrataContext('users'); const schemas = { create: { type: 'object', required: [ 'email', 'name' ], properties: { email: { type: 'string', format: 'email' }, name: { type: 'string', minLength: 1, maxLength: 100 }, age: { type: 'integer', minimum: 0, maximum: 150 }, }, additionalProperties: false, }, update: { type: 'object', required: [ 'userId' ], properties: { userId: { type: 'string' }, name: { type: 'string', minLength: 1, maxLength: 100 }, }, additionalProperties: false, }, }; const validator = new AjvPayloadValidationMiddleware(schemas); // Register as context-level middleware -- validates every operation in 'users' users.useMiddleware(validator); users.registerOperation('create', async (request) => { // Payload is guaranteed to match the 'create' schema here return createUser(request.payload); }); users.registerOperation('update', async (request) => { return updateUser(request.payload); }); ``` The schema config is a record where each key matches an operation name and each value is a JSON Schema object. When a request comes in, the middleware looks up the schema by `request.operation` and validates `request.payload` against it. ### Zod Import from `@strata-js/middleware-payload-validation/zod`: ```typescript import { StrataContext } from '@strata-js/strata'; import { ZodPayloadValidationMiddleware } from '@strata-js/middleware-payload-validation/zod'; import { z } from 'zod'; const users = new StrataContext('users'); const schemas = { create: z.object({ email: z.string().email(), name: z.string().min(1).max(100), age: z.number().int().min(0).max(150).optional(), }).strict(), update: z.object({ userId: z.string(), name: z.string().min(1).max(100).optional(), }).strict(), }; const validator = new ZodPayloadValidationMiddleware(schemas); users.useMiddleware(validator); users.registerOperation('create', async (request) => { return createUser(request.payload); }); ``` Both Zod v3 (>= 3.24.2) and Zod v4 are supported, including proper handling of union error formats in both versions. ## API ### `AjvPayloadValidationMiddleware` ```typescript new AjvPayloadValidationMiddleware( config : Record>, ajvOptions ?: AJVOptions, errorFormatter ?: (errors : DefinedError[]) => unknown ) ``` | Parameter | Type | Description | |-----------|------|-------------| | `config` | `Record>` | Map of operation name to JSON Schema. | | `ajvOptions` | `AJVOptions` | Custom [AJV options](https://ajv.js.org/options.html). `allErrors`, `$data`, and `discriminator` are always enabled. | | `errorFormatter` | `(errors : DefinedError[]) => unknown` | Custom formatter that receives raw AJV `DefinedError[]` and returns the error details to include in the `ValidationError`. | #### AJV Plugins The following plugins are automatically installed on every AJV instance: | Plugin | npm | Description | |--------|-----|-------------| | [ajv-formats](https://ajv.js.org/packages/ajv-formats.html) | `ajv-formats` | Format validation (`email`, `uri`, `date`, `uuid`, etc.) | | [ajv-errors](https://ajv.js.org/packages/ajv-errors.html) | `ajv-errors` | Custom error messages via the `errorMessage` keyword. | | [ajv-keywords](https://ajv.js.org/packages/ajv-keywords.html) | `ajv-keywords` | Additional validation keywords (`transform`, `uniqueItemProperties`, etc.) | All three are peer dependencies and must be installed alongside `ajv`. ### `ZodPayloadValidationMiddleware` ```typescript new ZodPayloadValidationMiddleware( schema : ZodPayloadValidationSchema, errorFormatter ?: (error : ZodError) => unknown ) ``` | Parameter | Type | Description | |-----------|------|-------------| | `schema` | `ZodPayloadValidationSchema` | Map of operation name to Zod schema. Each value must be a `ZodType`. | | `errorFormatter` | `(error : ZodError) => unknown` | Custom formatter that receives the raw `ZodError` and returns the error details to include in the `ValidationError`. | ## Custom Error Formatters Both middleware classes accept an optional error formatter. Without one, the middleware uses its built-in error parsing. With one, you control exactly what ends up in the `ValidationError.details` field. ### AJV Error Formatter Receives the raw array of AJV `DefinedError` objects: ```typescript import type { DefinedError } from 'ajv'; const validator = new AjvPayloadValidationMiddleware( schemas, {}, // AJV options (defaults) (errors : DefinedError[]) => { // Return whatever shape you want for error details return errors.map((e) => ({ field: e.instancePath, message: e.message, })); } ); ``` Without a custom formatter, validation errors are parsed into an array of human-readable strings using the built-in error parser (e.g., `"email must match format \"email\""`, `"must have required property 'name'"`). ### Zod Error Formatter Receives the full `ZodError` object: ```typescript import type { ZodError } from 'zod'; const validator = new ZodPayloadValidationMiddleware( schemas, (error : ZodError) => { return error.issues.map((issue) => ({ path: issue.path.join('.'), message: issue.message, })); } ); ``` Without a custom formatter, Zod errors are parsed into a structured `ZodErrorTree`: ```typescript interface ZodErrorTree { errors : string[]; properties ?: Record; items ?: (ZodErrorTree | null)[]; unionErrors ?: ZodErrorTree[]; } ``` This tree mirrors the structure of the validated data -- nested objects produce nested `properties`, arrays produce `items`, and discriminated unions produce `unionErrors`. ## Validation Failure Behavior When validation fails, the middleware: 1. **Adds a message** to `request.messages`: ```typescript { message: 'Schema validation failed. (See `details` property for more information.)', code: 'payload_validation_error', severity: 'error', type: 'validation_error', details: { validationErrors: /* parsed errors */ } } ``` 2. **Calls `request.fail()`** with a `ValidationError`. 3. **Returns the request** -- the operation handler is never called. ### `ValidationError` `ValidationError` extends `ServiceError` and includes: | Property | Value | |----------|-------| | `code` | `'VALIDATION_ERROR'` | | `message` | `'Schema validation failed. (See details property for more information.)'` | | `details` | The parsed validation errors (or the return value of your custom error formatter). | ## Operations Without Schemas If a request arrives for an operation that has no entry in the schema config, the middleware passes the request through without validation. A debug-level log message is emitted: ``` operation users.delete has no validation schema defined. ``` This lets you use the middleware at the context level even if not every operation needs validation. ## Deprecated Export The default import path re-exports `AjvPayloadValidationMiddleware` as `PayloadValidationMiddleware` for backwards compatibility. This will be removed in a future release. ```typescript // Deprecated -- will be removed import { PayloadValidationMiddleware } from '@strata-js/middleware-payload-validation'; // Use these instead import { AjvPayloadValidationMiddleware } from '@strata-js/middleware-payload-validation/ajv'; import { ZodPayloadValidationMiddleware } from '@strata-js/middleware-payload-validation/zod'; ``` ## Examples ### AJV with Custom Options Enable type coercion so string `"42"` is accepted for integer fields: ```typescript const validator = new AjvPayloadValidationMiddleware( schemas, { coerceTypes: true } ); ``` ### AJV with Custom Error Messages Use the `errorMessage` keyword from `ajv-errors` to produce friendlier errors: ```typescript const schemas = { create: { type: 'object', required: [ 'email', 'name' ], properties: { email: { type: 'string', format: 'email' }, name: { type: 'string', minLength: 1 }, }, additionalProperties: false, errorMessage: { required: { email: 'Email address is required', name: 'Name is required', }, properties: { email: 'Must be a valid email address', }, }, }, }; const validator = new AjvPayloadValidationMiddleware(schemas); ``` ### Zod with Refinements ```typescript import { z } from 'zod'; const schemas = { transferFunds: z.object({ fromAccount: z.string(), toAccount: z.string(), amount: z.number().positive(), }).refine( (data) => data.fromAccount !== data.toAccount, { message: 'Cannot transfer to the same account' } ), }; const validator = new ZodPayloadValidationMiddleware(schemas); ``` ### Operation-Level Registration Register validation on a single operation instead of the whole context: ```typescript const users = new StrataContext('users'); const validator = new AjvPayloadValidationMiddleware({ create: { type: 'object', required: [ 'email' ], properties: { email: { type: 'string', format: 'email' }, }, }, }); users.registerOperation('create', async (request) => { return createUser(request.payload); }, [], [ validator ]); ``` --- --- url: /tools/queue-monitor.md description: >- A Strata service that captures all messages and writes them to Elasticsearch, Logstash, or the console for observability. --- # Queue Monitor The Strata Queue Monitor (SQM) is a Strata service that captures messages flowing through your system and writes them to one or more output backends -- Elasticsearch, Logstash, or the console. It gives you full visibility into every request and response, making it an essential debugging and observability tool. SQM is safe to run in production. It processes messages on its own queue, scales like any other Strata service, and has no impact on the critical path of your services. ## How It Works SQM is a normal Strata service. It listens on the `StrataQueueMonitor` service group and exposes a single operation: `monitor.logMessage`. This operation accepts a `queue` and an `envelope` (a Strata request or response envelope) and writes it to whichever output plugins are enabled. Because SQM is just a service, it gets the same guarantees as everything else in Strata -- message acknowledgement, replay on crash, configurable queue length, and horizontal scaling. ### Sending Messages to SQM You *can* send messages to SQM manually, but the intended approach is the [message-logging middleware](/middleware/message-logging). Add it to any service and it automatically forwards every incoming request and outgoing response to SQM without blocking the service's normal processing. The middleware sends messages fire-and-forget. If SQM is down, a warning is logged but service processing continues unaffected. ## Installation SQM is deployed as a Docker container. It is not published as an npm package. ```bash docker run -d --name sqm \ -v /path/to/config/config.yml:/app/config/config.yml \ stratajs/strata-queue-monitor:latest ``` Or in a `compose.yaml`: ```yaml services: sqm: image: stratajs/strata-queue-monitor:latest restart: unless-stopped volumes: - ./config:/app/config:ro ``` ```bash docker compose up -d ``` ## Configuration SQM uses a `config.yml` file. Copy the default from the Docker image as a starting point: ```bash docker cp sqm:/app/config/config.yml ./config.yml ``` The config file supports environment variable substitution using `${ MY_VAR }` syntax. Variables are replaced before YAML parsing, so they are substituted verbatim. ### Service and Backend ```yaml service: serviceGroup: 'StrataQueueMonitor' backend: type: 'redis' redis: host: 'localhost' port: 6379 # username: ${ REDIS_USERNAME } # password: ${ REDIS_PASSWORD } ``` The `backend` section is a standard Strata backend config. The `redis` key is passed directly to ioredis. ### Processing ```yaml processing: auth: # Decode JWT auth tokens into a separate key in the logged message decode: 'jwt' # Key name for the decoded token (default: 'authDecode') key: 'token' # Replace the original auth value with 'SCRUBBED' scrub: true ``` The `processing.auth` section controls how the `auth` field on requests is handled before writing to the output. This is useful for decoding JWTs into readable form or scrubbing tokens from logs. ### Logging ```yaml logging: level: 'info' # trace | debug | info | warn | error | critical prettyPrint: false # Enable pino-pretty (development only) ``` ### Output Plugins SQM supports multiple output plugins. Enable whichever ones you need -- multiple can be active at the same time. ```yaml outputConfigs: # Elasticsearch (direct) elastic: enabled: true forceRefresh: false indexPrefix: 'strata-queue-monitor' bulkConfig: enabled: true maxMessages: 20 flushSize: 1000000 # bytes flushInterval: 3000 # ms concurrency: 4 clientOptions: nodes: - http://localhost:9200 # auth: # username: ${ ELASTIC_USERNAME } # password: ${ ELASTIC_PASSWORD } # Logstash (HTTP input) logstashHTTP: enabled: false url: http://localhost:5555 # Console (stdout) console: enabled: false # No-op (discard all messages) none: enabled: false ``` #### Elasticsearch Plugin | Option | Type | Description | |--------|------|-------------| | `enabled` | `boolean` | Enable this plugin. | | `forceRefresh` | `boolean` | Force Elasticsearch to refresh the index after each insert. Useful for development, not recommended in production. | | `indexPrefix` | `string` | Prefix for the Elasticsearch index name. Indices are created as `-YYYY.MM.DD`. | | `bulkConfig.enabled` | `boolean` | Use the bulk API for writes. Significantly improves throughput. | | `bulkConfig.maxMessages` | `number` | Number of messages to buffer before flushing. | | `bulkConfig.flushSize` | `number` | Maximum buffer size in bytes before flushing. | | `bulkConfig.flushInterval` | `number` | Maximum time in ms to hold messages before flushing, even if `flushSize` is not reached. | | `bulkConfig.concurrency` | `number` | Concurrent bulk requests to Elasticsearch. | | `clientOptions` | `object` | Passed directly to `@elastic/elasticsearch`. Supports all options from that library. | #### Logstash HTTP Plugin | Option | Type | Description | |--------|------|-------------| | `enabled` | `boolean` | Enable this plugin. | | `url` | `string` | URL of the Logstash HTTP input endpoint. | #### Console Plugin Writes messages to stdout. Useful for development or piping to external log aggregators. #### None Plugin Discards all messages. Useful if you want SQM running (consuming from the queue) but don't need to store anything. ## Kibana Setup Once messages are flowing to Elasticsearch, connect Kibana to view them: 1. Open Kibana (default: `http://localhost:5601`). 2. Navigate to **Stack Management** > **Index Patterns**. 3. Create an index pattern: `strata-queue-monitor-*`. 4. Go to **Discover**, select the `strata-queue-monitor-*` pattern, and browse your messages. Each document contains fields like `context`, `operation`, `status`, `request`, `response`, timestamps, and client/ service identifiers. The full shape is documented in the source under `StrataQueueMonitorMessage`. ## Development To develop SQM locally, you need an Elasticsearch cluster and optionally Kibana and Logstash. The repository includes a compose file that sets up a 3-node Elasticsearch cluster, Kibana, and Logstash: ```bash docker compose -p strata-dev -f compose/dev-env.yml up -d ``` This starts: * **Elasticsearch** on `http://localhost:9200` (3 nodes) * **Kibana** on `http://localhost:5601` * **Logstash** on `http://localhost:5555` (HTTP input) and `http://localhost:9600` (monitoring) Then run SQM locally: ```bash npm install npm start ``` The default `config.yml` is already configured to connect to the local Elasticsearch cluster. Enable the `console` output plugin for quick feedback during development. --- --- url: /getting-started/quick-start.md description: >- Get a Strata service running in 5 minutes with Redis and a minimal code example. --- # Quick Start Get a Strata service running in 5 minutes. ## Prerequisites * [Node.js](https://nodejs.org/) v18 or later * A running [Redis](https://redis.io/) instance (default: `localhost:6379`) If you don't have Redis running locally, the fastest way to get it going: ```bash # Docker docker run -d -p 6379:6379 redis # macOS brew install redis && brew services start redis ``` ## Install Create a new project and install Strata: ```bash mkdir my-service && cd my-service npm init -y npm install @strata-js/strata ``` ## Create a Service Create `service.ts` with a single context and one operation: ```typescript import { StrataService, Context } from '@strata-js/strata'; // Create the service const service = new StrataService({ service: { serviceGroup: 'QuickStart' }, backend: { type: 'redis', redis: { host: 'localhost', port: 6379 } }, }); // Create a context with an echo operation const echo = new Context('echo'); echo.registerOperation('send', async (request) => { return { message: `You said: ${ request.payload.text }` }; }); // Register the context and start service.registerContext(echo); await service.start(); ``` That's all the service needs. It listens on the `QuickStart` service group, routes requests to the `echo` context, and the `send` operation returns whatever text you give it. ## Create a Client In a separate file, create `client.ts`: ```typescript import { StrataClient } from '@strata-js/strata'; const client = new StrataClient({ client: { name: 'QuickStartClient' }, backend: { type: 'redis', redis: { host: 'localhost', port: 6379 } }, }); await client.start(); // Send a request to the service const response = await client.request('QuickStart', 'echo', 'send', { text: 'Hello, Strata!', }); console.log(response.payload); // { message: 'You said: Hello, Strata!' } process.exit(0); ``` The client connects to the same Redis backend, sends a request to the `QuickStart` service group targeting the `echo` context and `send` operation, and prints the response payload. ## Run It Start the service in one terminal: ```bash npx tsx service.ts ``` Send a request from another: ```bash npx tsx client.ts ``` You should see `{ message: 'You said: Hello, Strata!' }` printed in the client terminal. ## What Just Happened 1. The **service** connected to Redis and started listening on the `Requests:QuickStart` queue. 2. The **client** pushed a request envelope onto that queue with context `echo` and operation `send`. 3. The service popped the message, routed it to the `echo` context's `send` handler, and pushed the response back to the client's dedicated response queue. 4. The client received the response and printed the payload. All of the envelope construction, queue management, routing, and response matching happened automatically. You wrote a handler function and a request call -- Strata handled the rest. ## Next Steps * [Your First Service](./your-first-service) -- build a more realistic service with multiple contexts, middleware, configuration files, and service-to-service calls. * [Configuration](./configuration) -- understand every option in the service and client config. * [Architecture](../concepts/architecture) -- how services, contexts, and operations fit together. --- --- url: /core-api/request.md description: >- API reference for the StrataRequest class, which carries request data, response data, and lifecycle methods. --- # Request (StrataRequest) The `StrataRequest` class represents a request throughout its entire lifecycle -- from the moment it arrives to when the response is sent. It holds both the incoming request data and the outgoing response data. This is the object your operation handlers receive, and what middleware operates on. ```typescript import { StrataRequest } from '@strata-js/strata'; // Also exported as `Request` for convenience import { Request } from '@strata-js/strata'; ``` ## Type Parameters ```typescript class StrataRequest< PayloadType = Record, MetadataType = Record, ResponsePayloadType = Record > ``` All three type parameters default to `Record`. ## Request Properties These are set when the request is created and are read-only. | Property | Type | Description | |---------------------|-------------------------------|--------------------------------------------------------------------| | `id` | `string` | Unique request identifier. | | `context` | `string` | Target context name. | | `operation` | `string` | Target operation name. | | `payload` | `PayloadType` | Application-specific request data. | | `metadata` | `MetadataType` | Additional metadata (tracking, stats, etc.). | | `auth` | `string \| undefined` | Authentication data (typically a JWT). | | `client` | `string` | Human-readable client identifier. | | `messageType` | `'request' \| 'post'` | Whether this is a request (expects response) or a post (fire-and-forget). | | `timestamp` | `string` | ISO 8601 datetime of when the request was created. | | `receivedTimestamp` | `number` | `Date.now()` when the request was received by the service. | | `responseQueue` | `string \| undefined` | Queue for sending the response (set by the backend). Only on requests, not posts. | | `timeout` | `number \| undefined` | Timeout in milliseconds. Only on requests, not posts. | | `priorRequest` | `string \| undefined` | ID of the request that triggered this one. | | `requestChain` | `string[]` | Chain of request IDs for distributed tracing. | ## Response Properties These are populated as the request is processed. | Property | Type | Description | |----------------------|-------------------------------------------------|--------------------------------------------------| | `status` | `'pending' \| 'succeeded' \| 'failed'` | Current request status. | | `response` | `ResponsePayloadType \| undefined` | The response payload, set by `succeed()` or `fail()`. | | `completedTimestamp` | `number \| undefined` | `Date.now()` when the request was completed. | | `service` | `string \| undefined` | Identifier of the service that processed this. | | `messages` | `ResponseMessage[]` | Structured messages generated during processing. | ## Internal Properties | Property | Type | Description | |-----------|------------------|------------------------------------------------------------------------| | `promise` | `Promise` | Resolves when the request is succeeded or failed. Used internally for tracking. | ## Methods ### `succeed()` ```typescript request.succeed(payload : ResponsePayloadType) : void ``` Marks the request as succeeded with the given payload. Sets `status` to `'succeeded'`, records the `completedTimestamp`, and resolves the internal promise. ```typescript // In an operation handler context.registerOperation('get', async (request) => { const user = await db.findUser(request.payload.userId); request.succeed({ user }); }); ``` ::: tip If your handler returns a value without calling `succeed()` or `fail()`, Strata automatically calls `succeed()` with the return value. ::: ### `fail()` ```typescript request.fail(reason : string | Error | ResponsePayloadType) : void ``` Marks the request as failed. Sets `status` to `'failed'`, records the `completedTimestamp`, and resolves the internal promise. The `reason` determines the response payload: | Reason Type | Behavior | |-----------------|---------------------------------------------------------------------------------------------| | `ServiceError` | Serialized via `toJSON()` -- includes `name`, `message`, `code`, `isSafeMessage`, `stack`. | | `Error` | Wrapped into `{ message, name: 'FailedRequestError', stack, code, details, isSafeMessage }`. | | `string` | Wrapped into `{ message: reason, name: 'FailedRequestError', code: 'FAILED_REQUEST' }`. | | `object` | Used directly as the response payload. | ```typescript // Fail with a string request.fail('User not found'); // Fail with an Error request.fail(new Error('Database connection lost')); // Fail with a ServiceError (includes isSafeMessage for client-visible errors) import { errors } from '@strata-js/strata'; request.fail(new errors.ServiceError('User not found', 'USER_NOT_FOUND')); // Fail with a custom payload request.fail({ message: 'Validation failed', errors: [ 'name is required' ] }); ``` ### `renderRequest()` ```typescript request.renderRequest() : RequestEnvelope ``` Serializes the request into its wire format -- a plain `RequestEnvelope` object with no methods. Useful for logging, debugging, or creating clones. ```typescript const envelope = request.renderRequest(); console.log(JSON.stringify(envelope, null, 2)); ``` ### `renderResponse()` ```typescript request.renderResponse() : ResponseEnvelope ``` Serializes the response into its wire format -- a plain `ResponseEnvelope` object with no methods. ```typescript const envelope = request.renderResponse(); console.log(envelope.status); // 'succeeded' or 'failed' console.log(envelope.payload); // the response payload console.log(envelope.messages); // any ResponseMessage objects ``` ### `parseResponse()` ```typescript request.parseResponse(response : ResponseEnvelope) : void ``` Populates this request's response properties from a received `ResponseEnvelope`. Used internally by the client when a response arrives over the wire. Calls `succeed()` or `fail()` based on the envelope's `status`. ## Working with Messages The `messages` array on a request holds `ResponseMessage` objects. You can push messages onto this array during processing to include warnings, info, or debug details in the response -- even on successful requests. ```typescript context.registerOperation('create', async (request) => { const user = await db.createUser(request.payload); if(user.emailUnverified) { request.messages.push({ severity: 'warning', message: 'Email address has not been verified.', code: 'EMAIL_UNVERIFIED', }); } return { user }; }); ``` See [ResponseMessage](/core-api/envelope#responsemessage) for the full interface. ## Lifecycle 1. A request arrives and a `StrataRequest` is constructed with `status: 'pending'`. 2. Global `beforeRequest()` middleware runs. 3. Context `beforeRequest()` middleware runs. 4. Operation `beforeRequest()` middleware runs. 5. If still `'pending'`, the operation handler executes. 6. If the handler returns a value and the request is still `'pending'`, `succeed()` is called with the return value. 7. If the request succeeded, `success()` middleware runs (operation -> context -> global). 8. If the request failed, `failure()` middleware runs (operation -> context -> global). 9. The response is rendered and sent back via the backend. At any point, middleware can short-circuit by calling `request.succeed()` or `request.fail()` directly. See [Middleware Model](/concepts/middleware-model) for details. --- --- url: /tools/rpc-bridge.md description: >- HTTP and WebSocket bridge that exposes Strata services to browsers, mobile apps, and external consumers. --- # RPC Bridge The RPC Bridge exposes Strata services over HTTP and WebSockets. It sits between external consumers (browsers, mobile apps, third-party systems) and your Strata backend, translating HTTP POST requests and Socket.IO messages into Strata service calls. The bridge is split into two packages: * **`@strata-js/rpcbridge`** -- the server that accepts HTTP/WS requests and forwards them to Strata services. * **`@strata-js/rpcbridge-client`** -- a lightweight browser/Node client for calling services through the bridge via WebSocket. ## Installation ```bash # Server npm install @strata-js/rpcbridge # Client (browser or Node) npm install @strata-js/rpcbridge-client ``` ## Server ### Standalone Usage The simplest way to run the bridge is to let it create its own Express and Socket.IO server: ```typescript import { RPCBridgeServer } from '@strata-js/rpcbridge'; const config = { server: { enableHTTP: true, enableWS: true, port: 3000, path: '/api', excludeStack: true, excludeUnsafeMessages: true, }, strata: { backend: { type: 'redis', redis: { host: 'localhost', port: 6379 } }, client: { name: 'RPCBridge' }, }, globalBlockList: [ 'service:*' ], services: { users: { serviceGroup: 'UserService', blockList: [ 'admin:*' ], }, orders: { serviceGroup: 'OrderService', allowList: [ 'cart:*', 'checkout:*' ], }, }, }; const bridge = new RPCBridgeServer(config); bridge.startListening(); ``` ### Bring Your Own Server If you already have an Express app and want to mount the bridge alongside your own routes: ```typescript import http from 'http'; import express from 'express'; import { Server as SIOServer } from 'socket.io'; import { RPCBridgeServer } from '@strata-js/rpcbridge'; const app = express(); const server = http.createServer(app); const io = new SIOServer(server); // Your own routes app.get('/health', (_req, res) => { res.send('ok'); }); // Mount the bridge -- pass your server instances as the third argument new RPCBridgeServer(config, undefined, { expressApp: app, httpServer: server, ioServer: io, }); // You manage the listener server.listen(3000); ``` When providing external server instances, calling `startListening()` is unnecessary -- you control the listener yourself. ### Configuration #### `ServerConfig` The top-level config object passed to `RPCBridgeServer`: ```typescript interface ServerConfig { server : WebServerConfig; strata : StrataClientConfig; globalBlockList : string[]; services : ServiceConfig; } ``` #### `WebServerConfig` ```typescript interface WebServerConfig { enableHTTP : boolean; // Enable the HTTP POST endpoint enableWS : boolean; // Enable the WebSocket endpoint port : number; // Port to listen on (standalone mode) path : string; // Base path for both endpoints (e.g. '/api') excludeStack : boolean; // Strip stack traces from error responses excludeUnsafeMessages : boolean; // Replace non-safe error messages with a generic string allowServiceGroupOverride ?: boolean; // Allow callers to specify serviceGroup directly } ``` #### `ServiceConfig` A map of service names to their configuration. The key is the `serviceName` callers use in requests: ```typescript type ServiceConfig = Record; interface ServiceEntry { serviceGroup : string; // The Strata service group to forward requests to blockList ?: string[]; // Operations to deny (e.g. [ 'admin:*' ]) allowList ?: string[]; // Operations to allow (takes precedence over blockList) } ``` ### Block and Allow Lists Each service entry can have a `blockList` or `allowList`. Entries follow the `context:operation` pattern. Use `*` as the operation to match all operations in a context. There is also a `globalBlockList` on the top-level config that applies to **every** service. Evaluation order: 1. **`globalBlockList`** is checked first. If the call matches, it is denied. 2. If the service has an **`allowList`**, only calls matching the list are permitted. Everything else is denied. 3. If the service has a **`blockList`** (and no `allowList`), calls matching the list are denied. Everything else is permitted. 4. `allowList` takes precedence -- if both are defined, `blockList` is ignored. ```typescript services: { users: { serviceGroup: 'UserService', // Only these operations are reachable through the bridge allowList: [ 'profile:get', 'profile:update' ], }, admin: { serviceGroup: 'AdminService', // Everything is reachable except these blockList: [ 'system:shutdown', 'system:reset' ], }, }, ``` ### HTTP Endpoint When `enableHTTP` is `true`, the bridge mounts a `POST` route at the configured `path`. Send a JSON body with the following shape: ```json { "serviceName": "users", "context": "profile", "operation": "get", "payload": { "userId": "abc123" }, "meta": {}, "auth": "Bearer ...", "timeout": 5000 } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `serviceName` | `string` | yes\* | Key from the `services` config map. | | `serviceGroup` | `string` | yes\* | Direct service group name (requires `allowServiceGroupOverride`). | | `context` | `string` | yes | Target context name. | | `operation` | `string` | yes | Target operation name. | | `payload` | `object` | yes | Request payload. | | `meta` | `object` | no | Metadata passed through to the service. | | `auth` | `string` | no | Authentication token. | | `timeout` | `number` | no | Request timeout in milliseconds. | \*Either `serviceName` or `serviceGroup` must be provided, not both. On success the response body is the service's response payload. On failure the response is a `400` with an error object. ### WebSocket Endpoint When `enableWS` is `true`, the bridge listens on a Socket.IO namespace at the configured `path`. Clients emit an `rpc` event with the same request shape as the HTTP endpoint and receive the response via a Socket.IO acknowledgement callback. The response is an `RPCResponse` object: ```typescript interface RPCResponse { status : 'success' | 'failed'; response : unknown; } ``` ### Middleware You can inject middleware for both HTTP and WebSocket endpoints by passing a `MiddlewareConfig` as the second constructor argument: ```typescript import type { HTTPMiddleware, SocketMiddleware, MiddlewareConfig } from '@strata-js/rpcbridge'; const httpLogger : HTTPMiddleware = (req, _res, next) => { console.log('HTTP request:', req.body); next(); }; const wsAuth : SocketMiddleware = (_socket, rpcRequest, next) => { rpcRequest.auth = 'injected-token'; next(); }; const middleware : MiddlewareConfig = { http: [ httpLogger ], socket: [ wsAuth ], }; new RPCBridgeServer(config, middleware); ``` **HTTP middleware** follows the standard Express signature `(req, res, next)`. Middleware runs before the request is forwarded to Strata. **WebSocket middleware** receives `(socket, rpcRequest, next)`. Call `next()` to continue or `next('error message')` to reject the request. ```typescript type HTTPMiddleware = (req : Request, res : Response, next : () => void) => void; type SocketMiddleware = (socket : Socket, rpcRequest : RPCRequest, next : (errMsg ?: string) => void) => void; interface MiddlewareConfig { http ?: HTTPMiddleware[]; socket ?: SocketMiddleware[]; } ``` ## Client The client package provides a WebSocket-based client for calling Strata services through the RPC Bridge. ### Basic Usage ```typescript import { WebSocketClient } from '@strata-js/rpcbridge-client'; const client = new WebSocketClient('http://localhost:3000/api'); const user = await client.request( 'users', 'profile', 'get', { userId: 'abc123' } ); console.log(user); ``` ### Constructor ```typescript new WebSocketClient(uri : string, opts ?: ClientConstructorOptions) ``` | Parameter | Type | Description | |-----------|------|-------------| | `uri` | `string` | The bridge server URL including the path (e.g. `http://localhost:3000/api`). | | `opts` | `ClientConstructorOptions` | Socket.IO client options plus an optional `emitTimeout`. Defaults to `{ transports: [ 'websocket' ], reconnection: true, emitTimeout: 20000 }`. | ### `request()` ```typescript client.request( serviceName : string, context : string, operation : string, payload : Record, meta ?: Record, auth ?: string, timeout ?: number ) : Promise ``` Sends a request through the bridge and returns the service's response payload. Throws `RemoteServiceError` if the service returned a failure, or `EmitTimeoutError` if the request times out. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `serviceName` | `string` | yes | The service name as configured in the bridge's `services` map. | | `context` | `string` | yes | Target context. | | `operation` | `string` | yes | Target operation. | | `payload` | `Record` | yes | Request payload. | | `meta` | `Record` | no | Metadata. | | `auth` | `string` | no | Authentication token. | | `timeout` | `number` | no | Override the default `emitTimeout`. | ### Error Handling The client throws typed errors: * **`RemoteServiceError`** -- the bridge returned a `'failed'` status. Contains `code`, `message`, `name`, `stack`, and optional `details` from the remote service. * **`EmitTimeoutError`** -- the Socket.IO emit timed out waiting for an acknowledgement. ```typescript import { RemoteServiceError, EmitTimeoutError } from '@strata-js/rpcbridge-client'; try { await client.request('users', 'profile', 'get', { userId: 'bad' }); } catch(err) { if(err instanceof RemoteServiceError) { console.error(`Service error [${ err.code }]: ${ err.message }`); } else if(err instanceof EmitTimeoutError) { console.error('Request timed out'); } } ``` ### Accessing the Socket If you need lower-level access to the Socket.IO client (for custom events, connection state, etc.): ```typescript const socket = client.socket; socket.on('connect', () => { console.log('Connected'); }); socket.on('disconnect', () => { console.log('Disconnected'); }); ``` ## Examples ### Full Stack Example A minimal setup with a Strata service, the RPC Bridge, and a browser client calling through it: **Service** (`service.ts`): ```typescript import { StrataService, Context } from '@strata-js/strata'; const service = new StrataService({ service: { serviceGroup: 'GreeterService' }, backend: { type: 'redis', redis: { host: 'localhost', port: 6379 } }, }); const greeter = new Context('greeter'); greeter.registerOperation('hello', async (request) => { return { message: `Hello, ${ request.payload.name }!` }; }); service.registerContext(greeter); await service.start(); ``` **Bridge** (`bridge.ts`): ```typescript import { RPCBridgeServer } from '@strata-js/rpcbridge'; const bridge = new RPCBridgeServer({ server: { enableHTTP: true, enableWS: true, port: 3000, path: '/rpc', excludeStack: true, excludeUnsafeMessages: false, }, strata: { backend: { type: 'redis', redis: { host: 'localhost', port: 6379 } }, client: { name: 'RPCBridge' }, }, globalBlockList: [], services: { greeter: { serviceGroup: 'GreeterService' }, }, }); bridge.startListening(); ``` **Browser client**: ```typescript import { WebSocketClient } from '@strata-js/rpcbridge-client'; const client = new WebSocketClient('http://localhost:3000/rpc'); const result = await client.request<{ message : string }>( 'greeter', 'greeter', 'hello', { name: 'World' } ); console.log(result.message); // 'Hello, World!' ``` ### HTTP with cURL ```bash curl -X POST http://localhost:3000/rpc \ -H 'Content-Type: application/json' \ -d '{ "serviceName": "greeter", "context": "greeter", "operation": "hello", "payload": { "name": "cURL" } }' ``` --- --- url: /guides/service-commands.md description: >- How to send runtime commands to running services for concurrency tuning, graceful shutdown, and service inspection. --- # Service Commands Service commands let you control running Strata services at runtime without restarting them. You can adjust concurrency, trigger graceful shutdowns, query service info, and tune event loop settings -- all by sending a message over the same backend your services already use. Commands use a pub/sub model, not request/response. They are best-effort delivery: the backend does not retry if a service is unavailable, and delivery is not guaranteed. For the Redis backends, commands are sent via Redis pub/sub. ## How Commands Work Commands are lightweight JSON messages published to a target channel. Services subscribe to these channels on startup and handle incoming commands automatically. ### Command Targeting Commands can be sent at three levels of specificity: | Target Pattern | Scope | |----------------|-------| | `Services` | Every service connected to this backend | | `Services:` | Every instance in a specific service group | | `Services::` | A single service instance | This gives you fine-grained control. Broadcast a shutdown to an entire service group, or adjust concurrency on one specific instance. ### Command Envelope Every command is a JSON object with this structure: ```typescript interface ServiceCommand { id : string; // Unique command ID command : ValidCommandName; // 'info' | 'concurrency' | 'shutdown' | 'toobusy' payload ?: Record; // Command-specific data } ``` The `info` command adds a `responseChannel` field so the service knows where to send the reply: ```typescript interface StrataInfoCommand extends ServiceCommand { command : 'info'; responseChannel : string; // Channel for the response } ``` ## Built-in Commands Strata defines four built-in commands. These are the only valid command names (`ValidCommandName`). ### `info` Requests service information from any service that receives the command. This is the only command that returns a response -- the service sends back a `ServiceInfo` payload containing its ID, version, concurrency, registered contexts, and more. ```typescript const response = await client.command('info', 'Services:UserService'); console.log(response?.payload); // { // id: 'l7aNdfrRDZW8nt0FBaSS', // serviceName: 'User Service', // serviceGroup: 'UserService', // version: '1.2.0', // strataVersion: '2.0.0', // concurrency: 32, // outstanding: 3, // hostname: 'worker-01', // contexts: { service: ['info'], users: ['get', 'create'] }, // ... // } ``` ::: tip The `info` command targets a specific instance and gets a direct response. The `service/info` *operation* (available via `client.request()`) goes through the normal request queue and is handled by whichever instance picks it up first. Use the command when you need info from a specific instance; use the operation for general health checks. ::: ### `concurrency` Changes the maximum number of concurrent requests a service will process. Takes effect immediately. ```typescript // Set concurrency to 50 for all instances in UserService await client.command('concurrency', 'Services:UserService', { concurrency: 50 }); // Set concurrency on a specific instance await client.command('concurrency', 'Services:UserService:abc123', { concurrency: 10 }); ``` The payload requires a `concurrency` field with a number greater than or equal to 0. ### `shutdown` Initiates a shutdown of the target service(s). ```typescript // Graceful shutdown (default) -- finishes in-flight requests, then exits with code 0 await client.command('shutdown', 'Services:UserService'); // Graceful shutdown with a custom exit code await client.command('shutdown', 'Services:UserService', { graceful: true, exitCode: 0, }); // Immediate shutdown -- process exits immediately await client.command('shutdown', 'Services:UserService', { graceful: false, exitCode: 1, }); ``` | Payload Field | Type | Default | Description | |---------------|------|---------|-------------| | `graceful` | `boolean` | `true` | If `false`, calls `process.exit()` immediately | | `exitCode` | `number` | `0` | Exit code for the process | If the payload is omitted entirely, the service performs a graceful shutdown with exit code 0. ### `toobusy` Adjusts the `node-toobusy` parameters that control dynamic concurrency. This lets you tune how aggressively Strata backs off when the event loop is under load. ```typescript await client.command('toobusy', 'Services:UserService', { maxLag: 70, interval: 500, smoothingFactorOnRise: 0.33, smoothingFactorOnFall: 0.75, }); ``` | Payload Field | Type | Description | |---------------|------|-------------| | `maxLag` | `number` | Maximum event loop lag (ms) before the service is considered too busy | | `interval` | `number` | How often (ms) to check event loop lag | | `smoothingFactorOnRise` | `number` | Smoothing factor when lag is increasing (0-1) | | `smoothingFactorOnFall` | `number` | Smoothing factor when lag is decreasing (0-1) | All fields are optional. Only the fields you include will be updated. ## Sending Commands ### Via StrataClient The `client.command()` method is the primary way to send commands: ```typescript import { StrataClient } from '@strata-js/strata'; const client = new StrataClient({ client: { name: 'AdminClient' }, backend: { type: 'redis-streams', redis: { host: 'localhost', port: 6379 } }, }); await client.start(); // client.command(command, target?, payload?) await client.command('concurrency', 'Services:UserService', { concurrency: 25 }); ``` The method signature: ```typescript client.command( command : ValidCommandName, // 'info' | 'concurrency' | 'shutdown' | 'toobusy' target ?: string, // Default: 'Services' (all services) payload ?: Record ) : Promise ``` * **Returns a response** only for commands that produce one (currently just `info`). * **`target` defaults to `'Services'`** -- which broadcasts to every service on the backend. ### Directly via Redis Since commands are just pub/sub messages, you can send them from any Redis client without Strata: ```typescript import Redis from 'ioredis'; const redis = new Redis(); const command = { id: 'manual-cmd-001', command: 'concurrency', payload: { concurrency: 5 }, }; await redis.publish('Services:UserService', JSON.stringify(command)); ``` This is useful for ops tooling, scripts, or any situation where you don't want to instantiate a full `StrataClient`. ## Command Responses Only the `info` command returns a response. The response envelope is: ```typescript interface ServiceCommandResponse { id : string; // Matches the command's ID payload ?: Record; // Response data } ``` When you call `client.command('info', ...)`, Strata automatically sets up a `responseChannel` and waits for the reply. The response is returned directly from the `command()` call: ```typescript const response = await client.command('info', 'Services:UserService:abc123'); if(response) { console.log(`Service ${ response.payload.serviceGroup } is running v${ response.payload.version }`); } ``` For commands without responses (`concurrency`, `shutdown`, `toobusy`), the method returns `undefined`. ## Unknown Commands If a service receives a command it doesn't recognize, it logs a warning and ignores it. No error response is sent. There is currently no mechanism for registering custom command handlers on the service side -- the four built-in commands are the only ones supported. ## Next Steps * [Core API: Application](/core-api/application) -- service configuration and lifecycle. * [Core API: Client](/core-api/client) -- `client.command()` API reference. * [Backends](/core-api/backends) -- how commands are delivered on each backend. --- --- url: /tools/service-tools.md description: >- Web UI for discovering and calling Strata services with a JSON payload editor, saved environments, and auth support. --- # Service Tools Strata Service Tools (SST) is a web UI for calling Strata services. It connects to your Strata backend, discovers running services, and lets you invoke any operation with a JSON payload editor. It supports saved environments, authentication, and is useful for both development and production debugging. ## Installation SST is a standalone application. Clone the repository and install dependencies: ```bash git clone https://gitlab.com/stratajs/strata-service-tools.git cd strata-service-tools npm install ``` ## Setup ### Environment File Create a `.env` file in the project root. Use the provided `.env.sample` as a starting point: ```bash cp .env.sample .env ``` The default sample sets `ENVIRONMENT="local"`, which loads the local config file (no authentication required). ### Configuration SST uses YAML config files in the `config/` directory. The `ENVIRONMENT` variable in `.env` determines which config file is loaded (e.g. `local.yml`, `docker.yml`). A minimal local config: ```yaml logging: level: 'debug' prettyPrint: true http: secret: 'your-session-secret' key: 'sst_session' secure: false port: 9090 database: client: 'better-sqlite3' connection: filename: './db/sst.db' useNullAsDefault: true migrations: directory: './dist/server/knex/migrations' loadExtensions: [ '.js' ] seeds: directory: './dist/server/knex/seeds' loadExtensions: [ '.js' ] ``` ### Running **Development** (with hot module reloading): ```bash npm run dev ``` **Production**: ```bash npm run build npm start ``` SST will be available at `http://localhost:9090` (or whatever port you configured). ## Authentication Authentication is optional. For local development, leave the `auth` section commented out in your config and SST runs without requiring login. ### Providers SST supports two authentication providers via [Passport.js](http://www.passportjs.org/): #### GitLab Works with GitLab.com or self-hosted instances. 1. In your GitLab instance, go to **User Settings** > **Applications** > **New Application**. 2. Set the **Redirect URI** to your SST callback URL (e.g. `http://localhost:9090/auth/callback`). 3. Select the `read_user` scope. 4. Save and note the **Client ID** and **Client Secret**. ```yaml auth: strategy: 'gitlab' options: clientID: '' clientSecret: '' baseURL: 'https://gitlab.com' # Or your self-hosted instance URL ``` #### OpenID Connect Works with any OpenID Connect provider (Auth0, Okta, KeyCloak, etc.). ```yaml auth: strategy: 'openid' options: clientID: '' clientSecret: '' issuer: 'https://your-provider.com/auth/realms/your-realm' redirectURI: 'http://localhost:9090/auth/callback' scope: 'openid profile email groups' ``` ### Admin Access Admin access is used for managing system-level environments (shared across all users). There are three ways to grant admin access: 1. **GitLab admin** -- if using the GitLab provider, GitLab admins are automatically SST admins. 2. **OpenID group** -- request the `groups` scope and set `adminGroupName` in your provider options. Users in that group are admins. 3. **Hardcoded list** -- add email addresses to the `admins` list in config: ```yaml auth: strategy: 'openid' options: # ... provider options ... admins: - admin@example.com - ops@example.com ``` When running locally without authentication, you are automatically treated as a local admin. ## Service Discovery SST uses Strata's built-in service discovery to find running services. When you open the UI, it queries the backend for all registered service groups and their available contexts and operations. You can select a service, pick an operation, and fire off a request with a custom payload. ## Configuration Reference ### HTTP | Option | Type | Description | |--------|------|-------------| | `secret` | `string` | Session secret for cookie signing. | | `key` | `string` | Session cookie name. | | `secure` | `boolean` | Set the `secure` flag on session cookies (requires HTTPS). | | `port` | `number` | Port to listen on. | ### Database SST uses SQLite (via `better-sqlite3`) for storing user sessions and saved environments. The `database` section is passed directly to [Knex](https://knexjs.org/). ### Logging | Option | Type | Description | |--------|------|-------------| | `level` | `string` | Log level: `trace`, `debug`, `info`, `warn`, `error`, `critical`. | | `prettyPrint` | `boolean` | Enable `pino-pretty` for readable logs (development only). | ## Examples ### Calling a Service After starting SST and opening it in a browser: 1. Select a service group from the discovery panel. 2. Choose a context and operation. 3. Enter a JSON payload in the editor. 4. Click **Send** and view the response. This is equivalent to: ```typescript import { StrataClient } from '@strata-js/strata'; const client = new StrataClient({ client: { name: 'SST' }, backend: { type: 'redis', redis: { host: 'localhost', port: 6379 } }, }); await client.start(); const response = await client.request('UserService', 'users', 'get', { userId: '12345', }); ``` SST just gives you a browser UI for doing the same thing without writing code. ### Docker Deployment For production, use the Docker config: ```yaml # docker.yml logging: level: 'info' prettyPrint: false auth: strategy: 'gitlab' options: baseURL: $GITLAB_URL clientID: $GITLAB_CLIENT_ID clientSecret: $GITLAB_CLIENT_SECRET callbackURL: $CALLBACK_BASE_URL http: secret: '$HTTP_SECRET' key: 'sst_session' secure: false port: 9090 database: client: 'better-sqlite3' connection: filename: './db/sst.db' useNullAsDefault: true migrations: directory: './dist/server/knex/migrations' loadExtensions: [ '.js' ] seeds: directory: './dist/server/knex/seeds' loadExtensions: [ '.js' ] ``` Set the corresponding environment variables (`GITLAB_URL`, `GITLAB_CLIENT_ID`, etc.) and run with `ENVIRONMENT=docker`. --- --- url: /concepts/the-protocol.md description: >- The language-agnostic JSON envelope protocol that defines request and response message structure on the wire. --- # The Protocol Strata's protocol is the contract between services and clients. It defines the shape of messages exchanged over the message queue -- JSON envelopes with a fixed structure. The official implementation is Node.js, but the protocol is language-agnostic. Any process that can push and pop JSON from the backend can participate in a Strata system. This is the key to interoperability. The protocol is the contract -- anyone can build a service or client in any language that speaks it. ## Request Envelope A request envelope is sent by a client to a service. It contains everything the service needs to route and process the request. | Field | Type | Required | Default | Description | |------------------|------------|:--------:|:-----------:|----------------------------------------------------------------------------------| | `id` | `string` | yes | -- | Unique identifier. Generated with nanoid (20 chars, alphanumeric). | | `messageType` | `string` | yes | -- | Always `'request'`. | | `context` | `string` | yes | -- | The context to route to (e.g. `'users'`). | | `operation` | `string` | yes | -- | The operation within the context (e.g. `'get'`). | | `responseQueue` | `string` | yes | -- | Where to send the response. Set by the backend before sending. | | `timestamp` | `string` | yes | -- | ISO 8601 datetime (e.g. `'2024-06-25T04:08:22.000Z'`). | | `timeout` | `number` | no | `0` | Milliseconds the client will wait for a response. `0` uses the service default. | | `payload` | `object` | yes | `{}` | Application-specific request data. Opaque to the framework. | | `metadata` | `object` | yes | `{}` | Additional metadata. Opaque to the framework. | | `auth` | `unknown` | no | -- | Authentication data (typically a JWT or token). Passed through, never inspected. | | `client` | `string` | yes | `'unknown'` | Human-readable client identifier (e.g. `'MyApp v2.1.0'`). | | `requestChain` | `string[]` | no | `[]` | Chain of request IDs for distributed tracing. | | `priorRequest` | `string` | no | -- | ID of the request that triggered this one. | ## Response Envelope A response envelope is sent by a service back to the client. It carries the result of processing, along with any messages generated during execution. | Field | Type | Required | Default | Description | |---------------|---------------------|:--------:|:-----------:|--------------------------------------------------------------------------| | `id` | `string` | yes | -- | Matches the request `id` this is responding to. | | `messageType` | `string` | yes | -- | Always `'response'`. | | `context` | `string` | yes | -- | Echoed from the request. | | `operation` | `string` | yes | -- | Echoed from the request. | | `timestamp` | `string` | yes | -- | ISO 8601 datetime of when the response was created. | | `payload` | `object` | yes | `{}` | Application-specific response data. | | `status` | `string` | yes | -- | `'succeeded'`, `'failed'`, or `'pending'`. | | `messages` | `ResponseMessage[]` | yes | `[]` | Array of messages generated during processing (errors, warnings, etc.). | | `service` | `string` | yes | `'unknown'` | Human-readable service identifier (e.g. `'UserService v1.0.0'`). | ## ResponseMessage Structured messages generated during request processing. Loosely modeled after the `Error` object but usable for any severity level. | Field | Type | Required | Default | Description | |------------|----------|:--------:|:--------------------------:|--------------------------------------------------------------| | `severity` | `string` | yes | -- | `'error'`, `'warning'`, `'info'`, or `'debug'`. | | `message` | `string` | yes | -- | Human-readable message text. | | `code` | `string` | no | -- | Machine-readable message code. | | `type` | `string` | no | `'StrataResponseMessage'` | Message type name (typically the error class name). | | `details` | `object` | no | `{}` | Additional structured details. | | `stack` | `string` | no | -- | Stack trace, if applicable. Never expose to end users. | ## Post Envelope A post is fire-and-forget. It has `messageType: 'post'` and the same structure as a request envelope, minus the `responseQueue` and `timeout` fields. The service processes it but sends no response. Useful for events, notifications, or any case where the sender doesn't need confirmation. ## Queue Naming (Redis Backend) The Redis backend uses a predictable naming convention for queues: | Queue | Key Format | Example | |--------------------|------------------------------------------|----------------------------------| | Request queue | `Requests:` | `Requests:UserService` | | Response queue | `Responses::` | `Responses:MyApp:a1b2c3d4e5f6` | | Response queue | `Responses:` | `Responses:a1b2c3d4e5f6` | If the client provides a name, the response queue includes it. If not, the queue key is just `Responses:`. ## The Flow Here's what happens when a client sends a request to a service using the Redis lists backend, step by step: 1. **Client builds a request envelope** with a nanoid `id`, the target `context` and `operation`, a `payload`, a `timestamp`, and the `client` identifier. 2. **Backend stamps `responseQueue`** on the envelope. The client's backend knows the response queue name and fills it in before sending. 3. **Client sends the request:** ``` RPUSH Requests: ``` 4. **Service receives the request:** ``` BLPOP Requests: ``` The service pops the next message from the queue and parses the JSON. 5. **Service processes the request.** It routes to the correct context and operation, runs middleware, executes the handler, and builds a response envelope with the same `id`, `context`, and `operation`, plus the `status`, `payload`, and any `messages`. 6. **Service sends the response:** ``` RPUSH ``` 7. **Client receives the response:** ``` BLPOP Responses:: ``` The client pops the response and matches it to the pending request by `id`. ## Interoperability: A Python Example Because the protocol is just JSON on a queue, any language that can talk to the backend can be a Strata client. Here's a minimal Python client that sends a request to a Strata service and reads the response: ```python import redis import json import uuid from datetime import datetime, timezone r = redis.Redis(host='localhost', port=6379, decode_responses=True) service_group = 'MyService' client_id = uuid.uuid4().hex[:20] response_queue = f'Responses:{client_id}' request_id = uuid.uuid4().hex[:20] request = { 'id': request_id, 'messageType': 'request', 'context': 'users', 'operation': 'get', 'responseQueue': response_queue, 'timestamp': datetime.now(timezone.utc).isoformat(), 'timeout': 30000, 'payload': {'userId': '12345'}, 'metadata': {}, 'client': 'python-client', } # Send the request r.rpush(f'Requests:{service_group}', json.dumps(request)) # Wait for response (30 second timeout) result = r.blpop(response_queue, timeout=30) if result: _, raw = result response = json.loads(raw) print(f"Status: {response['status']}") print(f"Payload: {response['payload']}") ``` This Python script is a fully functional Strata client. It builds a valid request envelope, pushes it onto the service's request queue, and waits for a response on its own dedicated queue. No Strata library needed -- just Redis and JSON. The same approach works in Go, Rust, Java, C#, or anything else that has a Redis client. The protocol is the contract. The official framework is Node.js, but both clients and services can be built in any language that speaks the protocol. --- --- url: /tools.md description: >- Overview of standalone Strata tools for HTTP bridging, message monitoring, and service management. --- # Tools Strata ships several standalone tools that complement the core library. These are deployed independently from your services and provide operational capabilities like HTTP/WebSocket bridging, message monitoring, and a service management UI. ## Available Tools | Tool | Description | Package | |------|-------------|---------| | [RPC Bridge](./rpc-bridge) | Exposes Strata services over HTTP and WebSockets so browser and external clients can call operations without direct Redis access. | `@strata-js/rpcbridge` / `@strata-js/rpcbridge-client` | | [Queue Monitor](./queue-monitor) | A Strata service that captures every message flowing through your system and writes them to Elasticsearch, Logstash, or the console for debugging and observability. | `strata-queue-monitor` | | [Service Tools](./service-tools) | A web UI for calling Strata services, with service discovery, environment management, and authentication support. | `strata-service-tools` | --- --- url: /guides/using-custom-backends.md description: >- How to register and configure a third-party or custom backend implementation with your Strata service or client. --- # Using a Custom Backend If the built-in backends don't fit your infrastructure, you can register a third-party or custom backend implementation. This guide covers how to wire one up; for building your own, see [Writing a Custom Backend](./writing-custom-backends). ## When to Use a Custom Backend * Your infrastructure uses a messaging system Strata doesn't ship with (AMQP, Kafka, gRPC, etc.) * A team or community has published a Strata backend package you want to use * You've written your own backend and need to plug it in ## Registering a Backend Call `registerBackend()` on your service or client **before** calling `start()`. The first argument is the name you'll reference in config; the second is the backend class. ### With a Service ```typescript import { StrataService } from '@strata-js/strata'; import { MyBackend } from './backends/myBackend.js'; const service = new StrataService({ service: { serviceGroup: 'UserService' }, backend: { type: 'my-backend', connectionUrl: 'amqp://localhost:5672', }, }); service.registerBackend('my-backend', MyBackend); await service.start(); ``` ### With a Client ```typescript import { StrataClient } from '@strata-js/strata'; import { MyBackend } from './backends/myBackend.js'; const client = new StrataClient({ backend: { type: 'my-backend', connectionUrl: 'amqp://localhost:5672', }, }); client.registerBackend('my-backend', MyBackend); await client.start(); ``` ## How the `type` Field Works The `type` field in your backend config is a lookup key. When Strata initializes, it finds the backend class registered under that name and instantiates it. The built-in backends (`redis`, `redis-streams`, `null`) are pre-registered; custom backends need an explicit `registerBackend()` call. ## Important: Register Before `start()` `registerBackend()` must be called before `start()`. During startup, Strata resolves the `type` string to a backend class, instantiates it, and calls `init()`. If the backend isn't registered yet, startup fails. ## Next Steps * [Writing a Custom Backend](./writing-custom-backends) -- how to build one from scratch. * [Backends](/core-api/backends) -- built-in backend reference and common configuration. * [Application](/core-api/application) -- full `registerBackend()` API docs. --- --- url: /guides/using-middleware.md description: >- How to install, register, configure, and order middleware at the service, context, and operation levels. --- # Using Middleware Middleware lets you run code before and after every request your service handles. Authentication checks, response caching, request logging, payload validation -- these are all common middleware use cases. This guide covers how to install, register, configure, and order middleware in your services. For how the middleware system works conceptually, see [Middleware Model](/concepts/middleware-model). For how to build your own middleware, see [Writing Middleware](./writing-middleware). ## Installing Middleware Strata publishes first-party middleware as separate npm packages under the `@strata-js` scope: ```bash npm install @strata-js/mw-cache npm install @strata-js/mw-message-logging npm install @strata-js/mw-payload-validation ``` Each package exports a class (or factory) that you instantiate with options and then register on your service, context, or operation. ## Registering Middleware Middleware can be registered at three levels. The level determines the scope -- which requests the middleware applies to. ### Global (Service-level) Global middleware runs on every request the service processes, across all contexts and operations. Use this for cross-cutting concerns like authentication or request logging. ```typescript import { StrataService } from '@strata-js/strata'; import { MessageLoggingMiddleware } from '@strata-js/mw-message-logging'; const service = new StrataService(config); // Runs on every request in every context service.useMiddleware(new MessageLoggingMiddleware()); ``` You can also register multiple middleware at once by passing an array: ```typescript service.useMiddleware([ new AuthMiddleware(), new MessageLoggingMiddleware(), ]); ``` ### Context-level Context-level middleware runs on every operation within a specific context. Use this when you need middleware that only applies to a subset of your operations. ```typescript import { StrataContext } from '@strata-js/strata'; import { PayloadValidationMiddleware } from '@strata-js/mw-payload-validation'; const users = new StrataContext('users'); // Runs on every operation in the 'users' context users.useMiddleware(new PayloadValidationMiddleware(userSchemas)); ``` ### Operation-level Operation-level middleware runs on a single operation only. Pass it as the fourth argument to `registerOperation()`. ```typescript import { CacheMiddleware } from '@strata-js/mw-cache'; users.registerOperation('get', async (request) => { return { user: await db.findUser(request.payload.userId) }; }, [], [ new CacheMiddleware({ ttl: 60000 }) ]); ``` The third argument (`[]`) is for operation function parameters. The fourth is the middleware array. ## Configuring Middleware Each middleware package defines its own configuration options, passed to the constructor. Consult the specific middleware documentation for details: ```typescript // Cache middleware with a 60-second TTL const cache = new CacheMiddleware({ ttl: 60000 }); // Message logging that only logs failures const logging = new MessageLoggingMiddleware({ logLevel: 'warn', onlyFailures: true }); ``` Since middleware is just a class instance, you can also configure it dynamically: ```typescript const env = process.env.ENVIRONMENT ?? 'local'; const cache = new CacheMiddleware({ ttl: env === 'production' ? 300000 : 5000, }); ``` ## Middleware Ordering The order in which middleware is registered matters. Middleware executes in an **outside-in / inside-out** pattern: ### `beforeRequest()` -- Outside-in For the `beforeRequest()` hook, middleware runs in registration order, from broadest scope to narrowest: 1. Global middleware (in registration order) 2. Context middleware (in registration order) 3. Operation middleware (in array order) ### `success()` / `failure()` -- Inside-out For the `success()` and `failure()` hooks, the order is reversed: 1. Operation middleware 2. Context middleware 3. Global middleware This means global middleware wraps everything -- it sees the raw request first and the final response last. ```mermaid flowchart LR G["Global"] -->|"beforeRequest()"| C["Context"] C -->|"beforeRequest()"| O["Operation"] O -->|"Handler"| H["Operation Handler"] H -->|"Result"| O2["Operation"] O2 -->|"success() / failure()"| C2["Context"] C2 -->|"success() / failure()"| G2["Global"] ``` ### Practical Example If you register middleware like this: ```typescript // Global service.useMiddleware(new AuthMiddleware()); // 1st global service.useMiddleware(new LoggingMiddleware()); // 2nd global // Context users.useMiddleware(new ValidationMiddleware()); // 1st context // Operation users.registerOperation('create', handler, [], [ new CacheMiddleware(), // 1st operation ]); ``` Then for a request to `users/create`: | Phase | Order | |-------|-------| | `beforeRequest()` | Auth -> Logging -> Validation -> Cache | | `success()` / `failure()` | Cache -> Validation -> Logging -> Auth | ### Short-circuiting If any `beforeRequest()` hook calls `request.succeed()` or `request.fail()`, the remaining `beforeRequest()` hooks and the operation handler are skipped. Execution jumps directly to `success()` or `failure()`. This is how auth middleware rejects unauthorized requests without hitting the handler. The `success()` and `failure()` hooks **always run** -- they cannot be skipped. This ensures cleanup middleware (like logging or metrics) always executes. ## Common Patterns ### Authentication Register auth middleware globally so every request is checked: ```typescript service.useMiddleware(new AuthMiddleware({ headerField: 'auth', validateToken: async (token) => verifyJWT(token), })); ``` ### Caching Register cache middleware on specific operations that benefit from it: ```typescript users.registerOperation('get', handler, [], [ new CacheMiddleware({ ttl: 60000 }), ]); ``` ### Logging Register logging middleware globally to capture all traffic: ```typescript service.useMiddleware(new MessageLoggingMiddleware()); ``` ### Payload Validation Register validation middleware on a context to validate all operations within it: ```typescript users.useMiddleware(new PayloadValidationMiddleware({ create: createSchema, get: getSchema, list: listSchema, })); ``` ## Next Steps * [Writing Middleware](./writing-middleware) -- build your own custom middleware. * [Middleware Model](/concepts/middleware-model) -- the conceptual model behind the three hooks. * [Cache Middleware](/middleware/cache) -- response caching middleware reference. * [Message Logging Middleware](/middleware/message-logging) -- request/response logging middleware reference. * [Payload Validation Middleware](/middleware/payload-validation) -- schema validation middleware reference. --- --- url: /utilities.md description: >- Overview of standalone Strata utility packages for configuration loading and structured logging. --- # Utilities Strata provides a set of standalone utility packages. These are published separately so they can be used in projects that don't depend on the full `@strata-js/strata` package. ## Packages | Package | npm | Description | |---------|-----|-------------| | [Config](./config) | `@strata-js/util-config` | File-based configuration loader with YAML/JSON support and environment variable substitution. | | [Logging](./logging) | `@strata-js/util-logging` | Structured logging utility wrapping [pino](https://getpino.io) with a `console.*`-style API. | --- --- url: /concepts/why-strata.md description: >- Why Strata.js replaces REST with message queues and how its Express-like API makes the transition familiar. --- # Why Strata Strata is a microservice framework for Node.js that replaces REST with message queues. If you've used [Express](https://expressjs.com/), you already know how Strata works -- contexts are routers, operations are routes, and middleware is middleware. The difference is that instead of HTTP requests between services, Strata uses message queues as the communication layer. The term "microservice" is loose here. Strata gives you tools for building services. How micro they are is entirely up to you. ## The Express Analogy | Express | Strata | Purpose | |-----------------|-----------------|---------------------------------------| | `express()` | `StrataService` | The application instance | | `Router` | `StrataContext` | Groups related handlers together | | Route/endpoint | Operation | A single unit of work | | `app.use()` | `useMiddleware()`| Pluggable request/response processing | The API should feel familiar. You create a service, register contexts that handle operations, and add middleware to modify requests and responses. The mental model is the same -- the transport is different. ## Why Message Queues In a REST architecture, services communicate directly. Service A makes an HTTP call to Service B. That means Service A needs to know where Service B is, needs Service B to be running right now, and gets one response from one instance. Message queues remove all three of those constraints. ### Decoupling Services don't need to know where other services live. A client pushes a message onto a queue. One or more service instances read from that queue. The client never needs to know how many instances exist, what hosts they're on, or how the work gets distributed. It just writes to the queue and moves on. ### Resilience If a service goes down, messages queue up instead of failing. When the service comes back, it picks up right where it left off. No lost requests, no retry logic, no circuit breakers. The queue absorbs the shock. ### Scalability Need to process messages faster? Add more service instances reading from the same queue. Each message goes to exactly one instance -- no load balancer needed, no sticky sessions, no coordination. The queue handles distribution automatically. Scale up by starting more processes, scale down by stopping them. ``` /-> Service Instance 1 Client -> [ Message3, Message2, Message1 ] --------> Service Instance 2 \-> Service Instance 3 ``` Each instance pops one message at a time. No two instances get the same message. Add or remove instances at any time without reconfiguring anything. ## What Makes Strata Opinionated Strata is opinionated about the things that matter for service communication and indifferent about the things that don't. Specifically: * **The protocol is defined.** Request and response envelopes have a fixed structure. No bikeshedding over payload formats, error shapes, or metadata conventions. See [The Protocol](./the-protocol) for the full specification. * **Backends are pluggable.** Redis lists, Redis Streams, or bring your own. The service code doesn't change when you swap backends. * **Middleware has clear hooks.** Three hooks (`beforeRequest`, `success`, `failure`) with a defined calling order. No ambiguity about when your code runs. * **Service groups handle scaling.** Multiple instances of the same service share a queue automatically. Horizontal scaling is a deployment concern, not a code concern. What Strata does *not* care about: how you structure your business logic, what ORM you use, how you organize your files, or what patterns you follow internally. That's your domain. ## Backend Agnostic Strata v2 introduced a pluggable backend system. The framework doesn't assume Redis, or any specific queue technology. Built-in backends include Redis lists, Redis Streams, and a null backend for testing. You can also [write your own](/guides/writing-custom-backends) by implementing the `BaseStrataBackend` interface. Your service code stays the same regardless of which backend you choose. The backend is a configuration choice, not an architectural one. --- --- url: /guides/writing-custom-backends.md description: >- How to build a custom backend by extending BaseStrataBackend for any messaging system (AMQP, Kafka, gRPC, etc.). --- # Writing a Custom Backend You can create your own backend for any messaging system -- AMQP, Kafka, gRPC, in-memory, whatever fits your infrastructure. This guide walks through building one from scratch. If you just need to *use* an existing custom backend, see [Using a Custom Backend](./using-custom-backends). ## What You're Building A backend is a class that extends `BaseStrataBackend`. It handles sending and receiving messages, managing queues, and optionally supporting service discovery. Strata calls your backend's methods during its lifecycle; your backend emits events when messages arrive. ## Extending `BaseStrataBackend` `BaseStrataBackend` is an abstract class that provides the event emitter interface and defines all required methods. Here's the full skeleton: ```typescript import { logging } from '@strata-js/util-logging'; import { BackendConfig, BaseStrataBackend, ConcurrencyCheck, DiscoveredServices, KnownServiceCommand, PostEnvelope, RequestEnvelope, ServiceCommandResponse, StrataRequest, StrataService, } from '@strata-js/strata'; // ------------------------------------------------------------------------------------------------- const logger = logging.getLogger('myBackend'); // ------------------------------------------------------------------------------------------------- export interface MyBackendConfig extends BackendConfig { type : 'my-backend'; connectionUrl : string; } // ------------------------------------------------------------------------------------------------- export class MyBackend extends BaseStrataBackend { #initialized = false; get initialized() : boolean { return this.#initialized; } // --------------------------------------------------------------------------------------------- // Lifecycle // --------------------------------------------------------------------------------------------- async init(config ?: MyBackendConfig) : Promise { this.config = config; // Connect to your messaging system here this.#initialized = true; logger.info('MyBackend initialized.'); } async teardown() : Promise { // Close connections, clean up resources this.#initialized = false; } // --------------------------------------------------------------------------------------------- // Request Handling // --------------------------------------------------------------------------------------------- async listenForRequests( serviceGroup : string, serviceID : string, waterMarkCheck : ConcurrencyCheck ) : Promise { // Subscribe to your message source. // When a message arrives, emit it: // this.emit('incomingRequest', parsedEnvelope); // Use waterMarkCheck() to respect concurrency limits. } async stopListeningForRequests(serviceGroup : string) : Promise { // Unsubscribe from the message source } async sendRequest( serviceGroup : string, envelope : RequestEnvelope | PostEnvelope, clientID ?: string ) : Promise { // Publish the request envelope to the appropriate queue/topic } async sendResponse(request : StrataRequest) : Promise { // Send the response back to the client's response queue } async listenForResponses(clientID : string, clientName ?: string) : Promise { // Subscribe for response messages. // When a response arrives, emit it: // this.emit('incomingResponse', parsedEnvelope); } async stopListeningForResponses() : Promise { // Unsubscribe from response messages } // --------------------------------------------------------------------------------------------- // Commands // --------------------------------------------------------------------------------------------- async listenForCommands(serviceGroup : string, serviceID : string) : Promise { // Subscribe to command channels. // When a command arrives, emit it: // this.emit('incomingCommand', parsedCommand); } async stopListeningForCommands() : Promise { // Unsubscribe from command channels } async listenForCommandResponses(clientID : string) : Promise { // Subscribe to command response channels. // When a response arrives, emit it: // this.emit('incomingCommandResponse', parsedResponse); } async stopListeningForCommandResponses() : Promise { // Unsubscribe from command response channels } async sendCommand(target : string, command : KnownServiceCommand) : Promise { // Publish the command to the target } async sendCommandResponse( target : string, envelope : ServiceCommandResponse ) : Promise { // Send the command response back to the caller } // --------------------------------------------------------------------------------------------- // Discovery // --------------------------------------------------------------------------------------------- async enableDiscovery(service : StrataService) : Promise { // Register this service instance for discovery } async disableDiscovery() : Promise { // Deregister this service from discovery } async listServices() : Promise { // Return currently discovered services return {}; } } // ------------------------------------------------------------------------------------------------- ``` ## Events to Emit The critical contract between your backend and Strata is the event emitter. Your backend **must** emit these events when messages arrive: | Event | When to Emit | Payload | |-------|-------------|---------| | `incomingRequest` | A request or post arrives | `RequestEnvelope \| PostEnvelope` | | `incomingResponse` | A response arrives | `ResponseEnvelope` | | `incomingCommand` | A service command arrives | `KnownServiceCommand` | | `incomingCommandResponse` | A command response arrives | `ServiceCommandResponse` | Strata's service and client layers listen for these events and handle all processing from there. ## Registering Your Backend Once your backend class is ready, register it before starting the service or client. See [Using a Custom Backend](./using-custom-backends) for the full registration walkthrough. ```typescript service.registerBackend('my-backend', MyBackend); await service.start(); ``` ::: tip Look at the `NullBackend` source in the Strata repository for a minimal working implementation. It implements every method as a no-op, which is useful as a starting template. ::: ## Contributing If you build a backend for a messaging system that others might use (AMQP, Kafka, NATS, etc.), consider publishing it as a standalone npm package. The Strata project is open to contributions of new backends, and we'd welcome third-party backend plugins. Open an issue on [GitLab](https://gitlab.com/strata-js/strata) if you'd like to discuss getting your backend listed in the official docs. ## Next Steps * [Using a Custom Backend](./using-custom-backends) -- registering and configuring a custom backend. * [Backends](/core-api/backends) -- built-in backend reference and common configuration. * [Envelope Validation](./envelope-validation) -- what gets validated on every message. * [Service Commands](./service-commands) -- controlling running services via commands. --- --- url: /guides/writing-middleware.md description: >- How to create custom middleware by implementing the OperationMiddleware interface with beforeRequest, success, and failure hooks. --- # Writing Middleware This guide walks through creating custom middleware from scratch. If you just need to use existing middleware, see [Using Middleware](./using-middleware). For the conceptual model, see [Middleware Model](/concepts/middleware-model). ## The Middleware Interface A middleware is any object that implements the `OperationMiddleware` interface: ```typescript interface OperationMiddleware< PayloadType = Record, MetadataType = Record, ResponsePayloadType = Record, > { beforeRequest : (request : StrataRequest) => Promise; success ?: (request : StrataRequest) => Promise; failure ?: (request : StrataRequest) => Promise; teardown ?: () => Promise; } ``` Key points: * **`beforeRequest` is required.** The other hooks are optional. * **All hooks are async.** Return a `Promise`, even if the work is synchronous. * **Hooks run serially.** A slow hook delays the entire chain. Keep them fast. * **Return the request.** If you return `undefined`, the original request is used unchanged. * **`teardown` is for cleanup.** Called when the service shuts down -- close connections, flush buffers, etc. The recommended approach is a class, because it gives you a natural place for configuration and state. ## Implementing Each Hook ### `beforeRequest()` This is the only hook that runs before the operation handler. Use it for: * Validating or transforming the incoming request payload * Checking authentication or authorization * Setting up context (timing, tracing, etc.) * Short-circuiting the request (rejecting it before it hits the handler) ```typescript import type { OperationMiddleware, StrataRequest } from '@strata-js/strata'; class RequestLoggerMiddleware implements OperationMiddleware { async beforeRequest(request : StrataRequest) : Promise { console.log(`Incoming: ${ request.context }/${ request.operation }`); return request; } } ``` ### `success()` Called after the operation handler succeeds. Use it for: * Transforming or enriching the response payload * Recording metrics (duration, status) * Caching successful results ```typescript async success(request : StrataRequest) : Promise { // The response payload is available on the request object console.log(`Success: ${ request.context }/${ request.operation }`); return request; } ``` ### `failure()` Called after the operation handler fails (either via `request.fail()` or by throwing an exception). Use it for: * Logging errors * Transforming error responses * Recording failure metrics * Cleanup of resources set up in `beforeRequest()` ```typescript async failure(request : StrataRequest) : Promise { console.error(`Failed: ${ request.context }/${ request.operation }`); return request; } ``` ### `teardown()` Called when the service shuts down. Use it to close connections, flush buffers, or release resources. ```typescript async teardown() : Promise { await this.metricsClient.flush(); await this.metricsClient.close(); } ``` ## Modifying Requests and Responses ### Modifying the Request (in `beforeRequest`) The `beforeRequest()` hook receives the request before the handler sees it. You can modify `payload`, `metadata`, or any mutable properties: ```typescript async beforeRequest(request : StrataRequest) : Promise { // Add a field to the payload request.payload.processedAt = new Date().toISOString(); // Add metadata for downstream tracking request.metadata.traceId = crypto.randomUUID(); return request; } ``` ### Modifying the Response (in `success` / `failure`) The `success()` and `failure()` hooks run after the handler has produced a result. You can modify the response payload through the request object: ```typescript async success(request : StrataRequest) : Promise { // Wrap the response with additional metadata if(request.response) { request.response.servedAt = new Date().toISOString(); } return request; } ``` ## Short-circuiting Requests In `beforeRequest()`, you can end processing early by calling `request.succeed()` or `request.fail()`. When you do this: 1. All remaining `beforeRequest()` hooks are skipped. 2. The operation handler is skipped. 3. Execution jumps directly to the `success()` or `failure()` hooks. This is the standard pattern for auth middleware: ```typescript async beforeRequest(request : StrataRequest) : Promise { if(!request.auth) { request.fail('AUTH_REQUIRED', 'Authentication token is required.'); return request; } const isValid = await this.validateToken(request.auth); if(!isValid) { request.fail('AUTH_INVALID', 'The provided token is invalid or expired.'); return request; } return request; } ``` And for cache middleware that returns a cached result without hitting the handler: ```typescript async beforeRequest(request : StrataRequest) : Promise { const cacheKey = `${ request.context }:${ request.operation }:${ JSON.stringify(request.payload) }`; const cached = this.cache.get(cacheKey); if(cached) { request.succeed(cached); return request; } return request; } ``` ## Error Handling Middleware hooks should handle their own errors. An unhandled exception in a middleware hook can break the middleware chain and may prevent other middleware from executing: ```typescript async beforeRequest(request : StrataRequest) : Promise { try { const user = await this.authService.validateToken(request.auth); request.payload.currentUser = user; } catch(error) { // Fail the request gracefully instead of letting the exception propagate request.fail('AUTH_ERROR', `Authentication failed: ${ error.message }`); } return request; } ``` ::: warning Unhandled exceptions in `success()` or `failure()` hooks can prevent other middleware in the chain from running. Always wrap risky operations in try/catch within these hooks. ::: ## Stateful vs. Stateless Middleware ### Stateless Middleware Stateless middleware doesn't track anything between requests. Each invocation is independent. Most middleware is stateless: ```typescript class HeaderEnricherMiddleware implements OperationMiddleware { readonly #defaultRegion : string; constructor(region : string) { this.#defaultRegion = region; } async beforeRequest(request : StrataRequest) : Promise { request.metadata.region = request.metadata.region ?? this.#defaultRegion; return request; } } ``` Stateless middleware is safe to share across contexts and operations. One instance handles all requests. ### Stateful Middleware Stateful middleware tracks data across the lifecycle of a single request (e.g., timing) or across multiple requests (e.g., caching, rate limiting). For **per-request state** (tracking data from `beforeRequest` to `success`/`failure`), use a `Map` keyed by request ID: ```typescript class TimingMiddleware implements OperationMiddleware { #startTimes = new Map(); async beforeRequest(request : StrataRequest) : Promise { this.#startTimes.set(request.id, Date.now()); return request; } async success(request : StrataRequest) : Promise { const start = this.#startTimes.get(request.id); if(start) { console.log(`${ request.context }/${ request.operation }: ${ Date.now() - start }ms`); this.#startTimes.delete(request.id); } return request; } async failure(request : StrataRequest) : Promise { // Clean up even on failure this.#startTimes.delete(request.id); return request; } } ``` ::: tip Always clean up per-request state in both `success()` and `failure()` to prevent memory leaks. ::: For **cross-request state** (like caching or rate limiting), use an appropriate data structure and be aware of concurrency. Strata processes multiple requests concurrently, so your state management must be safe for concurrent access. ## Testing Middleware Since middleware is just a class with async methods, it's straightforward to test. Create a mock request, call the hooks directly, and assert the results: ```typescript import { describe, it, expect } from 'vitest'; import { StrataRequest } from '@strata-js/strata'; import { AuthMiddleware } from './authMiddleware.js'; describe('AuthMiddleware', () => { it('should fail requests without an auth token', async () => { const middleware = new AuthMiddleware({ secret: 'test-secret' }); // Create a minimal request-like object for testing const request = new StrataRequest({ context: 'users', operation: 'get', messageType: 'request', payload: { userId: '123' }, metadata: {}, }); const result = await middleware.beforeRequest(request); expect(result.status).toBe('failed'); }); it('should pass requests with a valid auth token', async () => { const middleware = new AuthMiddleware({ secret: 'test-secret' }); const token = generateTestToken('test-secret'); const request = new StrataRequest({ context: 'users', operation: 'get', messageType: 'request', payload: { userId: '123' }, metadata: {}, auth: token, }); const result = await middleware.beforeRequest(request); expect(result.status).toBe('pending'); // Still pending = not short-circuited }); }); ``` For integration testing, use the `null` backend so you can run a full service without needing Redis: ```typescript const service = new StrataService({ service: { serviceGroup: 'TestService' }, backend: { type: 'null' }, }); service.useMiddleware(new AuthMiddleware({ secret: 'test-secret' })); ``` ## Complete Example: Rate Limiting Middleware Here is a complete, real-world middleware that implements per-operation rate limiting using a sliding window counter: ```typescript import type { OperationMiddleware, StrataRequest } from '@strata-js/strata'; import { logging } from '@strata-js/strata'; // ----------------------------------------------------------------------------------------- const logger = logging.getLogger('rateLimitMiddleware'); // ----------------------------------------------------------------------------------------- interface RateLimitConfig { maxRequests : number; // Maximum requests per window windowMs : number; // Window size in milliseconds keyFn ?: (request : StrataRequest) => string; // Custom key extractor } // ----------------------------------------------------------------------------------------- export class RateLimitMiddleware implements OperationMiddleware { readonly #config : RateLimitConfig; readonly #windows = new Map(); constructor(config : RateLimitConfig) { this.#config = config; } async beforeRequest(request : StrataRequest) : Promise { // Build a rate limit key -- default is by auth token, fall back to client const keyFn = this.#config.keyFn ?? ((req : StrataRequest) => req.auth ?? req.client ?? 'anonymous'); const key = `${ request.context }:${ request.operation }:${ keyFn(request) }`; const now = Date.now(); let window = this.#windows.get(key); // Reset window if expired if(!window || now >= window.resetAt) { window = { count: 0, resetAt: now + this.#config.windowMs }; this.#windows.set(key, window); } window.count++; if(window.count > this.#config.maxRequests) { logger.warn(`Rate limit exceeded for key: ${ key }`); request.fail('RATE_LIMITED', 'Too many requests. Please try again later.'); return request; } return request; } async teardown() : Promise { this.#windows.clear(); } } // ----------------------------------------------------------------------------------------- ``` Register it: ```typescript // Limit to 100 requests per minute per authenticated user users.useMiddleware(new RateLimitMiddleware({ maxRequests: 100, windowMs: 60000, })); ``` ## Next Steps * [Using Middleware](./using-middleware) -- registering and ordering middleware in your service. * [Middleware Model](/concepts/middleware-model) -- the conceptual model behind the three hooks. * [Core API: Application](/core-api/application) -- `useMiddleware()` API reference. --- --- url: /getting-started/your-first-service.md description: >- Build a realistic Strata service with multiple contexts, middleware, YAML configuration, and service-to-service calls. --- # Your First Service The [Quick Start](./quick-start) showed the bare minimum. This guide builds a more realistic service: multiple contexts, middleware, YAML configuration, and service-to-service calls. ## Project Structure A typical Strata service looks like this: ``` my-service/ ├── config/ │ ├── local.yml │ └── production.yml ├── src/ │ ├── contexts/ │ │ ├── users.ts │ │ └── health.ts │ └── service.ts ├── package.json └── tsconfig.json ``` Contexts live in their own files and export a `Context` instance. The service file wires everything together. Config files are loaded per environment. ## Define the Contexts ### Users Context Create `src/contexts/users.ts`: ```typescript import { Context } from '@strata-js/strata'; // ----------------------------------------------------------------------------------------- const users = new Context('users'); // ----------------------------------------------------------------------------------------- // In-memory store for this example const userStore = new Map(); users.registerOperation('create', async (request) => { const { name, email } = request.payload; const id = crypto.randomUUID(); const user = { id, name, email }; userStore.set(id, user); return { user }; }); users.registerOperation('get', async (request) => { const { id } = request.payload; const user = userStore.get(id); if(!user) { throw new Error(`User ${ id } not found`); } return { user }; }); users.registerOperation('list', async () => { return { users: Array.from(userStore.values()) }; }); // ----------------------------------------------------------------------------------------- export default users; // ----------------------------------------------------------------------------------------- ``` Each operation is an async function that receives the full request object. You can read `request.payload`, `request.metadata`, and `request.auth`. Return a value and Strata wraps it in a response envelope automatically. ### Health Context Create `src/contexts/health.ts`: ```typescript import { Context } from '@strata-js/strata'; // ----------------------------------------------------------------------------------------- const health = new Context('health'); // ----------------------------------------------------------------------------------------- health.registerOperation('check', async () => { return { status: 'ok', timestamp: new Date().toISOString(), uptime: process.uptime(), }; }); // ----------------------------------------------------------------------------------------- export default health; // ----------------------------------------------------------------------------------------- ``` ## Add Configuration Install `@strata-js/util-config` for YAML config loading: ```bash npm install @strata-js/util-config ``` Create `config/local.yml`: ```yaml # Logging logging: level: debug prettyPrint: true # Service service: serviceGroup: "MyService.$HOSTNAME" # Aliases (friendly names for service groups) aliases: myService: "MyService.$HOSTNAME" # Backend backend: type: "redis-streams" redis: host: "localhost" port: 6379 ``` Environment variables are automatically substituted -- `$HOSTNAME` becomes the machine's hostname at load time. This means each developer gets their own isolated service group when running locally. See [Configuration](./configuration) for the full reference on every config option. ## Wire Up the Service Create `src/service.ts`: ```typescript import { hostname } from 'node:os'; import { StrataService, StrataConfig, logging } from '@strata-js/strata'; import configUtil from '@strata-js/util-config'; // Contexts import usersContext from './contexts/users.js'; import healthContext from './contexts/health.js'; // ----------------------------------------------------------------------------------------- // Determine environment const env = process.env.ENVIRONMENT ?? 'local'; process.env.HOSTNAME = process.env.HOSTNAME ?? hostname(); // Load config configUtil.load(`./config/${ env }.yml`); // Set up logging logging.setConfig(configUtil.get()?.logging ?? {}); const logger = logging.getLogger('service'); // Create the service const service = new StrataService(configUtil.get()); // Register contexts service.registerContext(usersContext); service.registerContext(healthContext); // Start async function main() : Promise { await service.start(); logger.info('Service is ready.'); } main().catch((error) => { logger.error('Failed to start service:', error.stack); process.exit(1); }); // ----------------------------------------------------------------------------------------- ``` A few things to note: * `configUtil.load()` reads the YAML file and performs environment variable substitution. * `configUtil.get()` returns the parsed config object. Since `StrataService` expects a `StrataServiceConfig`, and the YAML has the right shape, it just works. * Each context was created with a name (`new Context('users')`, `new Context('health')`), so `registerContext` can be called with just the context instance. You can also call `service.registerContext('users', usersContext)` if you prefer to name them at registration time. ## Add Middleware Middleware wraps operations with `beforeRequest`, `success`, and `failure` hooks. You can add middleware globally (on the service), per-context, or per-operation. Here is a simple timing middleware that logs how long each request takes: ```typescript import type { OperationMiddleware } from '@strata-js/strata'; import { logging } from '@strata-js/strata'; const logger = logging.getLogger('timing'); const timingMiddleware : OperationMiddleware = { beforeRequest(request) { request['_startTime'] = Date.now(); return request; }, success(request) { const elapsed = Date.now() - (request['_startTime'] ?? 0); logger.info(`${ request.context }.${ request.operation } succeeded in ${ elapsed }ms`); return request; }, failure(request) { const elapsed = Date.now() - (request['_startTime'] ?? 0); logger.warn(`${ request.context }.${ request.operation } failed in ${ elapsed }ms`); return request; }, }; export default timingMiddleware; ``` Register it globally in your service file: ```typescript service.useMiddleware(timingMiddleware); ``` Or register it on a specific context: ```typescript usersContext.useMiddleware(timingMiddleware); ``` Or on a single operation: ```typescript users.registerOperation('get', async (request) => { // ...handler code }, [], [ timingMiddleware ]); ``` The third argument to `registerOperation` is `opFuncParams` (an array of request property paths to extract as function arguments). The fourth is middleware. ## Service-to-Service Calls If your service needs to call other services, create a `StrataClient` inside the same process. The client uses the same backend configuration but maintains its own response queue. ```typescript import { StrataClient, StrataClientConfig } from '@strata-js/strata'; import configUtil from '@strata-js/util-config'; // ----------------------------------------------------------------------------------------- let client : StrataClient | undefined; export async function getClient() : Promise { if(!client) { const config = configUtil.get(); client = new StrataClient({ ...config, client: { name: 'MyServiceClient' }, }); await client.start(); } return client; } // ----------------------------------------------------------------------------------------- ``` Then use it inside an operation: ```typescript users.registerOperation('getWithOrders', async (request) => { const { userId } = request.payload; const user = userStore.get(userId); if(!user) { throw new Error(`User ${ userId } not found`); } // Call the OrderService to get orders for this user const client = await getClient(); const orderResponse = await client.request('OrderService', 'orders', 'listByUser', { userId, }); return { user, orders: orderResponse.payload.orders }; }); ``` The `client.request()` call sends a message to the `OrderService` service group, targeting the `orders` context and `listByUser` operation. The response comes back as a `ResponseEnvelope` -- the payload is in `response.payload`. ::: tip Aliases If you define `aliases` in your config, you can use the alias name instead of the raw service group: ```yaml aliases: orders: "OrderService.production" ``` ```typescript await client.request('orders', 'orders', 'listByUser', { userId }); ``` ::: ## Run It ```bash # Make sure Redis is running npx tsx src/service.ts ``` You should see log output showing the service starting up and listening on its service group. To test it, create a quick client script or use the built-in `service` context that every Strata service gets for free: ```typescript import { StrataClient } from '@strata-js/strata'; const client = new StrataClient({ client: { name: 'TestClient' }, backend: { type: 'redis-streams', redis: { host: 'localhost', port: 6379 } }, }); await client.start(); // Every service has a built-in 'service' context with an 'info' operation const info = await client.request('MyService.your-hostname', 'service', 'info', {}); console.log(info.payload); // Create a user const created = await client.request('MyService.your-hostname', 'users', 'create', { name: 'Jane Doe', email: 'jane@example.com', }); console.log('Created:', created.payload); // List users const listed = await client.request('MyService.your-hostname', 'users', 'list', {}); console.log('Users:', listed.payload); process.exit(0); ``` ## Examples The Strata project maintains a reference example service on GitLab: [example-service](https://gitlab.com/strata-js/examples/example-service). It demonstrates the same patterns covered here -- config loading, context registration, middleware, and client usage -- in a real project structure. ## Next Steps * [Configuration](./configuration) -- full reference for service, client, and backend config. * [Architecture](../concepts/architecture) -- how all the pieces fit together. * [Middleware Model](../concepts/middleware-model) -- the three-hook middleware lifecycle in detail.