# Gateway channel operations ## Purpose Free-function module containing every channel-level mutation the gateway exposes — `subscribeToChannel`, `unsubscribeFromChannel`, `publishToChannel`. Extracted from `gateway.ts` so the daemon file stays under the 300-line cap; consumed via `import * as channelOps from "./channel-ops.js"` from the gateway. ## Features - `subscribeToChannel` runs the rate-limit + authorization gate before the subscribe. - `unsubscribeFromChannel` is a pure passthrough (no gating on unsubscribe — the proposal treats teardown actions as always-allowed). - `publishToChannel` gates first, then routes through the channel manager, returning the deterministic recipient set. ## Usage: Basic Example ```ts import * as channelOps from "@suaveplan/websocket-gateway/channel-ops"; await channelOps.subscribeToChannel(deps, "events", "alice", "user.created"); ``` ## Usage: Advanced Example ```ts import * as channelOps from "@suaveplan/websocket-gateway/channel-ops"; const result = await channelOps.publishToChannel( deps, "events", "alice", "user.created", { id: "u1", at: Date.now() } ); // result.recipients contains every subscriber that matched, excluding alice. ``` ## API Reference - `subscribeToChannel(deps, channel, clientId, topic): Promise` - `unsubscribeFromChannel(mgr, channel, clientId, topic): boolean` - `publishToChannel(deps, channel, sender, topic, payload): Promise<{message, recipients} | null>` - `ChannelOpsDeps` — structurally-typed dependency bundle. ## Implementation Notes `unsubscribe` does not consult the gate because rejecting an unsubscribe would leave the client subscribed when they explicitly asked to stop receiving — a worse failure mode than allowing the teardown. `publishToChannel` returns `null` when the channel hasn't been registered (no subscribers exist yet); the daemon translates that into a no-op rather than a typed error so empty channels are silently ignored. The dep bundle is structural (`ChannelOpsDeps`), not nominal. Tests that exercise the ops module directly can construct minimal bundles wiring only the parts each test needs — the gate closures, the requireAuthCtx fallback, the clock — without instantiating the full daemon. The factoring follows the same pattern `room-ops.ts` uses and keeps the gateway file under the 300-line cap. The `subscribe` path is the only place a channel gets created today; the `getOrCreateChannel` call hides inside `ChannelManager.subscribe`. This keeps the wire protocol implicit: a client subscribes, the channel exists. Publishes to never-subscribed channels stay no-ops by design — a future stronger contract (rejecting publishes to unknown channels) is a follow-up policy, not a current requirement.