import type { AbstractActionCreator, LoguxSubscribeAction, LoguxUnsubscribeAction } from '@logux/actions' import type { Action, AnyAction, ID, Log, LogStore, Meta, ServerConnection, TestTime } from '@logux/core' import type { Unsubscribe } from 'nanoevents' import type { Server as HTTPServer, IncomingMessage, ServerResponse } from 'node:http' import type { WebSocket } from 'ws' import type { ChannelContext, ConnectContext, Context } from '../context/index.js' import type { ServerClient } from '../server-client/index.js' interface LogFn { (...objs: unknown[]): void } interface TypeOptions { /** * Name of the queue that will be used to process actions * of the specified type. Default is 'main' */ queue?: string } interface ChannelOptions { /** * Name of the queue that will be used to process channels * with the specified name pattern. Default is 'main' */ queue?: string } interface ConnectLoader { ( ctx: ConnectContext, lastSynced: number ): | [Action, ServerMeta][] | Promise< [ Action, Partial> & Pick ][] > } type ServerNodeConstructor = new (...args: unknown[]) => ServerNode export interface ServerMeta extends Meta { /** * All nodes subscribed to listed channels will receive the action. */ channels?: string[] /** * All nodes with listed client IDs will receive the action. */ clients?: string[] /** * Client IDs, which will not receive the action. */ excludeClients?: string[] /** * All nodes with listed node IDs will receive the action. */ nodes?: string[] /** * Node ID of the server received the action. */ server: string /** * All nodes with listed user IDs will receive the action. */ users?: string[] } export interface BaseServerOptions { /** * SSL certificate or path to it. Path could be relative from server * root. It is required in production mode, because WSS is highly * recommended. */ cert?: string /** * Regular expression which should be cleaned from error message and stack. * * By default it cleans `Bearer [^\s"]+`. */ cleanFromLog?: RegExp /** * Disable health check endpoint, {@link Server#http}. * * The server will process only WebSocket connection and ignore all other * HTTP request (so they can be processed by other HTTP server). */ disableHttpServer?: boolean /** * Development or production server mode. By default, * it will be taken from `NODE_ENV` environment variable. * On empty `NODE_ENV` it will be `'development'`. */ env?: 'development' | 'production' /** * URL of main JS file in the root dir for the cases where you can’t use * `import.meta.dirname`. * * ``` * fileUrl: import.meta.url * ``` */ fileUrl?: string /** * IP-address to bind server. Default is `127.0.0.1`. */ host?: string /** * Custom random ID to be used in node ID. */ id?: string /** * SSL key or path to it. Path could be relative from server root. * It is required in production mode, because WSS is highly recommended. */ key?: { pem: string } | string /** * The version requirements for client subprotocol version. */ minSubprotocol?: number /** * Replace class for ServerNode. */ Node?: ServerNodeConstructor /** * Process ID, to display in logs. */ pid?: number /** * Milliseconds since last message to test connection by sending ping. * Default is `20000`. */ ping?: number /** * Port to bind server. It will create HTTP server manually to connect * WebSocket server to it. Default is `31337`. */ port?: number | string /** * URL to Redis for Logux Server Pro scaling. */ redis?: string /** * Application root to load files and show errors. * Default is `process.cwd()`. * * ```js * root: import.meta.dirname * ``` */ root?: string /** * HTTP server to serve Logux’s WebSocket and HTTP requests. * * Logux will remove previous HTTP callbacks. Do not use it with Express.js * or other HTTP servers with defined routes. */ server?: HTTPServer /** * Store to save log. Will be {@link @logux/core:MemoryStore}, by default. */ store?: LogStore /** * Server current application subprotocol version. */ subprotocol?: number /** * How long an action can stay in the processing queue of the client * before the server answers `logux/undo` and moves to the next action. * `300000` (5 minutes) by default, `0` disables it. */ queueTimeout?: number /** * How many actions could be sent in a single message. `100` by default. * * Actions from a single `Server#log.add()` call (or from `ctx.sendBack()` * with an array) are sent together and will be split by this limit. */ syncBatch?: number /** * Test time to test server. */ time?: TestTime /** * Timeout in milliseconds to disconnect connection. * Default is `70000`. */ timeout?: number } export interface AuthenticatorOptions< Headers extends object, ClientData extends object > { client: ServerClient cookie: Record headers: Headers token: string userId: string } export type SendBackActions = | (Action | [Action, Partial])[] | Action | void /** * The authentication callback. * * @param userId User ID. * @param token The client credentials. * @param client Client object. * @returns `true` if credentials was correct */ interface ServerAuthenticator< Headers extends object, ClientData extends object > { (user: AuthenticatorOptions): boolean | Promise } /** * Check does user can do this action. * * @param ctx Information about node, who create this action. * @param action The action data. * @param meta The action metadata. * @returns `true` if client are allowed to use this action. */ interface Authorizer< TypeAction extends Action, Data extends object, Headers extends object > { ( ctx: Context, action: Readonly, meta: Readonly ): boolean | Promise } /** * Return object with keys for meta to resend action to other users. * * It is called only for the actions from the clients and * from {@link BaseServer#process}. Facts, which were added * by `Server#log.add()`, are routed by their meta alone. * * @param ctx Information about node, who create this action. * @param action The action data. * @param meta The action metadata. * @returns Meta’s keys. */ interface Resender< TypeAction extends Action, Data extends object, Headers extends object > { ( ctx: Context, action: Readonly, meta: Readonly ): Promise | Resend } /** * Action business logic. * * @param ctx Information about node, who create this action. * @param action The action data. * @param meta The action metadata. * @returns Promise when processing will be finished. */ interface Processor< TypeAction extends Action, Data extends object, Headers extends object > { ( ctx: Context, action: Readonly, meta: Readonly ): Promise | void } /** * Callback which will be run on the end of action/subscription * processing or on an error. * * @param ctx Information about node, who create this action. * @param action The action data. * @param meta The action metadata. */ interface ActionFinally< TypeAction extends Action, Data extends object, Headers extends object > { ( ctx: Context, action: Readonly, meta: Readonly ): void } /** * Channel filter callback * * @param ctx Information about node, who create this action. * @param action The action data. * @param meta The action metadata. * @returns Should action be sent to client. */ interface ChannelFilter { ( ctx: Context, action: Readonly, meta: Readonly ): boolean | Promise } /** * Channel authorizer callback * * @param ctx Information about node, who create this action. * @param action The action data. * @param meta The action metadata. * @returns `true` if client are allowed to subscribe to this channel. */ interface ChannelAuthorizer< SubscribeAction extends Action, Data extends object, ChannelParams extends object | string[], Headers extends object > { ( ctx: ChannelContext, action: Readonly, meta: Readonly ): boolean | Promise } /** * Generates custom filter for channel’s actions. * * @param ctx Information about node, who create this action. * @param action The action data. * @param meta The action metadata. * @returns Actions filter. */ interface FilterCreator< SubscribeAction extends Action, Data extends object, ChannelParams extends object | string[], Headers extends object > { ( ctx: ChannelContext, action: Readonly, meta: Readonly ): ChannelFilter | Promise> | void } /** * Send actions with current state. * * @param ctx Information about node, who create this action. * @param action The action data. * @param meta The action metadata. * @returns Promise during current actions loading. */ interface ChannelLoader< SubscribeAction extends Action, Data extends object, ChannelParams extends object | string[], Headers extends object > { ( ctx: ChannelContext, action: Readonly, meta: Readonly ): Promise | SendBackActions } /** * Callback which will be run on the end of subscription * processing or on an error. * * @param ctx Information about node, who create this action. * @param action The action data. * @param meta The action metadata. */ interface ChannelFinally< SubscribeAction extends Action, Data extends object, ChannelParams extends object | string[], Headers extends object > { ( ctx: ChannelContext, action: Readonly, meta: Readonly ): void } /** * Callback which will be called on listener unsubscribe * (with explicit intent or because of disconnect) * * @param ctx Information about node, who create this action. * @param action The action data. * @param meta The action metadata. */ interface ChannelUnsubscribe< Data extends object, ChannelParams extends object | string[], Headers extends object > { ( ctx: ChannelContext, action: LoguxUnsubscribeAction, meta: Readonly ): void } type ActionCallbacks< TypeAction extends Action, Data extends object, Headers extends object > = ( | { access: Authorizer process?: Processor } | { accessAndProcess: Processor } ) & { finally?: ActionFinally resend?: Resender } type ChannelCallbacks< SubscribeAction extends Action, Data extends object, ChannelParams extends object | string[], Headers extends object > = ( | { access: ChannelAuthorizer load?: ChannelLoader } | { accessAndLoad: ChannelLoader< SubscribeAction, Data, ChannelParams, Headers > } ) & { filter?: FilterCreator finally?: ChannelFinally unsubscribe?: ChannelUnsubscribe } interface ActionReporter { action: Readonly meta: Readonly } interface SubscriptionReporter { actionId: ID channel: string } interface CleanReporter { actionId: ID } interface AuthenticationReporter { connectionId: string nodeId: string subprotocol: string } interface ReportersArguments { add: ActionReporter addClean: ActionReporter authenticated: AuthenticationReporter clean: CleanReporter clientError: { connectionId?: string err: Error nodeId?: string } connect: { connectionId: string ipAddress: string } denied: CleanReporter destroy: void disconnect: { connectionId?: string nodeId?: string } error: { actionId?: ID connectionId?: string err: Error fatal?: true nodeId?: string } listen: { cert: boolean environment: 'development' | 'production' host: string loguxServer: string minSubprotocol: number nodeId: string notes: object port: string redis: string server: boolean subprotocol: number } processed: { actionId: ID latency: number } subscribed: SubscriptionReporter unauthenticated: AuthenticationReporter unknownType: { actionId: ID type: string } unsubscribed: SubscriptionReporter useless: ActionReporter duplicate: CleanReporter destroyDetached: { actions: number } wrongChannel: SubscriptionReporter zombie: { nodeId: string } } export interface Reporter { ( event: Event, payload: ReportersArguments[Event] ): void } export type Resend = | { channels?: string[] clients?: string[] excludeClients?: string[] nodes?: string[] users?: string[] } | string | string[] export interface Logger { debug(details: object, message: string): void error(details: object, message: string): void fatal(details: object, message: string): void info(details: object, message: string): void warn(details: object, message: string): void } /** * Return `false` if `cb()` got response error with 403. * * ```js * import { wasNot403 } from '@logux/server' * * server.auth(({ userId, token }) => { * return wasNot403(async () => { * get(`/checkUser/${userId}/${token}`) * }) * }) * ``` * * @param cb Callback with `request` calls. */ export function wasNot403(cb: () => Promise): Promise /** * Base server class to extend. */ export class BaseServer< Headers extends object = unknown, ServerLog extends Log = Log, ClientData extends object = unknown > { /** * Connected client by client ID. * * Do not rely on this data, when you have multiple Logux servers. * Each server will have a different list. */ clientIds: Map> /** * Connected clients. * * ```js * for (let client of server.connected.values()) { * console.log(client.remoteAddress) * } * ``` */ connected: Map> /** * Production or development mode. * * ```js * if (server.env === 'development') { * logDebugData() * } * ``` */ env: 'development' | 'production' /** * Server actions log. * * ```js * server.log.each(finder) * ``` * * Adding an action delivers it by `meta.channels`, `users`, `clients`, * `nodes`. Callbacks from {@link BaseServer#type} are not called, * use {@link BaseServer#process} for that. * * ```js * server.log.add({ type: 'users/renamed', userId, name }, { * channels: [`users/${userId}`] * }) * ``` */ log: ServerLog /** * Console for custom log records. It uses `pino` API. * * ```js * server.on('connected', client => { * server.logger.info( * { domain: client.httpHeaders.domain }, * 'Client domain' * ) * }) * ``` */ logger: { debug: LogFn error: LogFn fatal: LogFn info: LogFn warn: LogFn } /** * Server unique ID. * * ```js * console.log('Error was raised on ' + server.nodeId) * ``` */ nodeId: string /** * Connected client by node ID. * * Do not rely on this data, when you have multiple Logux servers. * Each server will have a different list. */ nodeIds: Map> /** * Server options. * * ```js * console.log('Server options', server.options.subprotocol) * ``` */ options: BaseServerOptions /** * Clients subscribed to some channel. * * Do not rely on this data, when you have multiple Logux servers. * Each server will have a different list. */ subscribers: { [channel: string]: { [nodeId: string]: { filters: Record | true> unsubscribe?: (action: LoguxUnsubscribeAction, meta: ServerMeta) => void } } } /** * Connected client by user ID. * * Do not rely on this data, when you have multiple Logux servers. * Each server will have a different list. */ userIds: Map[]> /** * @param opts Server options. */ constructor(opts: BaseServerOptions) /** * Add new client for server. You should call this method manually * mostly for test purposes. * * ```js * server.addClient(test.right) * ``` * * @param connection Logux connection to client. * @returns Client ID. */ addClient(connection: ServerConnection): number /** * Set authenticate function. It will receive client credentials * and node ID. It should return a Promise with `true` or `false`. * * ```js * server.auth(async ({ userId, cookie }) => { * const user = await findUserByToken(cookie.token) * return !!user && userId === user.id * }) * ``` * * @param authenticator The authentication callback. */ auth(authenticator: ServerAuthenticator): void /** * Define the channel. * * ```js * server.channel('user/:id', { * access (ctx, action, meta) { * return ctx.params.id === ctx.userId * } * filter (ctx, action, meta) { * return (otherCtx, otherAction, otherMeta) => { * return !action.hidden * } * } * async load (ctx, action, meta) { * const user = await db.loadUser(ctx.params.id) * ctx.sendBack({ type: 'USER_NAME', name: user.name }) * } * }) * ``` * * @param pattern Pattern for channel name. * @param callbacks Callback during subscription process. * @param options Additional options */ channel< ChannelParams extends object = unknown, Data extends object = unknown, SubscribeAction extends LoguxSubscribeAction = LoguxSubscribeAction >( pattern: string, callbacks: ChannelCallbacks, options?: ChannelOptions ): void /** * @param pattern Regular expression for channel name. * @param callbacks Callback during subscription process. * @param options Additional options */ channel< ChannelParams extends string[] = string[], Data extends object = unknown, SubscribeAction extends LoguxSubscribeAction = LoguxSubscribeAction >( pattern: RegExp, callbacks: ChannelCallbacks, options?: ChannelOptions ): void /** * Send runtime error stacktrace to all clients. * * ```js * process.on('uncaughtException', e => { * server.debugError(e) * }) * ``` * * @param error Runtime error instance. */ debugError(error: Error): void /** * Stop server and unbind all listeners. * * ```js * afterEach(() => { * testServer.destroy() * }) * ``` * * @returns Promise when all listeners will be removed. */ destroy(): Promise /** * Wait until the client will confirm all actions, which were sent to it. * * Use it to send a long history without loading it all into the memory: * the client’s speed will limit how fast you read the database. * * ```js * while (await server.drain(clientId)) { * let page = await cursor.next(100) * if (!page.length) break * server.log.add(page.map(i => [i.action, { clients: [clientId] }])) * } * ``` * * @param clientId Client ID. * @returns Promise with `false` if the client was disconnected. */ drain(clientId: string): Promise /** * Handle WebSocket connection explicitly * * This is a low-level method allowing to integrate Logux server with an existing server * * ```js * fastify.get('/', { websocket: true }, (socket, req) => { * loguxServer.handleClient(socket, req) * }) * ``` */ handleClient(ws: WebSocket, req: IncomingMessage): void /** * Add non-WebSocket HTTP request processor. * * ```js * server.http('GET', '/auth', (req, res) => { * let token = signIn(req) * if (token) { * res.setHeader('Set-Cookie', `token=${token}; Secure; HttpOnly`) * res.end() * } else { * res.statusCode = 400 * res.end('Wrong user or password') * } * }) * ``` * * The URL can contain `*` to match the rest of the path, including `/`. * * Listeners are checked in the order they were added, so add the specific * URLs before the patterns. Adding a listener for the same method and URL * replaces the previous one: it allows to override the built-in pages * like `/` or `/health`. */ http( method: string, url: string, listener: ( req: IncomingMessage, res: ServerResponse ) => Promise | void ): void http( listener: ( req: IncomingMessage, res: ServerResponse ) => boolean | Promise ): void /** * Replace the default `404 Not found` answer for HTTP requests, which * were not processed by `Server#http()` listeners. * * ```js * server.httpNotFound((req, res) => { * res.writeHead(404, { 'Content-Type': 'application/json' }) * res.end('{"error":"Not found"}') * }) * ``` */ httpNotFound( listener: ( req: IncomingMessage, res: ServerResponse ) => Promise | void ): void /** * Start WebSocket server and listen for clients. * * @returns When the server has been bound. */ listen(): Promise /** * @param event The event name. * @param listener Event listener. */ on(event: 'subscriptionCancelled', listener: () => void): Unsubscribe /** * Server calls listeners at the end of `destroy()`, after it stopped * processing actions, one by one and waits for returned promises. * Server exits on `SIGINT` and `SIGTERM` only after them. * * ```js * server.on('destroyed', async () => { * await db.close() * }) * ``` * * @param event The event name. * @param listener Event listener. */ on(event: 'destroyed', listener: () => Promise | void): Unsubscribe /** * @param event The event name. * @param listener Subscription listener. */ on( event: 'subscribing', listener: (action: LoguxSubscribeAction, meta: Readonly) => void ): Unsubscribe /** * @param event The event name. * @param listener Processing listener. */ on( event: 'processed', listener: ( action: Action, meta: Readonly, latencyMilliseconds: number ) => void ): Unsubscribe /** * @param event The event name. * @param listener Action listener. */ on( event: 'add' | 'clean', listener: (action: Action, meta: Readonly) => void ): Unsubscribe /** * @param event The event name. * @param listener Client listener. */ on( event: 'connected' | 'disconnected', listener: (client: ServerClient) => void ): Unsubscribe /** * Subscribe for synchronization events. It implements nanoevents API. * Supported events: * * * `error`: server error during action processing. * * `fatal`: server error during loading. * * `clientError`: wrong client behaviour. * * `connected`: new client was connected. * * `disconnected`: client was disconnected. * * `authenticated`: client was authenticated. * * `preadd`: action is going to be added to the log. * The best place to set `reasons`. * * `add`: action was added to the log. * * `clean`: action was cleaned from the log. * * `processed`: action processing was finished. * * `subscribed`: channel initial data was loaded. * * `subscribing`: channel initial data started to be loaded. * * `unsubscribed`: node was unsubscribed. * * `subscriptionCancelled`: subscription was cancelled because the client * is not connected. * * ```js * server.on('error', error => { * trackError(error) * }) * ``` * * @param event The event name. * @param listener The listener function. * @returns Unbind listener from event. */ on( event: 'clientError' | 'fatal', listener: (err: Error) => void ): Unsubscribe /** * @param event The event name. * @param listener Error listener. */ on( event: 'error', listener: (err: Error, action: Action, meta: Readonly) => void ): Unsubscribe /** * @param event The event name. * @param listener Client listener. */ on( event: 'authenticated' | 'unauthenticated', listener: ( client: ServerClient, latencyMilliseconds: number ) => void ): Unsubscribe /** * @param event The event name. * @param listener Action listener. */ on( event: 'preadd', listener: (action: Action, meta: ServerMeta) => void ): Unsubscribe /** * @param event The event name. * @param listener Subscription listener. */ on( event: 'subscribed', listener: ( action: LoguxSubscribeAction, meta: Readonly, latencyMilliseconds: number ) => void ): Unsubscribe /** * @param event The event name. * @param listener Subscription listener. */ on( event: 'unsubscribed', listener: ( action: LoguxUnsubscribeAction, meta: Readonly, clientNodeId: string ) => void ): Unsubscribe /** * @param event The event name. * @param listener Report listener. */ on(event: 'report', listener: Reporter): Unsubscribe /** * Set callbacks for unknown channel subscription. * *```js * server.otherChannel({ * async access (ctx, action, meta) { * const res = await phpBackend.checkChannel(ctx.params[0], ctx.userId) * if (res.code === 404) { * this.wrongChannel(action, meta) * return false * } else { * return response.body === 'granted' * } * } * }) * ``` * * @param callbacks Callback during subscription process. */ otherChannel( callbacks: ChannelCallbacks ): void /** * Define callbacks for actions, which type was not defined * by any {@link Server#type}. Useful for proxy or some hacks. * * Without this settings, server will call {@link Server#unknownType} * on unknown type. * * ```js * server.otherType( * async access (ctx, action, meta) { * const response = await phpBackend.checkByHTTP(action, meta) * if (response.code === 404) { * this.unknownType(action, meta) * return false * } else { * return response.body === 'granted' * } * } * async process (ctx, action, meta) { * return await phpBackend.sendHTTP(action, meta) * } * }) * ``` * * @param callbacks Callbacks for actions with this type. */ otherType( callbacks: ActionCallbacks ): void /** * Add new action to the server and return the Promise until it will be * resend to clients and processed. * * @param action New action to resend and process. * @param meta Action’s meta. * @returns Promise with the action’s meta. It will be rejected * with the error of the failed callback. */ process( action: TypeAction, meta?: Partial ): Promise> /** * Send an action, which was already processed by another server, * to the clients of this server. The action is not written to the log * and no callback from {@link BaseServer#type} is called. * * This method is for multi-server configuration only. Use * `Server#log.add()` to also store the action in this server’s log. * * ```js * server.on('add', (action, meta) => { * if (meta.server === server.nodeId) { * sendToOtherServers(action, meta) * } * }) * onReceivingFromOtherServer((action, meta) => { * server.sendWithoutProcess(action, meta) * }) * ``` * * @param action New action. * @param meta Action’s metadata. */ sendWithoutProcess(action: Action, meta: ServerMeta): Promise | void /** * Change a way how server loads actions history for the client. * * ```js * server.sendOnConnect(async (ctx, lastSynced) => { * return db.loadActions({ user: ctx.userId, after: lastSynced }) * }) * ``` * * Actions should be returned from the newest one to the oldest one. * The list will be split into messages by the `syncBatch` option, but it * is loaded into the memory as a whole. For a big history send it by * pages with `Context#sendBack()` and `Context#drain()` instead. * * Set `meta.added` to let the client ask only for newer actions after * the reconnect: the biggest `added` will be sent as the sync position. * * Like the default history, the returned actions are prefixed * by `logux/prepare`. * * @param loader Callback which loads list of actions and meta. */ sendOnConnect(loader: ConnectLoader): void /** * Send `logux/subscribed` if client was not already subscribed. * * ```js * server.subscribe(ctx.nodeId, `users/${loaded}`) * ``` * * @param nodeId Node ID. * @param channel Channel name. * @returns Promise until `logux/subscribed` was published. */ subscribe(nodeId: string, channel: string): Promise /** * @param actionCreator Action creator function. * @param callbacks Callbacks for action created by creator. * @param options Additional options */ type( actionCreator: Creator, callbacks: ActionCallbacks, Data, Headers>, options?: TypeOptions ): void /** * Define action type’s callbacks. * * ```js * server.type('CHANGE_NAME', { * access (ctx, action, meta) { * return action.user === ctx.userId * }, * resend (ctx, action) { * return `user/${ action.user }` * } * process (ctx, action, meta) { * if (isFirstOlder(lastNameChange(action.user), meta)) { * return db.changeUserName({ id: action.user, name: action.name }) * } * } * }) * ``` * * @param name The action’s type or action’s type matching rule as RegExp.. * @param callbacks Callbacks for actions with this type. * @param options Additional options */ type( name: RegExp | TypeAction['type'], callbacks: ActionCallbacks, options?: TypeOptions ): void /** * Undo action from client. * * ```js * if (couldNotFixConflict(action, meta)) { * server.undo(action, meta) * } * ``` * * During the action processing it sets the outcome of the current task: * the callback can not answer `logux/processed` after it. Outside * of the processing it publishes the compensating action. * * @param action The original action to undo. * @param meta The action’s metadata. * @param reason Optional code for reason. Default is `'error'`. * @param extra Extra fields to `logux/undo` action. * @returns When the answer was published. */ undo( action: Action, meta: ServerMeta, reason?: string, extra?: object ): Promise /** * If you receive action with unknown type, this method will undo it * on the clients. * * If you didn’t set {@link Server#otherType}, * Logux will call it automatically. * * ```js * server.otherType({ * access (ctx, action, meta) { * if (action.type.startsWith('myapp/')) { * return proxy.access(action, meta) * } else { * server.unknownType(action, meta) * } * } * }) * ``` * * @param action The action with unknown type. * @param meta Action’s metadata. */ unknownType(action: Action, meta: ServerMeta): void /** * Report that client try to subscribe for unknown channel. * * Logux call it automatically, * if you will not set {@link Server#otherChannel}. * * ```js * server.otherChannel({ * async access (ctx, action, meta) { * const res = phpBackend.checkChannel(params[0], ctx.userId) * if (res.code === 404) { * this.wrongChannel(action, meta) * return false * } else { * return response.body === 'granted' * } * } * }) * ``` * * @param action The subscribe action. * @param meta Action’s metadata. */ wrongChannel(action: LoguxSubscribeAction, meta: ServerMeta): void }