Skip to content

Commit 40e758c

Browse files
authored
Better compatibility with browser WebSockets for the JsonRpcSocket client. (#116)
- compatibility for `JsonRpcSocket` with the native browser `WebSocket` client. - enhanced test coverage - included `webSocketSupport` in the `/info` boolean to signal socket support.
1 parent e1396cb commit 40e758c

10 files changed

+104
-25
lines changed

package-lock.json

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@web5/dwn-server",
33
"type": "module",
4-
"version": "0.1.11",
4+
"version": "0.1.12",
55
"files": [
66
"dist",
77
"src"

src/config.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export const config = {
88
// port that server listens on
99
port: parseInt(process.env.DS_PORT || '3000'),
1010
// whether to enable 'ws:'
11-
webSocketServerEnabled: { on: true, off: false }[process.env.DS_WEBSOCKET_SERVER] ?? true,
11+
webSocketSupport: { on: true, off: false }[process.env.DS_WEBSOCKET_SERVER] ?? true,
1212
// where to store persistent data
1313
messageStore: process.env.DWN_STORAGE_MESSAGES || process.env.DWN_STORAGE || 'level://data',
1414
dataStore: process.env.DWN_STORAGE_DATA || process.env.DWN_STORAGE || 'level://data',

src/connection/socket-connection.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ export class SocketConnection {
175175
* Sends a JSON encoded Buffer through the Websocket.
176176
*/
177177
private send(response: JsonRpcResponse | JsonRpcErrorResponse): void {
178-
this.socket.send(Buffer.from(JSON.stringify(response)));
178+
this.socket.send(JSON.stringify(response));
179179
}
180180

181181
/**

src/dwn-server.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export class DwnServer {
6262
});
6363

6464
let eventStream: EventStream | undefined;
65-
if (this.config.webSocketServerEnabled) {
65+
if (this.config.webSocketSupport) {
6666
// setting `EventEmitterStream` as default the default `EventStream
6767
// if an alternate implementation is needed, instantiate a `Dwn` with a custom `EventStream` and add it to server options.
6868
eventStream = new EventEmitterStream();
@@ -84,7 +84,7 @@ export class DwnServer {
8484
this.#httpApi.server,
8585
);
8686

87-
if (this.config.webSocketServerEnabled) {
87+
if (this.config.webSocketSupport) {
8888
this.#wsApi = new WsApi(this.#httpApi.server, this.dwn);
8989
this.#wsApi.start();
9090
log.info('WebSocketServer ready...');

src/http-api.ts

+1
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ export class HttpApi {
190190
registrationRequirements : registrationRequirements,
191191
version : packageJson.version,
192192
sdkVersion : packageJson.dependencies['@tbd54566975/dwn-sdk-js'],
193+
webSocketSupport : config.webSocketSupport,
193194
});
194195
});
195196
}

src/json-rpc-socket.ts

+19-14
Original file line numberDiff line numberDiff line change
@@ -29,28 +29,33 @@ export class JsonRpcSocket {
2929
static async connect(url: string, options: JsonRpcSocketOptions = {}): Promise<JsonRpcSocket> {
3030
const { connectTimeout = CONNECT_TIMEOUT, responseTimeout = RESPONSE_TIMEOUT, onclose, onerror } = options;
3131

32-
const socket = new WebSocket(url, { timeout: connectTimeout });
32+
const socket = new WebSocket(url);
3333

34-
socket.onclose = onclose;
35-
socket.onerror = onerror;
36-
37-
if (!socket.onclose) {
34+
if (!onclose) {
3835
socket.onclose = ():void => {
3936
log.info(`JSON RPC Socket close ${url}`);
40-
}
37+
};
38+
} else {
39+
socket.onclose = onclose;
4140
}
4241

43-
if (!socket.onerror) {
42+
if (!onerror) {
4443
socket.onerror = (error?: any):void => {
4544
log.error(`JSON RPC Socket error ${url}`, error);
46-
}
45+
};
46+
} else {
47+
socket.onerror = onerror;
4748
}
4849

4950
return new Promise<JsonRpcSocket>((resolve, reject) => {
50-
socket.on('open', () => {
51+
socket.addEventListener('open', () => {
5152
resolve(new JsonRpcSocket(socket, responseTimeout));
5253
});
5354

55+
socket.addEventListener('error', (error) => {
56+
reject(error);
57+
});
58+
5459
setTimeout(() => reject, connectTimeout);
5560
});
5661
}
@@ -67,7 +72,7 @@ export class JsonRpcSocket {
6772
request.id ??= uuidv4();
6873

6974
const handleResponse = (event: { data: any }):void => {
70-
const jsonRpsResponse = JSON.parse(event.data.toString()) as JsonRpcResponse;
75+
const jsonRpsResponse = JSON.parse(event.data) as JsonRpcResponse;
7176
if (jsonRpsResponse.id === request.id) {
7277
// if the incoming response id matches the request id, we will remove the listener and resolve the response
7378
this.socket.removeEventListener('message', handleResponse);
@@ -120,19 +125,19 @@ export class JsonRpcSocket {
120125
const response = await this.request(request);
121126
if (response.error) {
122127
this.socket.removeEventListener('message', socketEventListener);
123-
return { response }
128+
return { response };
124129
}
125130

126131
// clean up listener and create a `rpc.subscribe.close` message to use when closing this JSON RPC subscription
127132
const close = async (): Promise<void> => {
128133
this.socket.removeEventListener('message', socketEventListener);
129134
await this.closeSubscription(subscriptionId);
130-
}
135+
};
131136

132137
return {
133138
response,
134139
close
135-
}
140+
};
136141
}
137142

138143
private closeSubscription(id: JsonRpcId): Promise<JsonRpcResponse> {
@@ -145,6 +150,6 @@ export class JsonRpcSocket {
145150
* Sends a JSON-RPC request through the socket. You must subscribe to a message listener separately to capture the response.
146151
*/
147152
send(request: JsonRpcRequest):void {
148-
this.socket.send(Buffer.from(JSON.stringify(request)));
153+
this.socket.send(JSON.stringify(request));
149154
}
150155
}

tests/dwn-server.spec.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,14 @@ describe('DwnServer', function () {
2020
expect(dwnServer.httpServer.listening).to.be.false;
2121
});
2222

23-
describe('webSocketServerEnabled config', function() {
23+
describe('webSocketSupport config', function() {
2424
it('should not return a websocket server if disabled', async function() {
2525
dwn = await getTestDwn({ withEvents: true });
2626
const withoutSocketServer = new DwnServer({
2727
dwn,
2828
config: {
2929
...dwnServerConfig,
30-
webSocketServerEnabled: false,
30+
webSocketSupport: false,
3131
}
3232
});
3333

@@ -44,7 +44,7 @@ describe('DwnServer', function () {
4444
dwn,
4545
config: {
4646
...dwnServerConfig,
47-
webSocketServerEnabled: true,
47+
webSocketSupport: true,
4848
}
4949
});
5050

tests/http-api.spec.ts

+25
Original file line numberDiff line numberDiff line change
@@ -537,5 +537,30 @@ describe('http api', function () {
537537
'proof-of-work-sha256-v0',
538538
);
539539
});
540+
541+
it('verify /info signals websocket support', async function() {
542+
let resp = await fetch(`http://localhost:3000/info`);
543+
expect(resp.status).to.equal(200);
544+
545+
let info = await resp.json();
546+
expect(info['server']).to.equal('@web5/dwn-server');
547+
expect(info['webSocketSupport']).to.equal(true);
548+
549+
550+
// start server without websocket support enabled
551+
server.close();
552+
server.closeAllConnections();
553+
554+
config.webSocketSupport = false;
555+
httpApi = new HttpApi(config, dwn, registrationManager);
556+
server = await httpApi.start(3000);
557+
558+
resp = await fetch(`http://localhost:3000/info`);
559+
expect(resp.status).to.equal(200);
560+
561+
info = await resp.json();
562+
expect(info['server']).to.equal('@web5/dwn-server');
563+
expect(info['webSocketSupport']).to.equal(false);
564+
});
540565
});
541566
});

tests/json-rpc-socket.spec.ts

+49-1
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,33 @@ describe('JsonRpcSocket', () => {
6767
await expect(requestPromise).to.eventually.be.rejectedWith('timed out');
6868
});
6969

70+
it('removes listener if subscription json rpc is rejected ', async () => {
71+
wsServer.addListener('connection', (socket) => {
72+
socket.on('message', (dataBuffer: Buffer) => {
73+
const request = JSON.parse(dataBuffer.toString()) as JsonRpcRequest;
74+
// initial response
75+
const response = createJsonRpcErrorResponse(request.id, JsonRpcErrorCodes.BadRequest, 'bad request');
76+
socket.send(Buffer.from(JSON.stringify(response)));
77+
});
78+
});
79+
80+
const client = await JsonRpcSocket.connect('ws://127.0.0.1:9003', { responseTimeout: 5 });
81+
const requestId = uuidv4();
82+
const subscribeId = uuidv4();
83+
const request = createJsonRpcSubscriptionRequest(
84+
requestId,
85+
'rpc.subscribe.test.method',
86+
{ param1: 'test-param1', param2: 'test-param2' },
87+
subscribeId,
88+
);
89+
90+
const responseListener = (_response: JsonRpcSuccessResponse): void => {}
91+
92+
const subscription = await client.subscribe(request, responseListener);
93+
expect(subscription.response.error).to.not.be.undefined;
94+
expect(client['socket'].listenerCount('message')).to.equal(0);
95+
});
96+
7097
it('opens a subscription', async () => {
7198
wsServer.addListener('connection', (socket) => {
7299
socket.on('message', (dataBuffer: Buffer) => {
@@ -230,5 +257,26 @@ describe('JsonRpcSocket', () => {
230257
expect(logMessage).to.equal('JSON RPC Socket close ws://127.0.0.1:9003');
231258
});
232259

233-
xit('calls onerror handler', async () => {});
260+
it('calls onerror handler', async () => {
261+
// test injected handler
262+
const onErrorHandler = { onerror: ():void => {} };
263+
const onErrorSpy = sinon.spy(onErrorHandler, 'onerror');
264+
const client = await JsonRpcSocket.connect('ws://127.0.0.1:9003', { onerror: onErrorHandler.onerror });
265+
client['socket'].emit('error', 'some error');
266+
267+
await new Promise((resolve) => setTimeout(resolve, 5)); // wait for close event to arrive
268+
expect(onErrorSpy.callCount).to.equal(1, 'error');
269+
270+
// test default logger
271+
const logInfoSpy = sinon.spy(log, 'error');
272+
const defaultClient = await JsonRpcSocket.connect('ws://127.0.0.1:9003');
273+
defaultClient['socket'].emit('error', 'some error');
274+
275+
await new Promise((resolve) => setTimeout(resolve, 5)); // wait for close event to arrive
276+
expect(logInfoSpy.callCount).to.equal(1, 'log');
277+
278+
// extract log message from argument
279+
const logMessage:string = logInfoSpy.args[0][0]!;
280+
expect(logMessage).to.equal('JSON RPC Socket error ws://127.0.0.1:9003');
281+
});
234282
});

0 commit comments

Comments
 (0)