export class PowerRealtimeHub { /** * Validate a `priority` value supplied to {@link subscribe}. * * Extracted from `subscribe` because that method was already at the * cyclomatic-complexity ceiling and this check is its own branch — and * because the rule it enforces is worth stating once rather than inline. * * `priority` is a drain order, and the failure mode it guards against is * specifically the silent one: a non-finite value coerces to `NaN`, which * compares unequal to everything, so `Array.sort` lands the subscriber in an * arbitrary position without throwing. The caller would get a wrong-order * delivery with no error, months after the subscribe that accepted it. * * @param {*} priority * @returns {number} A finite number. `0` when the caller omitted it. * @throws {TypeError} When `Number(priority)` is not finite. * @static */ static _validatePriority(priority: any): number; /** * Validate a `bytesAcknowledged` callback supplied to {@link subscribe}. * * WT-004. `bytesAcknowledged` is transport-reported per stream and is a * strictly better figure than the hub's own `bytesSent` when the connection * is over HTTP/2 — it counts bytes the peer has actually acknowledged, not * bytes handed to the adapter. But it is **not** a replacement: it is only * available on transports that report it, and on others it is absent. So the * hub keeps `bytesSent` as the floor and exposes this as an **optional** * callback the caller wires up at subscribe time. * * The shape is a function rather than a number, because the value moves: * a number captured at subscribe time would be stale by the next flush. * The callback is invoked **after** the transport has taken the frame, in the * same place `bytesSent` is incremented, so the two move together. * * @param {*} fn * @returns {((arg0: number, arg1: HubSubscriber) => void)|null} The callback, or `null` * when the caller supplied `null` or `undefined` — both are the "not * supplied" sentinel, and the field is typed `function | null` so a caller * passing `null` explicitly gets the no-op rather than an error. * @throws {TypeError} When `fn` is not a function and not `null`. * @static */ static _validateAcknowledged(fn: any): ((arg0: number, arg1: HubSubscriber) => void) | null; /** * @param {HubOptions} options - `send` is required; the constructor throws * without it, so the parameter is not defaulted. */ constructor(options: HubOptions); /** @type {Map>} topic -> subscriberId -> sub */ /** @type {Map} subscriberId -> sub */ /** @type {Map} topic -> retained messages (bounded) */ /** @type {import('./powerRateLimit.js').PowerRateLimit|null} */ _metrics: { unregister: () => boolean; name: string; } | null; /** * Subscribe to a topic. * * @param {string} topic - Topic name. * @param {function(any, object):void} handler - Invoked with each delivered * message. Throwing is isolated and reported through `onError`. * @param {SubscriberOptions} [options] * @returns {function():boolean} An unsubscribe function. Returns `false` if * the subscription was already gone. */ subscribe(topic: string, handler: (arg0: any, arg1: object) => void, options?: SubscriberOptions): () => boolean; /** * Remove a subscription. * @param {string} id - Subscriber id. * @returns {boolean} `true` when a subscription was removed. */ unsubscribe(id: string): boolean; /** * Publish a message to every subscriber of a topic. * * Delivery is asynchronous: the message is queued per subscriber and handed to * the transport on the next flush. A `send` adapter that throws is reported * through `onError` and does not affect other subscribers. * * @param {string} topic * @param {any} message * @param {Object} [options] * @param {boolean} [options.retain=false] - Keep the message for a subscriber * that subscribes later. Intended for a small, fixed set of topics such as * config changes; the retained log is not bounded per subscriber, so do not * use it for an unbounded feed. * @param {number} [options.priority] - Delivery order within each subscriber's * queue, higher first. Requires the hub's `messagePriority` option; passing * it without that throws, because an ordering that is silently ignored is * the misspelled-option failure this library refuses to have. Omitted means * `0`, which is indistinguishable from an explicit `0` — the same rule the * per-subscriber `priority` drain order follows. * @returns {number} The number of subscribers the message was queued for. */ publish(topic: string, message: any, options?: { retain?: boolean | undefined; priority?: number | undefined; }): number; /** * Flush every pending message immediately, bypassing batching. * @returns {Promise} Resolves once all subscribers have been drained. */ flush(): Promise; /** * Snapshot of counters and per-subscriber state. * * `subscribers` is the live *count*, and the per-subscriber array is `list`. * The declared return previously intersected `subscribers: Array` * onto `HubStats`, which is how the hub ended up publishing a type saying * `subscribers` was an array of records - a number at runtime. * * @returns {HubStats & {list: HubSubscriberStat[]}} */ stats(): HubStats & { list: HubSubscriberStat[]; }; /** * Alias for {@link stats}. * * See `guides/stats-naming.md` for why both spellings exist and why this * method is written out per class. */ getStats(): HubStats & { list: HubSubscriberStat[]; }; /** * Presence: the subscribers currently connected to the hub. * * Returns an array of `{ id, topic, transport }` for every live subscriber, * or only those on `topic` when one is given. A snapshot, not a live view — * re-call it after subscribe/unsubscribe to see the current set. * * @param {string} [topic] - Optional topic filter. * @returns {Array<{id: string, topic: string, transport: any}>} */ presence(topic?: string): Array<{ id: string; topic: string; transport: any; }>; /** * Alias for {@link presence}. * @param {string} [topic] * @returns {Array<{id: string, topic: string, transport: any}>} */ getPresence(topic?: string): Array<{ id: string; topic: string; transport: any; }>; /** * Close every subscription and release timers. The hub cannot be reused. * * Synchronous and non-graceful: in-flight `_inflightChain` sends and any * batched frames not yet flushed are dropped. Use `asyncDispose()` (or * `await using`) to flush pending frames first (B2-037). * @returns {void} */ close(): void; /** * Named alias for the `Symbol.dispose` implementation, so callers who do not * want to reach for the symbol still have something to call. * @returns {void} */ dispose(): void; /** * Queue a message for one subscriber, applying the slow-consumer policy when * the queue is full. * @private * @param {HubSubscriber} sub * @param {any} message * @param {number} [priority] - Message ordering, higher first. Ignored unless * the hub was built with `messagePriority`; `publish()` refuses to supply one * on a hub that is not, so this is unreachable rather than silently dropped. * @returns {boolean} */ /** * Retain a message for future subscribers of a topic. * @private * @param {string} topic * @param {any} message * @returns {void} */ /** * @private */ /** * Subscribers with queued work, highest priority first, ties broken by * insertion order. * * Extracted rather than inlined in `_drain` because `flush()` walks the same * set through `_flushAll`, and a priority that only applied to the * microtask path would be invisible to the caller-driven path — the two * would disagree about who gets served first, which is exactly the kind of * divergence a row like this exists to prevent. * * The sort is stable in every engine this package supports, so subscribers * at equal priority keep the order they were subscribed in. * @private * @returns {HubSubscriber[]} */ /** * Drain every subscriber with queued work, one batch per send. * * Subscribers are visited **highest priority first**, and ties fall back to * insertion order. A subscriber with nothing queued is skipped, so the walk * is O(subscribers) rather than a sort over the whole set — but the relative * order of two subscribers that *both* have queued work is decided by * priority, which is the only ordering this method is asked to guarantee. * @private * @returns {void} */ /** * @private * @returns {Promise} */ /** * Deliver everything queued for one subscriber, waiting out any send already * in flight, so the frames reach the transport one at a time and in order. * * Each turn either empties the queue or advances an outstanding send, and the * queue is bounded by `maxQueue`, so this terminates. * * @param {HubSubscriber} sub * @returns {Promise} * @private */ /** * Build one frame from a subscriber's queue and hand it to the transport. * @private * @param {HubSubscriber} sub * @returns {Promise} */ /** * @private * @param {any[]} batch * @returns {Uint8Array} */ /** * Detach a subscriber from the hub, optionally closing its transport. * @private * @param {HubSubscriber} sub * @param {{close?: boolean, reason?: string}} [options] * @returns {void} */ /** * Report an internal failure through the optional `onError` adapter. A * throwing `onError` must not break the hub, so it is swallowed. * @private * @param {any} err * @param {HubSubscriber} sub * @returns {void} */ [Symbol.dispose](): void; /** * Asynchronous disposal hook: **flush what is pending, then close.** * * This is the graceful half of the pair, and it exists because `close()` is not * graceful — it clears the pending batch along with everything else. A hub * configured with `batchDelayMs > 0` can be holding frames that have been * `publish`ed but not yet sent, and `using` at scope exit would drop them. So * `await using` gets the frames out first. * * Deliberately the same shape as `PowerPool`'s: drain, swallow drain failures, * then tear down. The swallowing is not carelessness — a `finally` in a * disposal path must leave the instance closed even if the flush fails, and * leaving it open would be worse than losing the flush. * * @returns {Promise} */ [Symbol.asyncDispose](): Promise; } export default PowerRealtimeHub; /** * What to do when a subscriber's queue is full. * * - `drop-oldest` — discard the oldest pending message to make room for the new * one. Best for chat/log-style feeds where the latest state matters most. * **This is the default**: it degrades a slow consumer into a stale one * rather than into a stalled server. * - `drop-newest` — refuse the incoming message and keep what is already * queued. Best for ordered, replay-sensitive streams. * - `disconnect` — close the subscriber once it falls behind. Use this when * silently delivering stale or missing data is worse than cutting the client * loose so it reconnects with fresh state. */ export type SlowConsumerPolicy = "drop-oldest" | "drop-newest" | "disconnect"; /** * A live subscription record, as the hub stores it. * * `SubscriberOptions` describes what a caller may pass; this is what the hub * keeps after merging in the per-hub defaults. The two maps holding them were * typed `object`, so `id`, `topic`, `queue`, `dropped`, `inFlight`, `maxQueue`, * `slowConsumer` and `closed` did not exist at any of the ~30 places the hub * reads them. */ export type HubSubscriber = { id: string; topic: string; /** * Bounded buffer for this subscriber. A {@link HubQueue} - the hub reads * `.length`, `.push`, `.shift` and `.splice` off it, so a queue typed as an * abstract buffer (the previous declaration) had no `.length` at any of the * five places that check it before enqueueing. It is a plain array in fifo * mode and a `PowerPriorityQueue` behind that surface in priority mode, which * is why the ordering policy never reaches the flush walk. */ queue: HubQueue; /** * - Messages discarded by the slow-consumer policy. */ dropped: number; /** * - Bytes of framed payload handed to this * subscriber's transport so far. Exact, and free: the frame was built for this * flush anyway, so this is one addition against an already-computed * `frame.length`. It is **not** a count of what is sitting in `queue` — see * {@link HubSubscriberStat.bytesSent}. */ bytesSent: number; /** * - Sends currently awaiting the transport. */ inFlight: number; /** * - The promise for the send * currently in flight, **including any follow-up flush it chained**, so * `flush()` can wait for a subscriber's queue to actually empty rather than * for one frame. */ _inflightChain?: Promise | null | undefined; maxQueue: number; maxBatch: number; slowConsumer: SlowConsumerPolicy; closed: boolean; /** * - Drain order. Higher numbers are delivered * first; `0` is the default and is indistinguishable from a subscriber that * asked for `0`, so the common case stays a stable insertion-order walk. */ priority: number; /** * - * **WT-004.** The callback the caller supplied at subscribe time, or * `null` when none was supplied — which is the normal case, because the * hub's own `bytesSent` is the floor and needs no callback. Invoked * after the transport has taken the frame, in the same statement that * increments `bytesSent`, so the two move together. */ bytesAcknowledged?: ((arg0: number, arg1: HubSubscriber) => void) | null | undefined; /** * - Invoked with each * delivered message, after the transport accepted it, plus the subscriber it * was delivered to. Spelled as a call signature so the two arguments the hub * passes are checked, and so a handler is callable rather than `Function`. */ handler: (arg0: any, arg1: HubSubscriber) => void; /** * - Opaque handle the caller attached at subscribe * time (a socket, a stream, a peer id). The hub never reads it; it exists so * a `send`/`close` adapter can get back to its own connection. */ transport?: any; }; export type SubscriberOptions = { /** * - Maximum messages buffered for this * subscriber before the slow-consumer policy applies. `0` disables queueing * entirely: a full-queue condition is evaluated immediately, which is the * right setting when delivery is fire-and-forget. */ maxQueue?: number | undefined; /** * - Policy applied * when `maxQueue` is exceeded. */ slowConsumer?: SlowConsumerPolicy | undefined; /** * - Maximum messages coalesced into one send. */ maxBatch?: number | undefined; /** * - Stable identifier; generated when omitted. */ id?: string | undefined; /** * - Drain order. Higher numbers are delivered * first within a topic on the next flush; `0` is the default and is * indistinguishable from a subscriber that asked for `0`, so the common * case stays a stable insertion-order walk. A non-finite value is rejected * at subscribe time, because it would coerce to `NaN` and sort to an * arbitrary position silently. */ priority?: number | undefined; /** * - * **WT-004.** Optional callback reporting bytes the transport has * *acknowledged* for this subscriber, as opposed to bytes the hub handed * over. Transport-reported per stream, so on HTTP/2 it matches the hub's * own `bytesSent`; on transports that do not report it the callback is * simply not supplied and the hub keeps `bytesSent` as the floor. Invoked * after the transport has taken the frame, in the same statement that * increments `bytesSent`, so the two move together. The hub does not * validate the number the callback reports — it is the caller's transport, * and the hub's job is to call it, not to audit it. */ bytesAcknowledged?: ((arg0: number, arg1: HubSubscriber) => void) | null | undefined; /** * - Carried through to the stored * {@link HubSubscriber} untouched, for the caller's own `send`/`close` * adapters to use. */ transport?: any; }; /** * One entry of the per-subscriber array in {@link PowerRealtimeHub#stats}. */ export type HubSubscriberStat = { id: string; topic: string; /** * - Messages waiting for this subscriber right now. */ queued: number; dropped: number; /** * - Bytes handed to this subscriber's transport so * far. **This is the per-subscriber share of `stats().bytesOut`, and the two * reconcile exactly:** the hub adds `frame.length` to both in the same * statement, so `bytesOut === Σ list[].bytesSent` for any set of subscribers * still attached. RT-026 replaced a field called `bytesQueued` here that was * initialised to `0` and never written, which made it the second * permanently-zero advertisement in a class whose entire job is to let a * caller see how far behind a subscriber is. The reconcilable pair is what * makes this one real; a counter nothing can check is decoration. */ bytesSent: number; inFlight: number; maxQueue: number; slowConsumer: SlowConsumerPolicy; /** * - Drain order, so a caller reading `stats().list` * can see *why* a subscriber was served before another rather than guessing * from `queued`/`inFlight`. Reflected from the stored subscriber, not * re-derived: it is a value the caller supplied, so reporting it back is the * honest thing and recomputing it would be inventing a value. */ priority: number; }; export type HubStats = { /** * - Current live subscription count. */ subscribers: number; /** * - Number of topics with at least one subscriber. */ topics: number; /** * - Total messages accepted by `publish`. */ published: number; /** * - Total messages handed to a `send` adapter. */ delivered: number; /** * - Total messages discarded by a policy. */ dropped: number; /** * - Total messages dropped by the rate limiter. */ rateLimited: number; /** * - Subscribers closed for falling behind. */ disconnected: number; /** * - Approximate bytes handed to the adapter. */ bytesOut: number; }; export type HubOptions = { /** * - Required * transport adapter, called as `send(subscriber, frame)`. Return a promise if * the transport is async; the hub tracks in-flight sends per subscriber. * * **The `frame` is shared and must be treated as read-only.** RT-006 encodes one * frame per `(topic, batch)` and hands the same buffer to every subscriber on * the topic, so a transport that writes into `frame` corrupts every other * subscriber's message. Copy it if the transport needs to own it. * `stats().encoded` makes a violation visible: it counts real encodes, so it * stays at one per flush however many subscribers the topic has. */ send: (arg0: object, arg1: Uint8Array) => (void | Promise); /** * - Opt in to * metrics: `true` registers this helper in the shared collector, or pass a * collector of your own. Off by default, so the common case allocates nothing. * See `guides/metrics.md`. */ observability?: boolean | import("./metrics.js").MetricsCollector | undefined; /** * - Optional * adapter called when the hub closes a subscriber for falling behind or on * `close()`. Takes the same `(subscriber, reason)` pair as `send` plus why * it happened - `'unsubscribe'`, `'slow-consumer'` or `'hub-closed'`. The * published type previously declared one parameter, while the guide, the * runtime and every test all pass and read two. */ close?: ((arg0: object, arg1: string) => (void | Promise)) | undefined; /** * - Coalesce messages published within the * same microtask into a single `send`. Turn off for tests or transports that * cannot handle several frames at once. */ batch?: boolean | undefined; /** * - Optional macrotask delay before * flushing, to widen the coalescing window beyond a single microtask. */ batchDelayMs?: number | undefined; /** * - Payload codec for outgoing frames. */ codec?: "json" | "raw" | undefined; /** * - Called when the `send` * adapter rejects or throws, instead of leaving an unhandled rejection. */ onError?: ((arg0: Error, arg1: object) => void) | undefined; /** * - Optional * per-topic rate limiter. When set, `publish()` calls `tryConsume(1, { context: * { topic } })` before enqueuing; a `false` return drops the message for that * topic and increments `stats().rateLimited`. Composes with any helper that * satisfies {@link RateLimiterLike} — `PowerThrottle`, `PowerGCRA`, * `PowerRateLimit` with `keyFn`, etc. */ rateLimit?: import("./jsdoc-types.js").RateLimiterLike | undefined; /** * - Order each subscriber's queue * by the `priority` passed to `publish()` rather than by arrival. Off by * default, so the common case stays a plain array with FIFO delivery and pays * nothing. When on, every subscriber's queue is a `PowerPriorityQueue`, and * `publish(topic, message, { priority })` is accepted — passing `priority` on * a hub without this option throws, because a silently ignored ordering is * the misspelled-option failure this library refuses to have. * * This is **message** priority and is independent of the per-subscriber * `priority` drain order: the first decides which message a subscriber * receives next, the second decides which subscriber is served first. */ messagePriority?: boolean | undefined; }; /** * A subscriber's pending queue, in its two shapes. * * The hub reads `length`, `push`, `shift` and `splice(0, n)` off a subscriber's * queue in five places, and every one of those is ordering-agnostic — so the * ordering policy lives entirely behind this surface and `_enqueue`, * `_flushSubscriber`, `_queuedSubscribers` and `stats()` never branch on it. * That is the point: a policy that leaked into the flush walk would be a policy * the two flush paths could disagree about. * * `fifo` is the default and is a plain array. Zero allocation per message, and * the documented `drop-oldest` / `drop-newest` / `disconnect` policies mean * exactly what they say. * * `priority` is backed by {@link PowerPriorityQueue}. It exists because a * bounded array with binary-search insertion is O(n) per publish and `maxQueue` * is a caller-chosen number, not a small one — a subscriber configured with * `maxQueue: 100000` would pay a 100000-element move on every message. * * @private */ declare class HubQueue { /** * @param {boolean} priority */ constructor(priority: boolean); /** @type {any[]} */ /** @type {PowerPriorityQueue|null} */ get length(): number; /** * @param {any} message * @param {number} [priority] - Ignored in fifo mode, which has no ordering * to apply. The hub rejects a `publish` that supplies one on a hub without * `messagePriority`, so this is unreachable rather than silently dropped. */ push(message: any, priority?: number): void; /** * Unwrap one entry taken off the heap. * * The heap hands back the wrapper `push()` stored, and everything downstream * of this class — the handler, the codec, the frame memo's identity key — * expects the caller's message. Unwrapping here rather than at each call site * is what keeps the ordering policy from leaking into the flush walk. * @param {any} entry * @returns {any} */ _unwrap(entry: any): any; shift(): any; /** * Take up to `count` items off the front, in delivery order. * * `start` is always `0` at the one call site (`_flushSubscriber`), and saying * so is cheaper than implementing a general splice the hub never asks for. * @param {number} start * @param {number} count * @returns {any[]} */ splice(start: number, count: number): any[]; /** * Discard the item furthest from delivery. * * In fifo mode that is the head, which is what `drop-oldest` has always * meant. In priority mode it is the lowest-priority item — and among equal * priorities the most recently queued, because that is the one `shift()` * reaches last. The policy name stays true of the *intent* (make room by * discarding what is least worth keeping) rather than of the literal * insertion order, and the guide says so. * @returns {any} */ dropOldest(): any; clear(): void; } import { PowerPriorityQueue } from './powerPriorityQueue.js';