135 lines
4.5 KiB
Markdown
135 lines
4.5 KiB
Markdown
# @wrnexus/pubsub
|
|
|
|
> Topic-based publish/subscribe with a pluggable driver — in-process by default, Redis for cross-process messaging.
|
|
|
|
Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.
|
|
|
|
## Overview
|
|
|
|
`@wrnexus/pubsub` is a small server-side pub/sub bus. You publish messages to a
|
|
topic and subscribe with topic patterns; handlers fire for matching topics. The
|
|
default driver keeps everything in-process, and you can swap in the Redis driver
|
|
(`@wrnexus/pubsub/redis`) to fan messages out across processes or hosts. It also
|
|
backs `@wrnexus/core`'s realtime bridge for horizontal scaling.
|
|
|
|
## Installation
|
|
|
|
```bash
|
|
bun add @wrnexus/pubsub
|
|
```
|
|
|
|
> Private package — the machine must be authenticated to the `wrnexus` npm org
|
|
> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported).
|
|
|
|
## API
|
|
|
|
### `createPubSub(driver?): PubSub`
|
|
|
|
Creates a bus over a driver. Defaults to `memoryDriver()` (in-process).
|
|
|
|
```ts
|
|
interface PubSub {
|
|
publish<T = unknown>(topic: string, message: T): Promise<void>;
|
|
subscribe<T = unknown>(pattern: string, handler: Handler<T>): () => void;
|
|
close(): Promise<void>;
|
|
}
|
|
|
|
type Handler<T = unknown> = (message: T, topic: string) => void | Promise<void>;
|
|
```
|
|
|
|
- `publish(topic, message)` — resolves once the driver and in-memory async handlers finish.
|
|
- `subscribe(pattern, handler)` — returns an unsubscribe function.
|
|
- `close()` — idempotently rejects new work, clears local subscriptions, and closes the driver.
|
|
|
|
### Pattern matching
|
|
|
|
Subscription patterns match in three ways:
|
|
|
|
- **Exact** — `"order:created"` matches only that topic.
|
|
- **Prefix** — `"order:*"` matches any topic starting with `"order:"`.
|
|
- **Everything** — `"*"` matches all topics.
|
|
|
|
### `memoryDriver(): PubSubDriver`
|
|
|
|
The default in-process driver. Handlers are invoked synchronously (fire-and-forget
|
|
for async handlers) whenever a published topic matches a registered pattern.
|
|
|
|
```ts
|
|
interface PubSubDriver {
|
|
publish(topic: string, message: unknown): void | Promise<void>;
|
|
subscribe(pattern: string, handler: Handler): () => void;
|
|
}
|
|
```
|
|
|
|
### `@wrnexus/pubsub/redis` — `redisDriver(url?)`
|
|
|
|
A cross-process driver backed by Redis. It speaks RESP over a raw TCP socket via
|
|
`Bun.connect`, so it adds **no npm dependency**. `url` defaults to `$REDIS_URL`,
|
|
then `redis://localhost:6379`. The URL may carry a password and a database index
|
|
(e.g. `redis://:secret@host:6379/2`).
|
|
|
|
```ts
|
|
function redisDriver(url?: string, options?: RedisDriverOptions): PubSubDriver & { close(): void };
|
|
```
|
|
|
|
- Exact topics use Redis `SUBSCRIBE`; wildcard patterns (`ns:*`, `*`) use
|
|
`PSUBSCRIBE`, whose glob semantics line up with this library's matching.
|
|
- Messages are JSON-stringified on publish and `JSON.parse`d on receipt; a payload
|
|
that isn't valid JSON is delivered as the raw string.
|
|
- `close()` tears down both the subscriber and publisher connections.
|
|
- Lost sockets reconnect with bounded exponential backoff and active subscriptions
|
|
are replayed. `maxPending` bounds unavailable-connection writes (default 1000);
|
|
`reconnectDelayMs` and `reconnectMaxDelayMs` tune recovery (100ms/5000ms).
|
|
|
|
### RESP codec (internal)
|
|
|
|
`redis.ts` uses a minimal RESP implementation exported from `resp.ts`
|
|
(`encodeCommand`, `parseReply`, `concat`, and the `RespValue` type). These are
|
|
implementation details of the Redis driver, not part of the public package entry.
|
|
|
|
## Usage
|
|
|
|
In-process (default):
|
|
|
|
```ts
|
|
import { createPubSub } from "@wrnexus/pubsub";
|
|
|
|
const bus = createPubSub();
|
|
|
|
const off = bus.subscribe("order:*", (msg, topic) => {
|
|
console.log(topic, msg);
|
|
});
|
|
|
|
await bus.publish("order:created", { id: 7 });
|
|
|
|
off(); // unsubscribe
|
|
```
|
|
|
|
Cross-process with Redis:
|
|
|
|
```ts
|
|
import { createPubSub } from "@wrnexus/pubsub";
|
|
import { redisDriver } from "@wrnexus/pubsub/redis";
|
|
|
|
const driver = redisDriver("redis://localhost:6379");
|
|
const bus = createPubSub(driver);
|
|
|
|
bus.subscribe("order:*", (msg, topic) => {
|
|
// received on any app process subscribed to this pattern
|
|
});
|
|
|
|
await bus.publish("order:created", { id: 7 });
|
|
|
|
// on shutdown (also closes the driver)
|
|
await bus.close();
|
|
```
|
|
|
|
## Requirements / Notes
|
|
|
|
- **Bun-only.** The Redis driver depends on `Bun.connect`; it throws
|
|
`redisDriver requires the Bun runtime (Bun.connect).` outside Bun. The default
|
|
in-memory driver has no runtime dependencies.
|
|
- The Redis driver reads `REDIS_URL` from the environment when no `url` is passed.
|
|
- Backs [`@wrnexus/core`](../core)'s realtime bridge for horizontal scaling.
|
|
- No external npm dependencies — the Redis client is a self-contained RESP codec.
|