-
Notifications
You must be signed in to change notification settings - Fork 452
feat: [ENG-2930] brv channel transport events + validating handler skeleton #747
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| /** | ||
| * Canonical wire codes for channel-domain errors. | ||
| * | ||
| * The daemon transport forwards a thrown {@link ChannelError}'s `code` verbatim | ||
| * in its `{success: false, code, error}` envelope, so these strings are part of | ||
| * the client-facing contract. Trimmed to the codes the skeleton needs; more | ||
| * land with the milestones that introduce them. | ||
| */ | ||
| export const CHANNEL_ERROR_CODE = { | ||
| DISABLED: 'CHANNEL_DISABLED', | ||
| INVALID_REQUEST: 'CHANNEL_INVALID_REQUEST', | ||
| NOT_IMPLEMENTED: 'CHANNEL_NOT_IMPLEMENTED' | ||
| } as const | ||
|
|
||
| /** | ||
| * Represents any channel-domain error. Carries the canonical wire `code` and | ||
| * optional structured `details` for the transport error envelope. | ||
| */ | ||
| export class ChannelError extends Error { | ||
| public readonly code: string | ||
| public readonly details?: unknown | ||
|
|
||
| public constructor(message: string, code: string, details?: unknown) { | ||
| super(message) | ||
| this.name = 'ChannelError' | ||
| this.code = code | ||
| this.details = details | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Signals that a `channel:*` event is registered but has no behavior yet. The | ||
| * skeleton throws this from every handler once the payload validates, so the | ||
| * wire surface stays stable while later milestones supply behavior. | ||
| */ | ||
| export class ChannelNotImplementedError extends ChannelError { | ||
| public constructor(event?: string) { | ||
| super( | ||
| event === undefined | ||
| ? 'channel operation is not implemented yet' | ||
| : `channel operation '${event}' is not implemented yet`, | ||
| CHANNEL_ERROR_CODE.NOT_IMPLEMENTED | ||
| ) | ||
| this.name = 'ChannelNotImplementedError' | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Signals that the channel surface is administratively disabled on this host | ||
| * (`BRV_CHANNELS_ENABLED` is set to a falsy value). A stub throwing this is | ||
| * registered for every `channel:*` event so the Socket.IO ack still fires | ||
| * instead of hanging. | ||
| */ | ||
| export class ChannelDisabledError extends ChannelError { | ||
| public constructor(message?: string) { | ||
| super( | ||
| message ?? | ||
| 'channel surface is disabled on this host (BRV_CHANNELS_ENABLED is set to a falsy value)', | ||
| CHANNEL_ERROR_CODE.DISABLED, | ||
| ) | ||
| this.name = 'ChannelDisabledError' | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Signals that a request payload failed schema validation at the wire boundary. | ||
| * `details` carries the flattened zod error so clients can surface specific | ||
| * field problems. | ||
| */ | ||
| export class ChannelInvalidRequestError extends ChannelError { | ||
| public constructor(message: string, details?: unknown) { | ||
| super(message, CHANNEL_ERROR_CODE.INVALID_REQUEST, details) | ||
| this.name = 'ChannelInvalidRequestError' | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| import type {ITransportServer} from '../../../core/interfaces/transport/index.js' | ||
|
|
||
| import {ChannelEvents} from '../../../../shared/transport/events/channel-events.js' | ||
| import {ChannelDisabledError} from '../../../core/domain/channel/errors.js' | ||
|
|
||
| /** Every `channel:*` request event the live per-event handlers register. */ | ||
| const STUBBABLE_EVENTS = [ | ||
| ChannelEvents.CREATE, | ||
| ChannelEvents.LIST, | ||
| ChannelEvents.GET, | ||
| ChannelEvents.INVITE, | ||
| ChannelEvents.ONBOARD, | ||
| ChannelEvents.MENTION, | ||
| ChannelEvents.SHOW, | ||
| ChannelEvents.LIST_TURNS, | ||
| ChannelEvents.SUBSCRIBE, | ||
| ChannelEvents.CANCEL, | ||
| ] as const | ||
|
|
||
| /** | ||
| * Registers a stub for every `channel:*` request event that throws | ||
| * {@link ChannelDisabledError}. Without it, a `channel:*` request sent while the | ||
| * surface is disabled would never receive an ack and the client would hang. | ||
| * Returns the list of stubbed events. | ||
| */ | ||
| export const registerDisabledStubs = (transport: ITransportServer): readonly string[] => { | ||
| for (const event of STUBBABLE_EVENTS) { | ||
| transport.onRequest(event, () => { | ||
| throw new ChannelDisabledError() | ||
| }) | ||
| } | ||
|
|
||
| return STUBBABLE_EVENTS | ||
| } | ||
|
|
||
| /** | ||
| * Reports whether the channel surface is enabled. Opt-out: enabled unless | ||
| * `BRV_CHANNELS_ENABLED` is `0`, `false`, `no`, or `off` (case-insensitive). | ||
| */ | ||
| export const channelsEnabled = (env: NodeJS.ProcessEnv = process.env): boolean => { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nitpick (code quality): Default-arg evaluation is fine here, but the gate is only consulted once at boot in |
||
| const value = env.BRV_CHANNELS_ENABLED | ||
| if (value === undefined) return true | ||
| return !['0', 'false', 'no', 'off'].includes(value.trim().toLowerCase()) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| import type {ITransportServer} from '../../../../core/interfaces/transport/index.js' | ||
| import type {ITransportHandler} from '../i-transport-handler.js' | ||
|
|
||
| import { | ||
| ChannelCancelRequest, | ||
| ChannelCancelRequestSchema, | ||
| ChannelCancelResponse, | ||
| ChannelEvents, | ||
| } from '../../../../../shared/transport/events/channel-events.js' | ||
| import {ChannelNotImplementedError} from '../../../../core/domain/channel/errors.js' | ||
| import {parseOrThrow} from './parse-or-throw.js' | ||
|
|
||
| /** | ||
| * Handles `channel:cancel`. `handle` validates the payload, then throws | ||
| * {@link ChannelNotImplementedError}; the cancel behavior lands in a later | ||
| * milestone, written after the validation step in `handle`. | ||
| */ | ||
| export class ChannelCancelHandler | ||
| implements ITransportHandler<ChannelCancelRequest, ChannelCancelResponse> | ||
| { | ||
| private readonly event: string | ||
| private readonly transportServer: ITransportServer | ||
|
|
||
| public constructor(transportServer: ITransportServer) { | ||
| this.event = ChannelEvents.CANCEL | ||
| this.transportServer = transportServer | ||
| } | ||
|
|
||
| public async handle(request: ChannelCancelRequest): Promise<ChannelCancelResponse> { | ||
| parseOrThrow(ChannelCancelRequestSchema, request) | ||
| throw new ChannelNotImplementedError(this.event) | ||
| } | ||
|
|
||
| public setup(): void { | ||
| this.transportServer.onRequest<ChannelCancelRequest, ChannelCancelResponse>(this.event, (request) => | ||
| this.handle(request), | ||
| ) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,25 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import type {ITransportServer} from "../../../../core/interfaces/transport/index.js"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import type {ITransportHandler} from "../i-transport-handler.js"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import {type ChannelCreateRequest, ChannelCreateRequestSchema, type ChannelCreateResponse, ChannelEvents} from "../../../../../shared/transport/events/channel-events.js"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import {ChannelNotImplementedError} from "../../../../core/domain/channel/errors.js"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import {parseOrThrow} from "./parse-or-throw.js"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export class ChannelCreateHandler implements ITransportHandler<ChannelCreateRequest, ChannelCreateResponse> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private readonly event: string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private readonly transportServer: ITransportServer | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public constructor(transportServer: ITransportServer) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.event = ChannelEvents.CREATE | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.transportServer = transportServer | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public async handle(request: ChannelCreateRequest): Promise<ChannelCreateResponse> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| parseOrThrow(ChannelCreateRequestSchema, request) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new ChannelNotImplementedError(this.event) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public setup(): void { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.transportServer.onRequest<ChannelCreateRequest, ChannelCreateResponse>(this.event, request => this.handle(request)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1
to
+25
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue (style): This file is the odd one out — all 9 sibling handlers use single quotes, no semicolons, multi-line imports, a class-level JSDoc, and
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| import type {ITransportServer} from '../../../../core/interfaces/transport/index.js' | ||
| import type {ITransportHandler} from '../i-transport-handler.js' | ||
|
|
||
| import { | ||
| ChannelEvents, | ||
| ChannelGetRequest, | ||
| ChannelGetRequestSchema, | ||
| ChannelGetResponse, | ||
| } from '../../../../../shared/transport/events/channel-events.js' | ||
| import {ChannelNotImplementedError} from '../../../../core/domain/channel/errors.js' | ||
| import {parseOrThrow} from './parse-or-throw.js' | ||
|
|
||
| /** | ||
| * Handles `channel:get`. `handle` validates the payload, then throws | ||
| * {@link ChannelNotImplementedError}; the get behavior lands in a later | ||
| * milestone, written after the validation step in `handle`. | ||
| */ | ||
| export class ChannelGetHandler implements ITransportHandler<ChannelGetRequest, ChannelGetResponse> { | ||
| private readonly event: string | ||
| private readonly transportServer: ITransportServer | ||
|
|
||
| public constructor(transportServer: ITransportServer) { | ||
| this.event = ChannelEvents.GET | ||
| this.transportServer = transportServer | ||
| } | ||
|
|
||
| public async handle(request: ChannelGetRequest): Promise<ChannelGetResponse> { | ||
| parseOrThrow(ChannelGetRequestSchema, request) | ||
| throw new ChannelNotImplementedError(this.event) | ||
| } | ||
|
|
||
| public setup(): void { | ||
| this.transportServer.onRequest<ChannelGetRequest, ChannelGetResponse>(this.event, (request) => | ||
| this.handle(request), | ||
| ) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| import type {ITransportServer} from '../../../../core/interfaces/transport/index.js' | ||
| import type {ITransportHandler} from '../i-transport-handler.js' | ||
|
|
||
| import { | ||
| ChannelEvents, | ||
| ChannelInviteRequest, | ||
| ChannelInviteRequestSchema, | ||
| ChannelInviteResponse, | ||
| } from '../../../../../shared/transport/events/channel-events.js' | ||
| import {ChannelNotImplementedError} from '../../../../core/domain/channel/errors.js' | ||
| import {parseOrThrow} from './parse-or-throw.js' | ||
|
|
||
| /** | ||
| * Handles `channel:invite`. `handle` validates the payload, then throws | ||
| * {@link ChannelNotImplementedError}; the invite behavior lands in a later | ||
| * milestone, written after the validation step in `handle`. | ||
| */ | ||
| export class ChannelInviteHandler | ||
| implements ITransportHandler<ChannelInviteRequest, ChannelInviteResponse> | ||
| { | ||
| private readonly event: string | ||
| private readonly transportServer: ITransportServer | ||
|
|
||
| public constructor(transportServer: ITransportServer) { | ||
| this.event = ChannelEvents.INVITE | ||
| this.transportServer = transportServer | ||
| } | ||
|
|
||
| public async handle(request: ChannelInviteRequest): Promise<ChannelInviteResponse> { | ||
| parseOrThrow(ChannelInviteRequestSchema, request) | ||
| throw new ChannelNotImplementedError(this.event) | ||
| } | ||
|
|
||
| public setup(): void { | ||
| this.transportServer.onRequest<ChannelInviteRequest, ChannelInviteResponse>(this.event, (request) => | ||
| this.handle(request), | ||
| ) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| import type {ITransportServer} from '../../../../core/interfaces/transport/index.js' | ||
| import type {ITransportHandler} from '../i-transport-handler.js' | ||
|
|
||
| import { | ||
| ChannelEvents, | ||
| ChannelListRequest, | ||
| ChannelListRequestSchema, | ||
| ChannelListResponse, | ||
| } from '../../../../../shared/transport/events/channel-events.js' | ||
| import {ChannelNotImplementedError} from '../../../../core/domain/channel/errors.js' | ||
| import {parseOrThrow} from './parse-or-throw.js' | ||
|
|
||
| /** | ||
| * Handles `channel:list`. `handle` validates the payload, then throws | ||
| * {@link ChannelNotImplementedError}; the list behavior lands in a later | ||
| * milestone, written after the validation step in `handle`. | ||
| */ | ||
| export class ChannelListHandler implements ITransportHandler<ChannelListRequest, ChannelListResponse> { | ||
| private readonly event: string | ||
| private readonly transportServer: ITransportServer | ||
|
|
||
| public constructor(transportServer: ITransportServer) { | ||
| this.event = ChannelEvents.LIST | ||
| this.transportServer = transportServer | ||
| } | ||
|
|
||
| public async handle(request: ChannelListRequest): Promise<ChannelListResponse> { | ||
| parseOrThrow(ChannelListRequestSchema, request) | ||
| throw new ChannelNotImplementedError(this.event) | ||
| } | ||
|
|
||
| public setup(): void { | ||
| this.transportServer.onRequest<ChannelListRequest, ChannelListResponse>(this.event, (request) => | ||
| this.handle(request), | ||
| ) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| import type {ITransportServer} from '../../../../core/interfaces/transport/index.js' | ||
| import type {ITransportHandler} from '../i-transport-handler.js' | ||
|
|
||
| import { | ||
| ChannelEvents, | ||
| ChannelListTurnsRequest, | ||
| ChannelListTurnsRequestSchema, | ||
| ChannelListTurnsResponse, | ||
| } from '../../../../../shared/transport/events/channel-events.js' | ||
| import {ChannelNotImplementedError} from '../../../../core/domain/channel/errors.js' | ||
| import {parseOrThrow} from './parse-or-throw.js' | ||
|
|
||
| /** | ||
| * Handles `channel:list-turns`. `handle` validates the payload, then throws | ||
| * {@link ChannelNotImplementedError}; the list-turns behavior lands in a later | ||
| * milestone, written after the validation step in `handle`. | ||
| */ | ||
| export class ChannelListTurnsHandler | ||
| implements ITransportHandler<ChannelListTurnsRequest, ChannelListTurnsResponse> | ||
| { | ||
| private readonly event: string | ||
| private readonly transportServer: ITransportServer | ||
|
|
||
| public constructor(transportServer: ITransportServer) { | ||
| this.event = ChannelEvents.LIST_TURNS | ||
| this.transportServer = transportServer | ||
| } | ||
|
|
||
| public async handle(request: ChannelListTurnsRequest): Promise<ChannelListTurnsResponse> { | ||
| parseOrThrow(ChannelListTurnsRequestSchema, request) | ||
| throw new ChannelNotImplementedError(this.event) | ||
| } | ||
|
|
||
| public setup(): void { | ||
| this.transportServer.onRequest<ChannelListTurnsRequest, ChannelListTurnsResponse>( | ||
| this.event, | ||
| (request) => this.handle(request), | ||
| ) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue (maintainability):
STUBBABLE_EVENTSre-lists the same 10 events thatfeature-handlers.tsinstantiates handlers for (lines 153-162). If a newchannel:*request event is added later (e.g. M4'schannel:permission-decision), three places need to stay in sync: the per-event handler instantiation infeature-handlers.ts, this list, andChannelEventsitself. A drift here means a request event registered in the "enabled" branch silently hangs in the "disabled" branch with no ack.One option: drive both from a single source — e.g. a
CHANNEL_REQUEST_EVENTStuple exported fromchannel-events.ts, used by bothfeature-handlers.ts(as keys for the handler-instance map) andregisterDisabledStubs. Out of scope for the skeleton, but worth a TODO if you don't want to do it now.