diff --git a/clients/js/lib/rest.js b/clients/js/lib/client.js similarity index 72% rename from clients/js/lib/rest.js rename to clients/js/lib/client.js index 9d092a9c..755c8ecd 100644 --- a/clients/js/lib/rest.js +++ b/clients/js/lib/client.js @@ -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'; @@ -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. * @@ -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}`); } @@ -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); + } + }); } /** @@ -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(), @@ -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, @@ -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 }; diff --git a/clients/js/main.js b/clients/js/main.js index 3db6404c..da220b39 100644 --- a/clients/js/main.js +++ b/clients/js/main.js @@ -1,3 +1,3 @@ -import { RaccoonClient, SerializationType, WireType } from './lib/rest.js'; +import { RaccoonClient, SerializationType, WireType } from './lib/client.js'; export { RaccoonClient, SerializationType, WireType }; diff --git a/clients/js/package-lock.json b/clients/js/package-lock.json index b67b4a43..6f11b48a 100644 --- a/clients/js/package-lock.json +++ b/clients/js/package-lock.json @@ -1,17 +1,18 @@ { "name": "@raystack/raccoon", - "version": "0.1.0-rc2", + "version": "0.2.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@raystack/raccoon", - "version": "0.1.0-rc2", + "version": "0.2.2", "license": "Apache-2.0", "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", @@ -5763,6 +5764,26 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/clients/js/package.json b/clients/js/package.json index 7a378503..6267bd9d 100644 --- a/clients/js/package.json +++ b/clients/js/package.json @@ -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", diff --git a/clients/js/test/rest.test.js b/clients/js/test/rest.test.js index 4e21a64a..ce1972d6 100644 --- a/clients/js/test/rest.test.js +++ b/clients/js/test/rest.test.js @@ -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 = { diff --git a/docs/docs/clients/javascript.md b/docs/docs/clients/javascript.md index a30593a8..e0bc8506 100644 --- a/docs/docs/clients/javascript.md +++ b/docs/docs/clients/javascript.md @@ -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 @@ -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). @@ -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) \ No newline at end of file +You can find examples of client usage [here](https://github.com/raystack/raccoon/tree/main/clients/js/examples)