Skip to content

Commit

Permalink
feat: add websocket types (#98)
Browse files Browse the repository at this point in the history
  • Loading branch information
Alexandre Chau authored Mar 14, 2023
1 parent 47bb092 commit 32f283b
Show file tree
Hide file tree
Showing 10 changed files with 330 additions and 4 deletions.
6 changes: 6 additions & 0 deletions src/declarations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
PublicItemTaskManager,
S3FileConfiguration,
} from './services';
import { WebsocketService } from './services/websockets/index';

declare module 'fastify' {
interface FastifyRequest {
Expand Down Expand Up @@ -135,5 +136,10 @@ declare module 'fastify' {
h5p?: {
taskManager: H5PTaskManager;
};

/**
* Websockets service
*/
websockets?: WebsocketService;
}
}
9 changes: 5 additions & 4 deletions src/services/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
export * from './app';
export * from './categories';
export * from './chat';
export * from './database';
export * from './etherpad';
export * from './file';
export * from './flag';
export * from './h5p';
export * from './etherpad';
export * from './invitation';
export * from './item-memberships';
export * from './item-tags';
export * from './items';
export * from './invitation';
export * from './members';
export * from './public';
export * from './chat';
export * from './categories';
export * from './websockets';
65 changes: 65 additions & 0 deletions src/services/websockets/api/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/**
* Client message types for the graasp websockets network API
* {@link https://github.com/graasp/graasp-plugin-websockets/blob/main/API.md}
*/
import { NotifMessage } from './message';

/**
* Client actions: describe what the client requests from the server
*/
export enum ClientActions {
/** Subscribe to a channel */
Subscribe = 'subscribe',
/** Unsubscribe from a channel */
Unsubscribe = 'unsubscribe',
/** Subscribe only to a channel and unsubscribe from all others */
SubscribeOnly = 'subscribeOnly',
/** Disconnect from the websocket system */
Disconnect = 'disconnect',
}

export type ClientAction = ClientActions | `${ClientActions}`;

/**
* Message sent by client to disconnect
*/
export interface ClientDisconnect extends NotifMessage {
action: `${ClientActions.Disconnect}`;
}

/**
* Message sent by client to subscribe to some channel
*/
export interface ClientSubscribe extends NotifMessage {
action: `${ClientActions.Subscribe}`;
topic: string;
channel: string;
}

/**
* Message sent by client to unsubscribe from some channel
*/
export interface ClientUnsubscribe extends NotifMessage {
action: `${ClientActions.Unsubscribe}`;
topic: string;
channel: string;
}

/**
* Message sent by client to subscribe to a single channel
* (i.e. it also unsubscribes it from any other channel)
*/
export interface ClientSubscribeOnly extends NotifMessage {
action: `${ClientActions.SubscribeOnly}`;
topic: string;
channel: string;
}

/**
* Client message type is union type of all client message subtypes
*/
export type ClientMessage =
| ClientDisconnect
| ClientSubscribe
| ClientUnsubscribe
| ClientSubscribeOnly;
3 changes: 3 additions & 0 deletions src/services/websockets/api/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './client';
export * from './message';
export * from './server';
28 changes: 28 additions & 0 deletions src/services/websockets/api/message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* Message types for the graasp websockets network API
* {@link https://github.com/graasp/graasp-plugin-websockets/blob/main/API.md}
*/

/**
* Message realms: describe the universe in which the message belongs to
*/
export enum Realms {
/** Notifications realm for real-time updates in Graasp */
Notif = 'notif',
}

export type Realm = Realms | `${Realms}`;

/**
* Base message shape
*/
export interface Message {
realm: Realm;
}

/**
* Base message for real-time updates and notifications in Graasp
*/
export interface NotifMessage extends Message {
realm: `${Realms.Notif}`;
}
76 changes: 76 additions & 0 deletions src/services/websockets/api/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/**
* Server message types for the graasp websockets network API
* {@link https://github.com/graasp/graasp-plugin-websockets/blob/main/API.md}
*/
import { WebsocketError } from '../errors';
import { ClientMessage } from './client';
import { NotifMessage } from './message';

/**
* Server message types: describe what the server can send to the clients
*/
export enum ServerMessageTypes {
/** Response to a previous client request */
Response = 'response',
/** Update with new data on a given channel */
Update = 'update',
/** Global broadcasts unrelated to any specific channel */
Info = 'info',
}

export type ServerMessageType = ServerMessageTypes | `${ServerMessageTypes}`;

/**
* Server response status
*/
export enum ResponseStatuses {
Success = 'success',
Error = 'error',
}

export type ResponseStatus = ResponseStatuses | `${ResponseStatuses}`;

/**
* Message sent by server as a response to a {@link ClientMessage} on success
*/
export interface SuccessServerResponse extends NotifMessage {
type: `${ServerMessageTypes.Response}`;
status: `${ResponseStatuses.Success}`;
request: ClientMessage;
}

/**
* Error message sent by server as a response to a {@link ClientMessage}
*/
export interface ErrorServerResponse extends NotifMessage {
type: `${ServerMessageTypes.Response}`;
status: `${ResponseStatuses.Error}`;
request?: ClientMessage;
error: WebsocketError;
}

export type ServerResponse = ErrorServerResponse | SuccessServerResponse;

/**
* Message sent by server for misc broadcasts unrelated to a channel
*/
export interface ServerInfo<T = unknown> extends NotifMessage {
type: `${ServerMessageTypes.Info}`;
message: string;
extra?: T;
}

/**
* Message sent by server for update notifications sent over a channel
*/
export interface ServerUpdate<T = unknown> extends NotifMessage {
type: `${ServerMessageTypes.Update}`;
topic: string;
channel: string;
body: T;
}

/**
* Server message type is union type of all server message subtypes
*/
export type ServerMessage = ServerResponse | ServerInfo | ServerUpdate;
47 changes: 47 additions & 0 deletions src/services/websockets/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* Top-level error class for websockets
* (useful e.g. to check instanceof)
*/
export abstract class WebsocketError extends Error {}

/**
* Available websocket error names
*
* @example
* ```
* if (err.name === Websocket.ErrorNames.AccessDenied) {
* // handle access denied
* }
* ```
*/
export enum ErrorNames {
AccessDenied = 'ACCESS_DENIED',
BadRequest = 'BAD_REQUEST',
NotFound = 'NOT_FOUND',
ServerError = 'SERVER_ERROR',
}

export type ErrorName = ErrorNames | `${ErrorNames}`;

export class AccessDeniedError extends WebsocketError {
name: `${ErrorNames.AccessDenied}` = ErrorNames.AccessDenied;
message = 'Websocket: Access denied for the requested resource';
}

export class BadRequestError extends WebsocketError {
name: `${ErrorNames.BadRequest}` = ErrorNames.BadRequest;
message =
'Websocket: Request message format was not understood by the server';
}

export class NotFoundError extends WebsocketError {
name: `${ErrorNames.NotFound}` = ErrorNames.NotFound;
message = 'Websocket: Requested resource not found';
}

export class ServerError extends WebsocketError {
name: `${ErrorNames.ServerError}` = ErrorNames.ServerError;
constructor(message: string) {
super((message = `Websocket: Internal server error: ${message}`));
}
}
45 changes: 45 additions & 0 deletions src/services/websockets/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import * as API from './api';
import * as Errors from './errors';
import * as Requests from './interfaces/request';

export * from './interfaces/service';

/**
* Re-export types and utility classes within the Websocket namespace
*/
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-namespace */
export namespace Websocket {
/* Errors */
export import Error = Errors.WebsocketError;
export import ErrorNames = Errors.ErrorNames;
export type ErrorName = Errors.ErrorName;
export import AccessDeniedError = Errors.AccessDeniedError;
export import BadRequestError = Errors.BadRequestError;
export import NotFoundError = Errors.NotFoundError;
export import ServerError = Errors.ServerError;
/* Requests */
export type SubscriptionRequest = Requests.SubscriptionRequest;
/* API */
export import Realms = API.Realms;
export type Realm = API.Realm;
export type Message = API.Message;
export type NotifMessage = API.NotifMessage;
export import ClientActions = API.ClientActions;
export type ClientAction = API.ClientAction;
export type ClientDisconnect = API.ClientDisconnect;
export type ClientSubscribe = API.ClientSubscribe;
export type ClientUnsubscribe = API.ClientUnsubscribe;
export type ClientSubscribeOnly = API.ClientSubscribeOnly;
export type ClientMessage = API.ClientMessage;
export import ServerMessageTypes = API.ServerMessageTypes;
export type ServerMessageType = API.ServerMessageType;
export import ResponseStatuses = API.ResponseStatuses;
export type ResponseStatus = API.ResponseStatus;
export type SuccessServerResponse = API.SuccessServerResponse;
export type ErrorServerResponse = API.ErrorServerResponse;
export type ServerResponse = API.ServerResponse;
export type ServerInfo = API.ServerInfo;
export type ServerUpdate = API.ServerUpdate;
export type ServerMessage = API.ServerMessage;
}
20 changes: 20 additions & 0 deletions src/services/websockets/interfaces/request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Member, UnknownExtra } from '@/index';

/**
* Internal request type when clients attempt to subscribe to some channel
* Allows consumers to operate on the subscription attempt in the server
*/
export interface SubscriptionRequest {
/**
* Subscription target channel name
*/
channel: string;
/**
* Member requesting a subscription
*/
member: Member<UnknownExtra>;
/**
* Rejects the subscription request with a specified error
*/
reject(error: Error): void;
}
35 changes: 35 additions & 0 deletions src/services/websockets/interfaces/service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { SubscriptionRequest } from './request';

/**
* Public WebSocket service exposed to other consumers on the server
* (e.g. other services that want to publish messages using websockets)
*/
export interface WebsocketService {
/**
* Registers a topic (a group of related channels) dedicated to the caller
* @param topic topic name, must be unique across server
* @param validateClient async function called when a client attempts to
* subscribe to a channel from this topic
*/
register(
topic: string,
validateClient: (request: SubscriptionRequest) => Promise<void>,
): this;

/**
* Publishes a message on a channel globally (incl. across server instances)
* @param topic topic name
* @param channel channel name
* @param message message to publish
*/
publish<Message>(topic: string, channel: string, message: Message): void;

/**
* Publishes a message on a channel locally (i.e. on this specific server
* instance only)
* @param topic topic name
* @param channel channel name
* @param message message to publish
*/
publishLocal<Message>(topic: string, channel: string, message: Message): void;
}

0 comments on commit 32f283b

Please sign in to comment.