import { ChangeEvent } from '@voltro/database'; import { Effect } from 'effect'; import { Schema } from 'effect'; import { VoltroPlugin } from '@voltro/protocol'; /** * Wire a store to a broadcast provider. Publishes local changes outward * and injects remote changes inward (skipping own-origin). Returns a * handle whose `close()` tears everything down. * * Publish failures are logged + dropped — they MUST NOT propagate onto * the mutation path (the write already committed + inline-emitted * locally; only cross-replica fan-out is affected). */ export declare const attachBroadcastBus: (options: AttachBroadcastBusOptions) => Promise; export declare interface AttachBroadcastBusOptions { readonly store: BroadcastStore; readonly provider: BroadcastProvider; /** This replica's id — stamped as `origin` on every publish and matched * against incoming `origin` to skip own writes. */ readonly replicaId: string; readonly logger?: BroadcastBusLogger; /** Channel override (tests). Default `voltro:changes`. */ readonly channel?: string; /** * Called when this replica proves it missed changes from a peer. * * The count is EXACT — the difference between the serial in hand and the last * one seen from that origin — not an estimate. There is nothing to replay * (pub/sub keeps no log), so the caller's job is to make the loss irrelevant: * a live query is idempotent, so re-running every one of them is always safe * and always correct. `voltro dev` and `voltro serve` wire this to the * dispatcher's refresh. * * Absent ⇒ the gap is still DETECTED and logged; only the recovery is * missing. That is deliberate: a bus used without a dispatcher (a test, an * embedder) should still say what it lost. */ readonly onGap?: (origin: string, missed: number) => void; } /** * The changes channel when no namespace is known — a last resort, not the * normal path. A real boot resolves a namespace from the app's name and passes * the channel in, so two apps on one broker do not share it. */ export declare const BROADCAST_CHANNEL: string; export declare interface BroadcastBusHandle { /** Detach the onChange listener, the bus subscription, and close the * provider. Idempotent. */ readonly close: () => Promise; } export declare interface BroadcastBusLogger { info: (message: string, fields?: Record) => void; warn: (message: string, fields?: Record) => void; debug: (message: string, fields?: Record) => void; } /** The channels the framework runs over one broker. */ export declare type BroadcastChannelKind = 'changes' | 'events' | 'members' | 'presence'; /** * What rides the wire on the `voltro:changes` channel. `origin` is the * publishing replica's id; subscribers skip their OWN origin (the writer * already inline-emitted locally) so there is no double-emit and no need * for a dedup table. `event` is the app-mutation ChangeEvent to inject * into every OTHER replica's store emitter. */ export declare interface BroadcastEnvelope { readonly origin: string; readonly event: ChangeEvent; /** * This origin's monotonic serial, from 1. * * The one thing a receiver cannot infer, and without it a dropped message is * undetectable: pub/sub has no retention, so a replica whose broker * connection blips simply never learns that a change happened. Its clients * keep their sockets — the client-side reconnect never fires — and their live * queries stay stale until something else touches the same table, which for a * quiet table can be never. * * Optional on the type because a message from an older replica during a * rolling deploy has none; the receiver treats that as "cannot tell" rather * than as a gap. */ readonly n?: number; } /** * Transport failure. `transient` flags broker hiccups (connection reset, * timeout) worth a retry vs configuration faults (bad URL, missing dep) * that won't fix themselves. The wiring never rethrows it onto the * mutation path — a publish failure logs + drops; local reactivity is * unaffected. * * A `Schema.TaggedError` (the framework house style, same shape as * `@voltro/plugin-storage`'s `StorageError`): it rides the Effect error * channel with a stable `_tag`, is `instanceof`-checkable, and — should a * future path ever surface it across the rpc wire — is Schema-encodable. */ export declare class BroadcastError extends BroadcastError_base { } declare const BroadcastError_base: Schema.TaggedErrorClass; } & { provider: typeof Schema.String; message: typeof Schema.String; transient: typeof Schema.Boolean; cause: Schema.optional; }>; /** * A broadcast plugin instance also exposes the resolved provider so the * CLI's serve pipeline can attach the bus to the live DataStore. The * extra fields ride on the returned `VoltroPlugin` and are read by the * framework via `getBroadcastProvider`. */ export declare interface BroadcastPlugin extends VoltroPlugin { readonly broadcast: { readonly provider: BroadcastProvider; readonly url: string | null; readonly crossReplica: boolean; /** * The namespace as DECLARED here, before the app name or env is consulted. * `undefined` means "derive it" — the framework resolves the effective value * at boot, because only it knows the app's name. */ readonly namespace: string | undefined; }; } export declare const broadcastPlugin: (options?: BroadcastPluginOptions) => BroadcastPlugin; export declare interface BroadcastPluginOptions { /** Provider name (`'redis' | 'nats' | 'memory'`) or a pre-built * `BroadcastProvider`. Default: inferred from `BROADCAST_URL` / * `BROADCAST_PROVIDER` / `REDIS_URL` env, else `'memory'`. */ readonly provider?: BroadcastProviderName | BroadcastProvider; /** Broker URL — a broker-agnostic override (redis:// or nats://). Takes * precedence over the named-connection env. Default `BROADCAST_URL` env. */ readonly url?: string; /** Named connection for the redis provider — resolves the broker URL via the * shared convention `_REDIS_URL` → `REDIS_URL` (like cache / kv / * ratelimit). Default `'broadcast'` (→ `BROADCAST_REDIS_URL`, then * `REDIS_URL`). Point broadcast at its own server or the shared one purely * by which env var you set. */ readonly connection?: string; /** * Namespace for EVERY framework channel on this broker — changes, events, * membership and presence alike. * * Defaults to your app's name, so two different apps sharing one Redis or * NATS separate without anyone remembering to do anything. Set it explicitly * for the case that default cannot see: **several deployments of the SAME app * on one broker** (staging and production, say) have the same name and the * same code, so nothing derivable tells them apart. There, this — or * `VOLTRO_BROADCAST_NAMESPACE` — is the only thing that works. */ readonly namespace?: string; /** Disambiguates multiple instances of this plugin in one app. */ readonly name?: string; } /** * Dumb pub/sub transport. One channel, string payloads, no framing. * * - `publish` — fire-and-forget a payload onto a channel. Transient * broker hiccups should fail the Effect (the caller logs + drops; * cross-replica reactivity degrades, local reactivity is untouched). * - `subscribe` — register a handler for every payload on a channel. * Returns an unsubscribe Effect. The handler is invoked once per * received message; it must not throw (errors are swallowed by the * wiring's decode guard). * - `close` — tear down connections / the pub + sub sockets. * * Implementations are constructed eagerly but connect lazily where the * backend allows, so a process that wires the plugin but never publishes * doesn't pay a connect cost on an unrelated path. */ export declare interface BroadcastProvider { /** Backend name — `'redis' | 'nats' | 'memory'`. Surfaced in the boot banner. */ readonly name: string; readonly publish: (channel: string, payload: string) => Effect.Effect; readonly subscribe: (channel: string, handler: (payload: string) => void) => Effect.Effect<() => void, BroadcastError>; readonly close: () => Effect.Effect; } /** Provider id understood by `resolveBroadcastProvider`. */ export declare type BroadcastProviderName = 'redis' | 'nats' | 'memory'; /** The minimal store surface the bus drives — a subset of `DataStore` so * the bus stays decoupled from the concrete store classes. */ export declare interface BroadcastStore { onChange: (listener: (event: ChangeEvent) => void) => () => void; injectExternalChange: (event: ChangeEvent) => void; } /** * One framework channel inside a namespace. * * Every channel goes through here so a fifth one cannot be added as a flat * constant that quietly skips the namespace — which is how the first four came * to be flat in the first place. */ export declare const channelFor: (namespace: string, kind: BroadcastChannelKind) => string; /** * The namespace used when nothing else is known. * * Only reachable when there is no app name to derive from — a bare library use. * A real boot always has one. */ export declare const DEFAULT_BROADCAST_NAMESPACE = "voltro"; /** * Whether the resolved namespace differs from what was CONFIGURED — and if so, * from which source. * * Sanitising is correct and must stay: the value becomes a broker subject, and a * subject with whitespace is refused outright by NATS. But normalising silently * has a hazard of its own, and it is exactly the one this namespace exists to * remove. `my app` and `my.app` both resolve to `my-app`, so two deployments * configured DIFFERENTLY share a channel — which is the failure the option was * added to prevent, arrived at by the option itself. * * Nothing here refuses: the resolved value is safe either way, and a boot * failure over a dot would be worse than the collapse. The boot logs it instead, * so an operator who wrote `prod env` sees `prod-env` and can spot two * environments landing in one place. */ export declare const describeNamespaceResolution: (input: ResolveBroadcastNamespaceInput) => { readonly resolved: string; readonly configured: string | undefined; readonly changed: boolean; }; /** * The channel for ONE declared event. * * Per event rather than per app, so a replica can decline the traffic it has no * subscribers for. The event name comes from a declaration and is unique by boot * audit, but it is sanitised anyway: a name containing a NATS wildcard would * turn one event's channel into a pattern matching others. */ export declare const eventChannelFor: (namespace: string, event: string) => string; /** * Pull the resolved broadcast carrier off a plugin list (the framework's * serve pipeline calls this after building the store). Returns the FIRST * broadcast plugin found, or `null`. */ export declare const getBroadcastPlugin: (plugins: ReadonlyArray) => BroadcastPlugin | null; /** * In-process pub/sub over a named bus. Delivery is synchronous within the * process. Two providers sharing a `bus` name see each other's publishes — * the seam the cross-instance test exercises without a real broker. */ export declare const memoryProvider: (bus?: string) => BroadcastProvider; declare interface NatsLike { publish: (subject: string, data: Uint8Array) => void; subscribe: (subject: string) => NatsSubscription; drain: () => Promise; } export declare const natsProvider: (options: NatsProviderOptions) => BroadcastProvider; export declare interface NatsProviderOptions { /** `nats://host:port` (comma-separated for a cluster). Required. */ readonly url: string; /** Bring your own connected NATS connection (e.g. for shared creds). */ readonly connection?: NatsLike; } declare interface NatsSubscription extends AsyncIterable<{ data: Uint8Array; }> { unsubscribe: () => void; } /** The slice of ioredis the provider touches — keeps the optional dep out * of the type surface. */ declare interface RedisLike { publish: (channel: string, message: string) => Promise; subscribe: (channel: string) => Promise; unsubscribe: (channel: string) => Promise; on: (event: 'message', listener: (channel: string, message: string) => void) => void; off: (event: 'message', listener: (channel: string, message: string) => void) => void; duplicate: () => RedisLike; quit: () => Promise; } export declare const redisProvider: (options: RedisProviderOptions) => BroadcastProvider; export declare interface RedisProviderOptions { /** `redis://[:pass@]host:port[/db]`. Required. */ readonly url: string; /** Bring your own ioredis client (publisher). A `subscriber` is * `client.duplicate()`d internally — a RESP connection in subscribe * mode can't issue normal commands. */ readonly client?: RedisLike; } /* Excluded from this release type: resetMemoryBus */ export declare const resolveBroadcastNamespace: (input: ResolveBroadcastNamespaceInput) => string; export declare interface ResolveBroadcastNamespaceInput { /** `broadcast({ namespace })` — explicit, and wins over everything. */ readonly option?: string | undefined; /** `VOLTRO_BROADCAST_NAMESPACE` — for the case the code cannot see. */ readonly env?: string | undefined; /** The app's name, the default. Different apps therefore separate on their own. */ readonly appName?: string | undefined; } export declare interface ResolveBroadcastOptions { /** Explicit provider name or a pre-built `BroadcastProvider`. */ readonly provider?: BroadcastProviderName | BroadcastProvider; /** Broker URL — a broker-agnostic override (redis:// or nats://). Takes * precedence over the named-connection env. Default `BROADCAST_URL` env. */ readonly url?: string; /** Named connection for the redis provider. The broker URL resolves via the * shared convention `_REDIS_URL` → `REDIS_URL` (like cache / kv / * ratelimit), so a deployment points broadcast at the same server or its own * purely by env. Default `'broadcast'` (→ `BROADCAST_REDIS_URL`). */ readonly connection?: string; } /** * Resolve the bus backend. Order: an object provider is used verbatim; a * named provider builds from `url`/env; absent → `BROADCAST_PROVIDER` env, * else inferred from a `redis://` / `nats://` URL, else `memory`. * * Returns the provider plus the URL it resolved (for the boot banner) and * whether the choice is a real cross-replica bus or the single-process * memory fallback. */ export declare const resolveBroadcastProvider: (options: ResolveBroadcastOptions, env: NodeJS.ProcessEnv) => { provider: BroadcastProvider; url: string | null; crossReplica: boolean; }; export { }