cap-js-community / websocket

Exposes a WebSocket protocol via WebSocket standard or Socket.IO for CDS services
https://www.npmjs.com/package/@cap-js-community/websocket
Apache License 2.0
11 stars 1 forks source link
cap cds plugin socket-io websocket ws

@cap-js-community/websocket

npm version monthly downloads REUSE status Main CI

WebSocket Adapter for CDS

Exposes a WebSocket protocol via WebSocket standard or Socket.IO for CDS services. Runs in context of the SAP Cloud Application Programming Model (CAP) using @sap/cds (CDS Node.js).

Table of Contents

Getting Started

Usage

Server

Client

In browser environment implement the websocket client: index.html

WebSocket Standard

Socket.IO (kind: socket.io)

Documentation

Architecture Overview

WebSocket Overview

The CDS Websocket module supports the following use-cases:

Protocol Annotations

The CDS WebSocket module supports the following protocols definitions options in CDS:

If protocol path is not specified (e.g. via @path), it is determined from service name.

If the specified path is relative (i.e. does not start with a slash /), it is appended to the default protocol path e.g. /ws. If the path is absolute (i.e. starts with a slash /), it is used as is.

Examples:

WebSocket Server

The CDS websocket server is exposed on cds object implementation-independent at cds.ws and implementation-specific at cds.wss for WebSocket Standard or cds.io for Socket.IO. Additional listeners can be registered bypassing CDS definitions and runtime. WebSocket server options can be provided via cds.websocket.options.

Default protocol path is /ws and can be overwritten via cds.env.protocols.websocket.path resp. cds.env.protocols.ws.path;

WebSocket Implementation

The CDS websocket server supports the following two websocket implementations:

The server implementation abstracts from the concrete websocket implementation. The websocket client still needs to be implemented websocket implementation specific.

WebSocket Service

Annotated services with websocket protocol are exposed at endpoint: /ws/<service-path>:

Websocket client connection happens as follows for exposed endpoints:

WebSocket Event

Websocket services can contain events that are exposed as websocket events. Emitting an event on the service, broadcasts the event to all websocket clients.

  @protocol: 'ws'
  @path: 'chat'
  service ChatService {
    event received {
      text: String;
    }
  }

In addition, also non-websocket services can contain events that are exposed as websocket events:

  @protocol: 'odata'
  @path: 'chat'
  service ChatService {
    entity Chat as projection on chat.Chat;
    function message(text: String) returns String;
    @websocket
    event received {
      text: String;
    }
  }

Although the service is exposed as an OData protocol at /odata/v4/chat, the service events annotated with @websocket or @ws are exposed as websocket events under the websocket protocol path as follows: /ws/chat. Entities and operations are not exposed, as the service itself is not marked as websocket protocol.

The service path can be overruled on event level via @websocket.path or @ws.path annotation as follows:

@ws.path: 'fns-websocket'
@ws.format: 'pcp'
event notify {
    text : String
};

The specified event path must match the service path of another websocket enabled CDS service, otherwise the event is not processed. In addition the websocket format can be specified on event level via @websocket.format or @ws.format annotation for websocket events of non-websocket services.

Hint:

Non-websocket service events are only active when at least one websocket enabled service is available (i.e. websocket protocol adapter is active).

Server Socket

Each CDS handler request context is extended to hold the current server socket instance of the event. It can be accessed via the service websocket facade via req.context.ws.service or cds.context.ws.service. In addition the native websocket server socket can be accessed via req.context.ws.socket or cds.context.ws.socket. Events can be directly emitted via the native socket, bypassing CDS runtime, if necessary.

Service Facade

The service facade provides native access to websocket implementation independent of CDS context and is accessible on socket via socket.facade or in CDS context via req.context.ws.service. It abstracts from the concrete websocket implementation by exposing the following public interface:

Middlewares

For each server websocket connection the standard CDS middlewares are applied. That means, that especially the correct CDS context is set up and the configured authorization strategy is applied.

Tenant Isolation

WebSockets are processed tenant aware. Especially for broadcasting events tenant isolation is ensured, that only websocket clients connected for the same tenant are notified in tenant context. Tenant isolation is also ensured over remote distribution via Redis.

Authentication & Authorization

Authentication only works via AppRouter (e.g. using a UAA configuration), as the auth token is forwarded via authorization header bearer token by AppRouter to backend instance. CDS middlewares process the auth token and set the auth info accordingly. Authorization scopes are checked as defined in the CDS services @requires annotations and authorization restrictions are checked as defined in the CDS services @restrict annotations.

Invocation Context

In context of a WebSocket enabled CDS services, WebSockets events can be directly emitted to the service in the event handler:

srv.on("action", async (req) => {
  await srv.emit("message", req.data);
});

In case, the context of invocation is not a WebSocket service, e.g. call is coming from OData or Rest request, still the WebService events can be published by connecting to the WebSocket enabled service as follows:

const wsService = await cds.connect.to("WSService");
await wsService.emit("message", req.data);

cds.conntect.to can be used to connect to any WebSocket enabled service, to emit events to the WebSocket service.

Transactional Safety

In most situations only websocket events shall be broadcast, in case the primary transaction succeeded. It can be done manually, by emitting CDS event as part of the req.on("succeeded") handler.

req.on("succeeded", async () => {
  await srv.emit("received", req.data);
});

Alternatively you can leverage the CAP in-memory outbox via cds.outboxed as follows:

const chatService = cds.outboxed(await cds.connect.to("ChatService"));
await chatService.emit("received", req.data);

This has the benefit, that the event emitting is coupled to the success of the primary transaction. Still the asynchronous event processing could fail, and would not be retried anymore. That's where the CDS persistent outbox comes into play.

CDS Persistent Outbox

Websocket events can also be sent via the CDS persistent outbox. That means, the CDS events triggering the websocket broadcast are added to the CDS persistent outbox when the primary transaction succeeded. The events are processed asynchronously and transactional safe in a separate transaction. It is ensured, that the event is processed in any case, as outbox keeps the outbox entry open, until the event processing succeeded.

The transactional safety can be achieved using cds.outboxed with kind persistent-outbox as follows:

const chatService = cds.outboxed(await cds.connect.to("ChatService"), {
  kind: "persistent-outbox",
});
await chatService.emit("received", req.data);

In that case, the websocket event is broadcast to websocket clients exactly once, when the primary transaction succeeds. In case of execution errors, the event broadcast is retried automatically, while processing the persistent outbox.

Client Determination

The client determination during WebSocket event broadcasting/emitting, is based on the following filtering options of the event:

Tenant and service are determined automatically based on the CDS context and are applied per default to ensure tenant and service isolation. User, context and client identifier are optional and are determined based on the event data or event emit headers. They can be combined arbitrarily to filter the websocket clients to be notified.

The client filtering options are depicted in the following diagram:

Client Determination Overview

The diagram shows the mandatory filtering layer tenant and service and the optional filtering layers user, context and client identifier. The + and - symbols on the optional filter layers indicating the possibility to include (+) or exclude (-) filtering conditions as described in the upcoming sections.

Event Users

Current User

Events are broadcast to all websocket clients, including clients established in context of current context user. To influence event broadcasting based on current context user, the annotation @websocket.user or @ws.user is available on event level and event element level (alternatives include @websocket.broadcast.user or @ws.broadcast.user):

Valid annotation values are:

Furthermore, also additional equivalent annotations alternatives are available:

Examples:

Event Level:

@websocket.user: 'includeCurrent'
event received {
  name: String;
  text: String;
}

Event is published only to websocket clients established in context to the current context user.

Event Element Level:

event received {
  name: String;
  text: String;
  @websocket.currentUser.exclude
  flag: Boolean
}

Event is published only to websocket clients not established in context to the current context user, if the event data of flag is falsy.

Defined Users

Events are broadcast to all websocket clients. To influence event broadcasting based on defined users, the following annotations to include or exclude defined users are available:

Valid annotation values are:

Examples:

Event Level:

@websocket.user.exclude: 'ABC'
event received {
  name: String;
  text: String;
}

Event is published to all users except the user ABC.

Event Element Level:

event received {
  name: String;
  text: String;
  @websocket.user.include
  users: many String;
}

Event is only published to all users listed in the event data of users.

Event Contexts

It is possible to broadcast events to a subset of clients. By entering or exiting contexts, the server can be instructed to determine to which subset of clients the event shall be emitted, based on the event. To specify which data parts of the event are leveraged for setting up the context, the annotation @websocket.context or @ws.context is available on event element level (alternatives include @websocket.broadcast.context or @ws.broadcast.context). For static contexts the annotation can also be used on event level, providing a static event context string.

To influence event broadcasting based on event contexts, the following annotations to include or exclude contexts are available:

Valid annotation values are:

Examples:

Event Level:

@websocket.context: 'ABC'
event received {
  ID: UUID;
  text: String;
}

Event is only published to all clients in context ABC.

Event Element Level:

event received {
  @websocket.context
  ID: UUID;
  text: String;
}

Event is only published to all clients in context of the event data of ID.

The annotation can be used on multiple event elements setting up different event contexts in parallel, if event shall be broadcast/emitted into multiple contexts at the same time.

event received {
  @websocket.context
  ID: UUID;
  @websocket.context
  name: String;
  text: String;
}

Event contexts can also be established via event elements of many or array of type:

event received {
  @websocket.context
  ID: many UUID;
  text: String;
}

This allows setting up an unspecified number of different event contexts in parallel during runtime.

Event contexts support all CDS/JS types. The serialization is performed as follows:

To manage event contexts the following options exist:

Multiple contexts can be entered for the same server socket at the same time. Furthermore, a service operation named wsContext is invoked, if existing on the websocket enabled CDS service. Event context isolation is also ensured over remote distribution via Redis.

For Socket.IO (kind: socket.io) contexts are implemented leveraging Socket.IO rooms.

Event Client Identifiers

Events are broadcast to all websocket clients, including clients that performed certain action. When events are send as part of websocket context, access to current socket is given, but if actions are performed outside websocket context, there are no means to identify the client that performed the action.

That's where the event client identifier come into play. Client identifier are unique consumer-provided strings, that are provided during the websocket connection to identify the websocket client as well as in other request cases (e.g. OData call). When an OData call with a client identifier is performed, it can be used to restrict the websocket event broadcasting.

In some cases, the websocket clients shall be restricted on an instance basis. There are use-cases, that only certain clients are informed about an event and also in other cases the client shall not be informed about the event, that was triggered by the same client (maybe via a different channel, e.g. OData). Therefore, websocket clients can be identified optionally by a unique identifier provided as URL parameter option ?id=<globally unique value>.

The annotation @websocket.identifier.include or @ws.identifier.include is available on event level and event element level to influence event broadcasting based websocket client identifier to include certain clients based on their identifier (not listed clients are no longer respected when set) (alternatives include @websocket.broadcast.identifier.include or @ws.broadcast.identifier.include):

The annotation @websocket.identifier.exclude or @ws.identifier.exclude is available on event level and event element level to influence event broadcasting based websocket client identifier to exclude certain clients based on their identifier (alternatives include @websocket.broadcast.identifier.exclude or @ws.broadcast.identifier.exclude):

The full list of annotations is:

Valid annotation values are:

Examples:

Event Level:

@websocket.identifier.include: 'ABC'
event received {
  ID: UUID;
  text: String;
}

Event is only published to all clients with identifier ABC.

Event Element Level:

event received {
  ID: UUID;
  @websocket.identifier.include
  ids: many String;
}

Event is only published to all clients with identifiers listed in the event data of ids.

Client Setup

The unique identifier can be provided for a websocket client as follows:

Event Emit Headers

The websocket implementation allows to provide event emit headers to dynamically control websocket processing. The following headers are available:

Emitting events with headers can be performed as follows:

await srv.emit("customEvent", { ... }, {
  contexts: ["..."],
  currentUser: {
    exclude: req.data.type === "1"
  },
  user: {
    include: "...",
    exclude: ["..."],
  },
  identifier: {
    include: ["..."],
    exclude: "...",
  },
});
Event HTTP Headers

In addition to the above event emit headers, HTTP conform headers can be specified starting with x-websocket- or x-ws-
prefix. The lower case header names are converted to camel-cased header names removing prefix, e.g. x-ws-current-user becomes currentUser. Header string values are parsed according to their value to types Boolean, Number or String.

Format specific HTTP conform headers can be defined in formatter named subsection, x-websocket-<format>- or x-ws-<format>-.

Examples (for format cloudevent):

Value Aggregation

The respective event annotations (described in sections above) are respected in addition to event emit header specification. All event annotation values (static or dynamic) and header values are aggregated during event emit according to their kind. Values of all headers and annotations of same semantic type are unified for single and array values.

Format Headers

In addition to the above event emit headers, format specific event headers can be specified in the websocket or ws section during event emit.

await srv.emit("customEvent", { ... }, {
  ws: {
    a: 1,
    cloudevent: {
      e: true
    }
  },
  websocket: {
    b: "c"
  }
});

These headers are made available to the format compose(event, data, headers) function, to be included in the composed WebSocket message, if applicable (e.g. format: pcp, cloudevent). Format specific headers can also be defined in formatter named subsection, e.g. ws.cloudevent.e: true (for format cloudevent), to avoid conflicts.

Ignore Definitions

To ignore elements and parameters during event processing, the annotation @websocket.ignore or @ws.ignore is available on event element and operation parameter level. The annotation can be used to exclude elements and parameters from WebSocket event.

WebSocket Format

Per default the CDS websocket format is json, as CDS internally works with JSON objects.

WS Standard and Socket.IO support JSON format as follows:

SAP Push Channel Protocol (PCP)

CDS WebSocket module supports the SAP Push Channel Protocol (PCP) out-of-the-box.

A PCP message has the following structure:

pcp-action:MESSAGE
pcp-body-type:text
field1:value1
field2:value2

this is the body!

To configure the PCP format, the service needs to be annotated in addition with @websocket.format: 'pcp' or @ws.format: 'pcp':

@ws
@ws.format: 'pcp'
service PCPService {
  // ...
}

With this configuration WebSocket events consume or produce PCP formatted messages. To configure the PCP message format the following annotations are available:

Cloud Events

CDS WebSocket module supports the Cloud Events specification out-of-the-box according to WebSockets Protocol Binding for CloudEvents.

A Cloud Event message has the following structure:

{
  "specversion": "1.0",
  "type": "com.example.someevent",
  "source": "/mycontext",
  "subject": null,
  "id": "C234-1234-1234",
  "time": "2018-04-05T17:31:00Z",
  "comexampleextension1": "value",
  "comexampleothervalue": 5,
  "datacontenttype": "application/json",
  "data": {
    "appinfoA": "abc",
    "appinfoB": 123,
    "appinfoC": true
  }
}

To configure the CloudEvents format, the service needs to be annotated in addition with @websocket.format: 'cloudevent' or @ws.format: 'cloudevent'.

@ws
@ws.format: 'cloudevent'
service CloudEventService {
  // ...
}

To create a Cloud Event compatible CDS event, either the event is modeled as CDS service event according to the specification or a CDS event is mapped via annotations to a Cloud Event compatible event.

Modeling Cloud Event

Cloud event can be explicitly modelled as CDS event, matching the specification of cloud event attributes.

Example:

event cloudEvent {
    specversion : String;
    type : String;
    source : String;
    subject : String;
    id : String;
    time : String;
    comexampleextension1 : String;
    comexampleothervalue : String;
    datacontenttype : String;
    data: {
        appinfoA : String;
        appinfoB : Integer;
        appinfoC : Boolean;
    }
}

The CDS event cloudEvent is explicitly modeled according to the Cloud Event specification. The event data is passed inbound and outbound in the exact same representation as JSON object, as specified. No additional annotations are necessary to be defined.

Mapping Cloud Event

CDS events can also be mapped to Cloud Event compatible events via headers and CDS annotations. The implementation is based on the generic formatter (see section below), that allows to map CDS events to Cloud Event compatible events based on cloud event specific headers and wildcard annotations, starting with @websocket.cloudevent.<annotation> or @ws.cloudevent.<annotation> to match the Cloud Event specific attributes.

The provided header values in the websocket or ws section are mapped to the cloud event attributes generically, if available.

Example:

await srv.emit(
  "cloudEvent",
  {
    appinfoA,
    appinfoB,
    appinfoC,
  },
  {
    ws: {
      specversion: "1.0",
      type: "com.example.someevent.cloudEvent4",
      source: "/mycontext",
      subject: req.data._subject || "example",
      id: "C234-1234-1234",
      time: "2018-04-05T17:31:00Z",
      comexampleextension1: "value",
      comexampleothervalue: 5,
      datacontenttype: "application/json",
    },
  },
);

Subsequently, the following annotations are respected:

Examples:

Event Level:

@ws.cloudevent.specversion         : '1.0'
@ws.cloudevent.type                : 'com.example.someevent'
@ws.cloudevent.source              : '/mycontext'
@ws.cloudevent.subject             : 'example'
@ws.cloudevent.id                  : 'C234-1234-1234'
@ws.cloudevent.time                : '2018-04-05T17:31:00Z'
@ws.cloudevent.comexampleextension1: 'value'
@ws.cloudevent.comexampleothervalue: 5
@ws.cloudevent.datacontenttype     : 'application/json'
event cloudEvent2 {
    appinfoA : String;
    appinfoB : Integer;
    appinfoC : Boolean;
}

Event is published via cloud event sub-protocol, with the specified static cloud event attributes. The CDS event data is consumed as cloud event data section.

Event Element Level:

event cloudEvent3 {
    @ws.cloudevent.specversion
    specversion     : String
    @ws.cloudevent.type
    type            : String
    @ws.cloudevent.source
    source          : String
    @ws.cloudevent.subject
    subject         : String
    @ws.cloudevent.id
    id              : String
    @ws.cloudevent.time
    time            : String
    @ws.cloudevent.comexampleextension1
    extension1      : String
    @ws.cloudevent.comexampleothervalue
    othervalue      : String
    @ws.cloudevent.datacontenttype
    datacontenttype : String;
    appinfoA        : String;
    appinfoB        : Integer;
    appinfoC        : Boolean;
}

Event is published via cloud event sub-protocol, with the specified dynamic cloud event attributes derived from CDS event elements. Annotated elements are consumed as cloud event attributes, non-annotated elements are consumed as cloud event data section.

Static and dynamic annotations can be combined. Static values have precedence over dynamic values, if defined.

Cloud Event Operation

CDS service operations (actions or functions) can also be exposed via cloud event. The operation name is derived from the @websocket.cloudevent.name or @ws.cloudevent.name annotation. Emitting a cloud event based websocket event that matches the annotation value of name, calls the respective service operation handler.

The operation parameter structure can be either modelled according to the Cloud Event specification using the attributes as parameter names or mapped via annotations like @websocket.cloudevent.<annotation> or @ws.cloudevent.<annotation> to a Cloud Event compatible structure.

The following annotations are respected:

Examples:

Model Operation Parameters:

type CloudEventDataType : {
    appinfoA : String;
    appinfoB : Integer;
    appinfoC : Boolean;
};

@ws.cloudevent.name: 'com.example.someevent'
action sendCloudEventModel( subject : String, comexampleextension1 : String, comexampleothervalue : Integer, data: CloudEventDataType) returns Boolean;

Map Operation Parameters:

@ws.cloudevent.name: 'com.example.someevent'
@ws.cloudevent.subject: 'cloud'
action sendCloudEventMap(
  @ws.cloudevent.subject subject : String,
  @ws.cloudevent.comexampleextension1 extension1 : String,
  @ws.cloudevent.comexampleothervalue othervalue : Integer,
  appinfoA : String,
  appinfoB : Integer,
  appinfoC : Boolean
  @ws.ignore appinfoD : String
) returns Boolean;

Unmapped operation parameters are consumed as cloud event data section and can be skipped for cloud event data section via @ws.ignore, if not necessary.

Cloud Event Format Alternative

Alternatives for format cloudevent also allows to use the plural name @websocket.format: 'cloudevents' or @ws.format: 'cloudevents', if preferred. All headers and annotations are also named in plural form accordingly, e.g. @ws.cloudevents.name, etc.

Custom Format

A custom websocket format implementation can be provided via a path relative to the project root in @websocket.format resp. @ws.format annotation (e.g. @ws.format: './format/xyz.js').

The custom format class needs to implement the following functions:

In addition, it can implement the following functions (optional):

Generic Format

Additionally, a custom formatter can be based on the generic implementation src/format/generic.js providing a name and identifier. Values are derived via CDS annotations based on wildcard annotations @websocket.<format>.<annotation> or @ws.<format>.<annotation> using the formatter name.

In addition, provided header values in the websocket or ws section are also used to derived values from. Format specific headers can also be defined in formatter named subsection, e.g. ws.cloudevent.e: true (for format cloudevent), to avoid conflicts.

The following generic implementation specifics are included:

The generic formatter can also directly be used via annotations @websocket.format: 'generic' or @ws.format: 'generic'. Values are derived from data via CDS annotations based on wildcard annotations @websocket.generic.<annotation> or @ws.generic.<annotation> and headers from subsections websocket.generic.<header> or ws.generic.<header>.

Connect & Disconnect

Every time a server socket is connected via websocket client, the CDS service is notified by calling the corresponding service operation:

Approuter

Authorization in provided in production by Approuter component (e.g. via XSUAA auth). Valid UAA bindings for Approuter and backend are necessary, so that the authorization flow is working. Locally, the following default environment files need to exist:

Approuter is configured to support websockets in xs-app.json according to @sap/approuter - websockets property:

{
  "websockets": {
    "enabled": true
  }
}

For local testing without approuter a mocked basic authorization is hardcoded in flp.html/index.html.

Operations

Operations comprise actions and function in the CDS service that are exposed by CDS service either unbound (static level) or bound (entity instance level). Operations are exposed as part of the websocket protocol as described below.

Operation Results

Operation results will be provided via optional websocket acknowledgement callback.

Operation results are only supported with Socket.IO (kind: socket.io) using acknowledgement callbacks.

Unbound Operations

Each unbound function and action is exposed as websocket event. The signature (parameters and return type) is passed through without additional modification. Operation result will be provided as part of acknowledgment callback.

Special operations

The websocket adapter tries to call the following special operations on the CDS service, if available:

Bound Operations

Each service entity is exposed as CRUD interface via as special events as proposed here. The event is prefixed with the entity name and has the CRUD operation as suffix, e.g. Books:create. In addition, also bound functions and actions are included into these schema, e.g. Books:sell. The signature (parameters and return type) is passed through without additional modification. It is expected, that the event payload contains the primary key information. CRUD/action/function result will be provided as part of acknowledgment callback.

CRUD Operations

Create, Read, Update and Delete (CRUD) actions are mapped to websocket events as follows:

Events can be emitted and the response can be retrieved via acknowledgment callback (kind: socket.io only).

CRUD Broadcast Events

CRUD events that modify entities automatically emit another event after successful processing:

Because of security concerns, it can be controlled which data of those events is broadcast, via annotations @websocket.broadcast or @ws.broadcast on entity level.

If the CRUD broadcast event is modeled as part of CDS service the annotations above are ignored for that event, and the broadcast data is filtered along the event elements. As character : is not allowed in CDS service event names, character : is replaced by a scoped event name using character ..

Example: WebSocket Event: Object:created is mapped to CDS Service Event: Object.created

Per default, events are broadcast to every connected socket, except the socket, that was called with the CRUD event. To also include the triggering socket within the broadcast, this can be controlled via annotations @websocket.broadcast.all or @ws.broadcast.all on entity level.

Examples

Todo (UI5)

The example UI5 todo application using Socket.IO can be found at test/_env/app/todo.

Example application can be started by:

Chat (HTML)

An example chat application using Socket.IO can be found at test/_env/app/chat.

Example application can be started by:

Unit-Tests

Unit-test can be found in folder test and can be executed via npm test; The basic unit-test setup for WebSockets in CDS context looks as follows:

WS

"use strict";

const cds = require("@sap/cds");
const WebSocket = require("ws");

cds.test(__dirname + "/..");

const authorization = `Basic ${Buffer.from("alice:alice").toString("base64")}`;

describe("WebSocket", () => {
  let socket;

  beforeAll((done) => {
    const port = cds.app.server.address().port;
    socket = new WebSocket(`ws://localhost:${port}/ws/chat`, {
      headers: {
        authorization,
      },
    });
  });

  afterAll(() => {
    socket.close();
    cds.ws.close();
  });

  test("Test", (done) => {
    socket.send(
      JSON.stringify({
        event: "event",
        data: {},
      }),
    );
    done();
  });
});

Socket.io

"use strict";

const cds = require("@sap/cds");
const ioc = require("socket.io-client");

cds.test(__dirname + "/..");

cds.env.websocket.kind = "socket.io";

const authorization = `Basic ${Buffer.from("alice:alice").toString("base64")}`;

describe("WebSocket", () => {
  let socket;

  beforeAll((done) => {
    const port = cds.app.server.address().port;
    socket = ioc(`http://localhost:${port}/ws/chat`, {
      extraHeaders: {
        authorization,
      },
    });
    socket.on("connect", done);
  });

  afterAll(() => {
    socket.disconnect();
    cds.ws.close();
  });

  test("Test", (done) => {
    socket.emit("event", {}, (result) => {
      expect(result).toBeDefined();
      done();
    });
  });
});

Adapters

An Adapter is a server-side component which is responsible for broadcasting events to all or a subset of clients.

WS Standard Adapters

The following adapters for WS Standard are supported out-of-the-box.

Redis

Every event that is sent to multiple clients is sent to all matching clients connected to the current server and published in a Redis channel, and received by the other websocket servers of the cluster. The app needs to be bound to a Redis service instance to set up and connect Redis client.

To use the Redis Adapter (basic publish/subscribe), the following steps have to be performed:

Custom Adapter

A custom websocket adapter implementation can be provided via a path relative to the project root with the configuration cds.websocket.adapter.impl (e.g. cds.websocket.adapter.impl: './adapter/xyz.js').

The custom adapter class needs to implement the following functions:

In addition, it can implement the following functions (optional):

Socket.IO Adapters

The following adapters for Socket.IO are supported out-of-the-box.

Redis Adapter

To use the Redis Adapter, the following steps have to be performed:

Details: https://socket.io/docs/v4/index-adapter/

Redis Streams Adapter

To use the Redis Stream Adapter, the following steps have to be performed:

Details: https://socket.io/docs/v4/index-streams-adapter/

Custom Adapter

A custom websocket adapter implementation can be provided via a path relative to the project root with the configuration cds.websocket.adapter.impl (e.g. cds.websocket.adapter.impl: './adapter/xyz.js').

The custom adapter need to fulfill the Socket.IO adapter interface (https://socket.io/docs/v4/adapter/).

Deployment

This module also works on a deployed infrastructure like Cloud Foundry (CF) or Kubernetes (K8s).

An example Cloud Foundry deployment can be found in test/_env:

In deployed infrastructure, websocket protocol is exposed via Web Socket Secure (WSS) at wss:// over an encrypted TLS connection. For WebSocket standard the following setup in browser environment is recommended to cover deployed and local use-case:

const protocol = window.location.protocol === "https:" ? "wss://" : "ws://";
const socket = new WebSocket(protocol + window.location.host + "/ws/chat");

Support, Feedback, Contributing

This project is open to feature requests/suggestions, bug reports etc. via GitHub issues. Contribution and feedback are encouraged and always welcome. For more information about how to contribute, the project structure, as well as additional contribution information, see our Contribution Guidelines.

Code of Conduct

We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone. By participating in this project, you agree to abide by its Code of Conduct at all times.

Licensing

Copyright 2024 SAP SE or an SAP affiliate company and websocket contributors. Please see our LICENSE for copyright and license information. Detailed information including third-party components and their licensing/copyright information is available via the REUSE tool.