Skip to content

Commit b648463

Browse files
committed
- Websocket events in non-websocket services
- Support multiple endpoints
1 parent 82542aa commit b648463

File tree

8 files changed

+220
-35
lines changed

8 files changed

+220
-35
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
66
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
77

8+
## Version 0.1.1 - 22.12.23
9+
10+
### Added
11+
12+
- Websocket events in non-websocket services
13+
- Support multiple endpoints
14+
815
## Version 0.1.0 - 21.12.23
916

1017
### Added

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,25 @@ Valid UAA bindings for approuter and backend are necessary, so that the authoriz
9191
Locally, the following default environment files need to exist:
9292

9393
- `test/\_env/default-env.json`
94+
```
95+
{
96+
"VCAP_SERVICES": {
97+
"xsuaa": [
98+
{
99+
...
100+
}
101+
]
102+
}
103+
}
104+
```
94105
- `test/\_env/approuter/default-services.json`
106+
```
107+
{
108+
"uaa": {
109+
...
110+
}
111+
}
112+
```
95113

96114
Approuter is configured to support websockets in `xs-app.json` according to [@sap/approuter - websockets property](https://www.npmjs.com/package/@sap/approuter#websockets-property):
97115

package-lock.json

Lines changed: 19 additions & 19 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/index.js

Lines changed: 74 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,56 @@ function serveWebSocketServer(options) {
2727
cds.on("listening", (app) => {
2828
socketServer = initWebSocketServer(app.server, options.path);
2929
if (socketServer) {
30+
// Websocket services
3031
for (const serviceName in options.services) {
3132
const service = options.services[serviceName];
3233
if (isServedViaWebsocket(service)) {
3334
serveWebSocketService(socketServer, service, options);
3435
}
3536
}
37+
// Websockets events
38+
const eventServices = {};
39+
for (const name in cds.model.definitions) {
40+
const definition = cds.model.definitions[name];
41+
if (definition.kind === "event" && (definition["@websocket"] || definition["@ws"])) {
42+
const service = cds.services[definition._service?.name];
43+
if (service && !isServedViaWebsocket(service)) {
44+
eventServices[service.name] ??= eventServices[service.name] || {
45+
name: service.name,
46+
definition: service.definition,
47+
endpoints: service.endpoints.map((endpoint) => {
48+
const protocol =
49+
cds.env.protocols[endpoint.kind] ||
50+
(endpoint.kind === "odata" ? cds.env.protocols["odata-v4"] : null);
51+
return {
52+
kind: "websocket",
53+
path: cds.env.protocols.websocket.path + normalizeServicePath(service.path, protocol.path),
54+
};
55+
}),
56+
operations: () => {
57+
return [];
58+
},
59+
entities: () => {
60+
return [];
61+
},
62+
events: [],
63+
on: service.on.bind(service),
64+
tx: service.tx.bind(service),
65+
};
66+
eventServices[service.name].events.push(definition);
67+
}
68+
}
69+
}
70+
for (const name in eventServices) {
71+
const eventService = eventServices[name];
72+
const events = eventService.events;
73+
if (events.length > 0) {
74+
eventService.events = () => {
75+
return events;
76+
};
77+
serveWebSocketService(socketServer, eventService, options);
78+
}
79+
}
3680
}
3781
});
3882
}
@@ -51,27 +95,35 @@ function initWebSocketServer(server, path) {
5195
}
5296
}
5397

54-
function serveWebSocketService(socketServer, service, options) {
55-
let servicePath = service.path;
56-
if (servicePath.startsWith(options.path)) {
57-
servicePath = servicePath.substring(options.path.length);
98+
function normalizeServicePath(servicePath, protocolPath) {
99+
if (servicePath.startsWith(protocolPath)) {
100+
return servicePath.substring(protocolPath.length);
58101
}
59-
try {
60-
socketServer.service(servicePath, (socket) => {
102+
return servicePath;
103+
}
104+
105+
function serveWebSocketService(socketServer, service, options) {
106+
for (const endpoint of service.endpoints || []) {
107+
if (["websocket", "ws"].includes(endpoint.kind)) {
108+
const servicePath = normalizeServicePath(endpoint.path, options.path);
61109
try {
62-
socket.setup();
63-
emitConnect(socket, service);
64-
bindServiceDefaults(socket, service);
65-
bindServiceOperations(socket, service);
66-
bindServiceEntities(socket, service);
67-
bindServiceEvents(socket, service);
110+
socketServer.service(servicePath, (socket) => {
111+
try {
112+
socket.setup();
113+
emitConnect(socket, service);
114+
bindServiceDefaults(socket, service);
115+
bindServiceOperations(socket, service);
116+
bindServiceEntities(socket, service);
117+
bindServiceEvents(socket, service);
118+
} catch (err) {
119+
LOG?.error(err);
120+
socket.disconnect();
121+
}
122+
});
68123
} catch (err) {
69124
LOG?.error(err);
70-
socket.disconnect();
71125
}
72-
});
73-
} catch (err) {
74-
LOG?.error(err);
126+
}
75127
}
76128
}
77129

@@ -247,7 +299,13 @@ function serviceLocalName(service, name) {
247299
}
248300

249301
function isServedViaWebsocket(service) {
302+
if (!service) {
303+
return false;
304+
}
250305
const serviceDefinition = service.definition;
306+
if (!serviceDefinition) {
307+
return false;
308+
}
251309
let protocols = serviceDefinition["@protocol"];
252310
if (protocols) {
253311
protocols = !Array.isArray(protocols) ? [protocols] : protocols;

test/_env/srv/handlers/odata.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"use strict";
2+
3+
module.exports = (srv) => {
4+
const { Header } = srv.entities();
5+
6+
srv.before("CREATE", Header, async (req) => {
7+
srv.emit("received", req.data);
8+
srv.emit("receivedToo", req.data);
9+
});
10+
};

test/_env/srv/odata.cds

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,15 @@ service ODataService {
66
entity Header as projection on test.Header;
77

88
function message(text: String) returns String;
9+
10+
@ws
11+
event received {
12+
text: String;
13+
}
14+
15+
@websocket
16+
event receivedToo {
17+
text: String;
18+
}
919
}
1020

test/odata_socketio.test.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
"use strict";
2+
3+
const cds = require("@sap/cds");
4+
5+
const { connect, disconnect, emitEvent, waitForEvent } = require("./_env/util/socketio");
6+
const { authorization } = require("./_env/util/common");
7+
8+
cds.test(__dirname + "/_env");
9+
10+
cds.env.requires.websocket = {
11+
kind: "socket.io",
12+
};
13+
14+
describe("OData", () => {
15+
let socket;
16+
let service;
17+
18+
beforeAll(async () => {
19+
socket = await connect("odata");
20+
service = await cds.connect.to("TodoService");
21+
});
22+
23+
afterAll(() => {
24+
disconnect(socket);
25+
});
26+
27+
test("Event", async () => {
28+
const waitReceivedPromise = waitForEvent(socket, "received");
29+
const waitReceivedTooPromise = waitForEvent(socket, "receivedToo");
30+
const response = await fetch(cds.server.url + "/odata/v4/odata/Header", {
31+
method: "POST",
32+
headers: { "content-type": "application/json", authorization },
33+
body: JSON.stringify({ name: "Test" }),
34+
});
35+
const result = await response.json();
36+
expect(result.ID).toBeDefined();
37+
const ID = result.ID;
38+
const waitResult = await waitReceivedPromise;
39+
expect(waitResult).toMatchObject({});
40+
const waitResultToo = await waitReceivedTooPromise;
41+
expect(waitResultToo).toMatchObject({});
42+
});
43+
});

test/odata_ws.test.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"use strict";
2+
3+
const cds = require("@sap/cds");
4+
5+
const { connect, disconnect, emitEvent, waitForEvent } = require("./_env/util/ws");
6+
const { authorization } = require("./_env/util/common");
7+
8+
cds.test(__dirname + "/_env");
9+
10+
describe("OData", () => {
11+
let socket;
12+
let service;
13+
14+
beforeAll(async () => {
15+
socket = await connect("/ws/odata");
16+
service = await cds.connect.to("TodoService");
17+
});
18+
19+
afterAll(() => {
20+
disconnect(socket);
21+
});
22+
23+
test("Event", async () => {
24+
const waitReceivedPromise = waitForEvent(socket, "received");
25+
const waitReceivedTooPromise = waitForEvent(socket, "receivedToo");
26+
const response = await fetch(cds.server.url + "/odata/v4/odata/Header", {
27+
method: "POST",
28+
headers: { "content-type": "application/json", authorization },
29+
body: JSON.stringify({ name: "Test" }),
30+
});
31+
const result = await response.json();
32+
expect(result.ID).toBeDefined();
33+
const ID = result.ID;
34+
const waitResult = await waitReceivedPromise;
35+
expect(waitResult).toMatchObject({});
36+
const waitResultToo = await waitReceivedTooPromise;
37+
expect(waitResultToo).toMatchObject({});
38+
});
39+
});

0 commit comments

Comments
 (0)