Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JS Client: Add websocket support #101

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 63 additions & 4 deletions clients/js/lib/rest.js → clients/js/lib/client.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import axios from 'axios';
import { v4 as uuidv4 } from 'uuid';
import EventEmitter from 'events';
import WebSocket from 'ws';

import createJsonSerializer from './serializer/json_serializer.js';
import createProtobufSerializer from './serializer/proto_serializer.js';
Expand All @@ -12,7 +14,7 @@ import { raystack, google } from '../protos/proton_compiled.js';

const NANOSECONDS_PER_MILLISECOND = 1e6;

class RaccoonClient {
class RaccoonClient extends EventEmitter {
/**
* Creates a new instance of the RaccoonClient.
*
Expand All @@ -26,9 +28,11 @@ class RaccoonClient {
* @param {string} [options.url=''] - The base URL for the API requests.
* @param {string} [options.logger=''] - Logger object for logging.
* @param {number} [options.timeout=1000] - The timeout in milliseconds.
* @param {string} [options.protocol='rest'] - The protocol to use, either 'rest' or 'ws'.
* @returns {RaccoonClient} A new instance of the RaccoonClient.
*/
constructor(options = {}) {
super();
if (!Object.values(SerializationType).includes(options.serializationType)) {
throw new Error(`Invalid serializationType: ${options.serializationType}`);
}
Expand All @@ -50,7 +54,37 @@ class RaccoonClient {
this.logger = options.logger || console;
this.timeout = options.timeout || 1000;
this.uuidGenerator = () => uuidv4();
this.httpClient = axios.create();
this.protocol = options.protocol || 'rest';

if (this.protocol === 'rest') {
this.httpClient = axios.create();
} else if (this.protocol === 'ws') {
this.initializeWebSocket();
} else {
throw new Error(`Invalid protocol: ${this.protocol}`);
}
}

initializeWebSocket() {
this.wsClient = new WebSocket(this.url);
this.wsClient.on('open', () => {
this.logger.info('WebSocket connection established');
});
this.wsClient.on('error', (error) => {
this.logger.error('WebSocket error:', error);
});
this.wsClient.on('message', (data) => {
try {
const response = JSON.parse(data);
const sendEventResponse = this.marshaller.unmarshal(
response,
raystack.raccoon.v1beta1.SendEventResponse
);
this.emit('ack', sendEventResponse.toJSON());
} catch (error) {
this.logger.error('Error processing WebSocket message:', error);
}
});
}

/**
Expand Down Expand Up @@ -96,12 +130,18 @@ class RaccoonClient {
this.logger
);

this.logger.info(`ended request, url: ${this.url}, req-id: ${requestId}`);

if (this.protocol !== 'rest') {
return {
reqId: requestId
};
}

const sendEventResponse = this.marshaller.unmarshal(
response,
raystack.raccoon.v1beta1.SendEventResponse
);

this.logger.info(`ended request, url: ${this.url}, req-id: ${requestId}`);
return {
reqId: requestId,
response: sendEventResponse.toJSON(),
Expand All @@ -114,6 +154,17 @@ class RaccoonClient {
}

async executeRequest(raccoonRequest) {
switch (this.protocol) {
case 'rest':
return this.executeRestRequest(raccoonRequest);
case 'ws':
return this.executeWebSocketRequest(raccoonRequest);
default:
throw new Error(`Unsupported protocol: ${this.protocol}`);
}
}

async executeRestRequest(raccoonRequest) {
this.headers['Content-Type'] = this.marshaller.getContentType();
const response = await this.httpClient.post(this.url, raccoonRequest, {
headers: this.headers,
Expand All @@ -122,6 +173,14 @@ class RaccoonClient {
});
return response.data;
}

async executeWebSocketRequest(raccoonRequest) {
if (this.wsClient.readyState !== WebSocket.OPEN) {
throw new Error('WebSocket is not open');
}

this.wsClient.send(raccoonRequest);
}
}

export { RaccoonClient, SerializationType, WireType };
2 changes: 1 addition & 1 deletion clients/js/main.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { RaccoonClient, SerializationType, WireType } from './lib/rest.js';
import { RaccoonClient, SerializationType, WireType } from './lib/client.js';

export { RaccoonClient, SerializationType, WireType };
27 changes: 24 additions & 3 deletions clients/js/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion clients/js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"dependencies": {
"axios": "^1.4.0",
"protobufjs": "^7.2.4",
"uuid": "^9.0.0"
"uuid": "^9.0.0",
"ws": "^8.18.0"
},
"devDependencies": {
"eslint": "^8.47.0",
Expand Down
2 changes: 1 addition & 1 deletion clients/js/test/rest.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// eslint-disable-next-line
import { jest } from '@jest/globals';
import { RaccoonClient, SerializationType, WireType } from '../lib/rest.js';
import { RaccoonClient, SerializationType, WireType } from '../lib/client.js';
import { raystack, google } from '../protos/proton_compiled.js';

const mockHTTPClient = {
Expand Down
31 changes: 27 additions & 4 deletions docs/docs/clients/javascript.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
Make sure that Nodejs >= `20.0` is installed on your system. See [installation instructions](https://nodejs.org/en/download/package-manager) on Nodejs's website for more info.

## Installation
Install Raccoon's Javascript client using [npm](https://docs.npmjs.com/cli/v10/commands/npm)
```javascript
Install Raccoon's Javascript client using [npm](https://docs.npmjs.com/cli/v10/commands/npm)```javascript
$ npm install --save @raystack/raccoon
```
## Usage
Expand Down Expand Up @@ -64,7 +63,8 @@ To create the client, use `new RaccoonClient(options)`. `options` is javascript
| retryMax | The maximum number of retry attempts for failed requests (default: `3`) |
| retryWait | The time in milliseconds to wait between retry attempts (default: `1000`)|
| timeout | The timeout in milliseconds (default: `1000`)|
| logger | Logger object for logging (default: `global.console`)
| logger | Logger object for logging (default: `global.console`)|
| protocol | The protocol to use, either 'rest' or 'ws' (default: 'rest') |

#### Publishing events
To publish events, create an array of objects and pass it to `RaccoonClient#send()`. The return value is a [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise).
Expand Down Expand Up @@ -93,5 +93,28 @@ The following table lists which serializer to use for a given payload type.

Once a client is constructed with a specific kind of serializer, you may only pass it events of that specific type. In particular, for `JSON` serialiser the event data must be a javascript object. While for `PROTOBUF` serialiser the event data must be a protobuf message.

#### Working with WebSocket

When using the websocket protocol, the response from the server is not returned immediately. Instead, the client is notified of the event's status via an `ack` event. You can subscribe to the `ack` event with `client.on('ack', callback)`. You can select the protocol by setting the `protocol` option in the client constructor. Here's an example:

```js
const client = new RaccoonClient({
protocol: 'ws',
url: 'ws://localhost:8080/api/v1/events',
serializationType: SerializationType.JSON,
wireType: WireType.JSON,
});

client.on('ack', (event) => {
console.log('Response received:', event); // event is of type SendEventResponse
});

client.send(events)
.then(result => console.log('Request ID:', result.reqID))
.catch(err => console.error(err))
```



## Examples
You can find examples of client usage [here](https://github.com/raystack/raccoon/tree/main/clients/js/examples)
You can find examples of client usage [here](https://github.com/raystack/raccoon/tree/main/clients/js/examples)
Loading