@cap-js-community/websocket
Advanced tools
Comparing version 0.8.0 to 0.8.1
@@ -8,2 +8,12 @@ # Changelog | ||
## Version 0.8.1 - 2024-03-04 | ||
### Added | ||
- Allows to provide event emit headers to dynamically control websocket processing without annotations | ||
### Fixed | ||
- Describe the usage of CDS persistent outbox for websocket events | ||
## Version 0.8.0 - 2024-02-15 | ||
@@ -10,0 +20,0 @@ |
{ | ||
"name": "@cap-js-community/websocket", | ||
"version": "0.8.0", | ||
"version": "0.8.1", | ||
"description": "WebSocket adapter for CDS", | ||
@@ -46,3 +46,3 @@ "homepage": "https://cap.cloud.sap/", | ||
"cookie": "^0.6.0", | ||
"express": "^4.18.2", | ||
"express": "^4.18.3", | ||
"redis": "^4.6.13", | ||
@@ -54,12 +54,12 @@ "socket.io": "^4.7.4", | ||
"@cap-js-community/websocket": "./", | ||
"@cap-js/sqlite": "^1.5.0", | ||
"@sap/cds": "^7.6.3", | ||
"@sap/cds-dk": "^7.6.1", | ||
"@cap-js/sqlite": "^1.5.1", | ||
"@sap/cds": "^7.7.0", | ||
"@sap/cds-dk": "^7.7.0", | ||
"@sap/xssec": "3.6.1", | ||
"@socket.io/redis-adapter": "^8.2.1", | ||
"@socket.io/redis-streams-adapter": "^0.1.0", | ||
"@socket.io/redis-streams-adapter": "^0.2.0", | ||
"@types/express": "^4.17.21", | ||
"eslint": "^8.54.0", | ||
"eslint": "^8.57.0", | ||
"eslint-config-prettier": "^9.0.0", | ||
"eslint-plugin-jest": "^27.8.0", | ||
"eslint-plugin-jest": "^27.9.0", | ||
"jest": "^29.7.0", | ||
@@ -66,0 +66,0 @@ "passport": "0.7.0", |
@@ -223,2 +223,43 @@ # @cap-js-community/websocket | ||
### 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. | ||
```js | ||
req.on("succeeded", async () => { | ||
await srv.emit("received", req.data); | ||
}); | ||
``` | ||
Alternatively you can leverage the CAP in-memory outbox via `cds.outboxed` as follows: | ||
```js | ||
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: | ||
```js | ||
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. | ||
### Event User | ||
@@ -320,2 +361,22 @@ | ||
### Event Emit Headers | ||
The websocket implementation allows to provide event emit headers to dynamically control websocket processing. | ||
The following headers are available: | ||
- `excludeCurrentUser: boolean`: Exclude current user from event broadcasting (see section Event User) | ||
- `contexts: String[]`: Provide an array of context strings to identify a subset of clients (see section Event Contexts) | ||
Emitting events with headers can be performed as follows: | ||
```js | ||
await srv.emit("customContextHeaderEvent", req.data, { | ||
contexts: ["..."], | ||
excludeCurrentUser: req.data.type === "1", | ||
}); | ||
``` | ||
The respective event annotations (described in sections above) are respected in addition to event emit header specification, | ||
so that primitive typed values have priority when specified as part of headers and array-like data is unified. | ||
### Connect & Disconnect | ||
@@ -322,0 +383,0 @@ |
@@ -139,6 +139,6 @@ "use strict"; | ||
service.on(event, async (req) => { | ||
const localEventName = serviceLocalName(service, event.name); | ||
try { | ||
const user = deriveUser(event, req.data, req); | ||
const contexts = deriveContexts(event, req.data); | ||
const localEventName = serviceLocalName(service, event.name); | ||
const user = deriveUser(event, req.data, req.headers, req); | ||
const contexts = deriveContexts(event, req.data, req.headers); | ||
await socketServer.broadcast({ | ||
@@ -297,4 +297,4 @@ service: servicePath, | ||
if (eventDefinition) { | ||
user = deriveUser(eventDefinition, data, socket); | ||
contexts = deriveContexts(eventDefinition, data); | ||
user = deriveUser(eventDefinition, data, {}, socket); | ||
contexts = deriveContexts(eventDefinition, data, {}); | ||
} | ||
@@ -345,3 +345,9 @@ const contentData = broadcastData(entity, data, eventDefinition); | ||
function deriveUser(event, data, req) { | ||
function deriveUser(event, data, headers, req) { | ||
if (headers?.excludeCurrentUser !== undefined) { | ||
if (headers?.excludeCurrentUser) { | ||
return req.context.user.id; | ||
} | ||
return; | ||
} | ||
let user = | ||
@@ -369,5 +375,5 @@ event["@websocket.user"] || event["@ws.user"] || event["@websocket.broadcast.user"] || event["@ws.broadcast.user"]; | ||
function deriveContexts(event, data) { | ||
const contexts = []; | ||
let isContextEvent = false; | ||
function deriveContexts(event, data, headers) { | ||
let isContextEvent = !!Array.isArray(headers?.contexts); | ||
const contexts = isContextEvent ? headers.contexts : []; | ||
if (event.elements) { | ||
@@ -374,0 +380,0 @@ for (const name in event.elements) { |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
85514
1247
726
Updatedexpress@^4.18.3