Skip to content

Commit 66e7ac0

Browse files
authored
Merge pull request #24 from bugwheels94/alpha
Alpha
2 parents 310cc1e + b6bb099 commit 66e7ac0

9 files changed

+404
-94
lines changed

Readme.MD

+93-22
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,104 @@
1-
## Restify Websocket
1+
<h1 align="center">Restify Websocket ( Beta )</h1>
2+
<p align="center">Easiest Websocket library for big projects</p>
23

3-
This provides a very lightweight HTTP similar API for using Websockets in Node/Browser. WebSocket code often gets confusing because of generic `.on` and `.emit` events.
4+
![alt text](docs/banner.png)
45

5-
## Example
6+
## Why Another Library?
67

7-
// Sending Message
8-
const { client, router } = new RestifyWebSocket(socket)
9-
client.post('/users', { body: {
10-
userId: 15372
11-
}})
12-
client.put('/users/15372', { body: {
13-
age: 60
14-
}})
8+
1. Is event based system(.emit/.on) too basic to meet your app needs making development slow?
9+
1. Do you want to wait for server response while sending websocket message with ease?
10+
1. Do you want to use react-query with websockets?
11+
1. Do you want to deploy to multi-node cluster with ease using Redis without doing anything extra? (Coming Soon!)
1512

13+
## Introduction
1614

17-
// Receiving Message
18-
const { client, router } = new RestifyWebSocket(socket)
19-
router.post('/users', (req, res) => {
20-
const userId = req.data.userId
21-
// save in DB and get user
22-
res.status(200).send(user)
15+
`restify-websocket` provides a very lightweight HTTP similar API for using Websockets in Node/Browser. WebSocket code often gets confusing because of generic `.on` and `.emit` events. While HTTP model has REST standard for CRUD, Websockets have nothing more than socket.io(rooms, namespaces). This library provides:
16+
17+
1. Connect, reconnect functionality out of the box. You just start sending messages and we take care of rest.
18+
1. If you are familiar with ExpressJS then this library you already know with few exceptions.
19+
1. Send Request from Browser like Fetch API and get promise in return
20+
21+
## Examples
22+
23+
### HTTP Get-like Request
24+
25+
// Browser
26+
export const { client, receiver, socket } = new RestifyWebSocket(SERVER_URL)
27+
// Request, just like HTTP(axios)
28+
const response = await client.get('/users/123', {
29+
body
2330
})
24-
router.put('/users/:userId', (req, res) => {
31+
console.log(response.data)
32+
33+
34+
35+
// Websocket Server, just like express
36+
const { clients, router, server } = new RestifyWebSocket.Server()
37+
router.get('/users/:userId', (req, res) => {
2538
const userId = req.params.userId
26-
// save in DB and get user
39+
const user = {
40+
userId
41+
}
2742
res.status(200).send(user)
2843
})
2944

30-
// In browser it is recommended to pass url to RestifyWebSocket as it also handles reconnect.
45+
### Sending data from Server
46+
47+
Many times this happens that Websocket server sends data to Browser without being requested by Browser. For that case, receivers are available.
48+
49+
// BROWSER
50+
export const { client, receiver, socket } = new RestifyWebSocket(SERVER_URL)
51+
receiver.get('/stocks/:companyName', (request, response) => {
52+
console.log(response.data)
53+
})
54+
55+
// SERVER
56+
const { clients, router, server } = new RestifyWebSocket.Server()
57+
clients.find('*').get('/stocks/goldman-sachs', {
58+
data: [1, 2, 3] // it is body because we are sending for browser
59+
})
60+
61+
### Multiple Tabs Communication
62+
63+
If user is opening from multiple tabs and you want to update each tab, since this is a normal real-time application requirement
64+
65+
const { clients, router, server } = new RestifyWebSocket.Server()
66+
router.patch('/users/:userId', (req, res) => {
67+
const userId = req.params.userId
68+
const user = // change user in DB
69+
res.groupedClients.status(200).send(user) // will send to all sockets belongin to a group
70+
res.send(user) // This will send to current tab only that made the update request
71+
})
72+
73+
For the above request, you will need a receiver since many tabs did not ask for this update
74+
75+
// Other tabs
76+
export const { client, receiver, socket } = new RestifyWebSocket(SERVER_URL)
77+
receiver.patch('/users/:userId', (request, response) => {
78+
console.log(response.data)
79+
})
80+
81+
Meanwhile the tab that made the request in the first place will also get the acknowledgment of update fulfilled
82+
83+
// Tab that is making the user update request
84+
export const { client, receiver, socket } = new RestifyWebSocket(SERVER_URL)
85+
const { data } = await client.patch('/users/123', {
86+
body: {
87+
newName: 'james'
88+
}
89+
}
90+
console.log(data) // Generally you dont want data here, since the receiver will also get the data for you.
91+
92+
### Multiple Tabs Warning like Whatsapp
93+
94+
const { clients, router, server } = new RestifyWebSocket.Server()
95+
router.get('/users/:userId', (req, res) => {
96+
const userId = req.params.userId
97+
const user = // change user in DB
98+
res.othersInGroup.status(400).send("Please Close this Tab as you are open from another tab")
99+
res.send(user) // This will send to current tab with the user info
100+
})
101+
102+
### With react-query
31103

32-
const {router, client, socket} = new RestifyWebSocket("server-url")
33-
// Avoid using socket because in case of redirect you might end up pointing to old socket instance
104+
Coming soon!

docs/banner.png

325 KB
Loading

package.json

+3
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@
3434
},
3535
"author": "bugwheels94",
3636
"license": "ISC",
37+
"peerDependencies": {
38+
"redis": "^4.2.0"
39+
},
3740
"devDependencies": {
3841
"@babel/plugin-proposal-decorators": "^7.17.9",
3942
"@babel/plugin-transform-runtime": "^7.17.0",

rollup.config.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const isProduction = process.env.NODE_ENV === 'production';
1818
const isPackageDependency = (pkg, path, importer = '') => {
1919
return (
2020
path.includes('node_modules/' + pkg) ||
21-
(importer.includes('node_modules/' + pkg) && (console.log('???', path, importer), path.startsWith('.'))) ||
21+
(importer.includes('node_modules/' + pkg) && path.startsWith('.')) ||
2222
path === pkg
2323
);
2424
};

src/client.ts

+84-31
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import WebSocket from 'isomorphic-ws';
22
import HttpStatusCode from './statusCodes';
3-
import { store } from './utils';
43
export type ClientResponse = {
54
_id: number;
65
status: HttpStatusCode;
76
data: any;
87
};
8+
type ServerClientRequest = Partial<Omit<ClientResponse, '_id'>>;
99
export type ClientRequest = {
1010
body?: any;
1111
forget?: boolean;
@@ -28,55 +28,46 @@ export class Client {
2828
id: number = -1;
2929
socket: WebSocket;
3030
promiseStore: ClientPromiseStore = {};
31-
pendinMessageStore: string[] = [];
32-
method(
33-
method: 'get' | 'post' | 'put' | 'patch' | 'delete',
34-
url: string,
35-
options: ClientRequest = {},
36-
socketId?: string | number
37-
) {
31+
pendingMessageStore: string[] = [];
32+
method(method: 'get' | 'post' | 'put' | 'patch' | 'delete', url: string, options: ClientRequest = {}) {
3833
let socket: WebSocket, message: string;
39-
if (!options.forget) {
34+
const { forget, ...remaining } = options;
35+
if (!forget) {
4036
this.id += 1;
41-
message = JSON.stringify({ [method]: url, id: this.id, ...options });
37+
message = JSON.stringify({ ...remaining, [method]: url, id: this.id });
4238
} else {
43-
message = JSON.stringify({ [method]: url, ...options });
44-
}
45-
if (socketId) {
46-
socket = store[socketId];
47-
} else {
48-
socket = this.socket;
39+
message = JSON.stringify({ ...remaining, [method]: url });
4940
}
41+
socket = this.socket;
5042
if (socket.CONNECTING === socket.readyState) {
51-
console.log('pushing into pending', socket.readyState, socket.OPEN);
52-
this.pendinMessageStore.push(message);
43+
this.pendingMessageStore.push(message);
5344
} else {
5445
socket.send(message);
5546
}
5647
return new Promise<ClientResponse>((resolve, reject) => {
5748
this.promiseStore[this.id] = { resolve, reject };
5849
});
5950
}
60-
get(url: string, options?: ClientRequest, socketId?: string | number) {
61-
return this.method('get', url, options, socketId);
51+
get(url: string, options?: ClientRequest) {
52+
return this.method('get', url, options);
6253
}
63-
post(url: string, options?: ClientRequest, socketId?: string | number) {
64-
return this.method('post', url, options, socketId);
54+
post(url: string, options?: ClientRequest) {
55+
return this.method('post', url, options);
6556
}
66-
put(url: string, options?: ClientRequest, socketId?: string | number) {
67-
return this.method('put', url, options, socketId);
57+
put(url: string, options?: ClientRequest) {
58+
return this.method('put', url, options);
6859
}
69-
patch(url: string, options?: ClientRequest, socketId?: string | number) {
70-
return this.method('patch', url, options, socketId);
60+
patch(url: string, options?: ClientRequest) {
61+
return this.method('patch', url, options);
7162
}
72-
delete(url: string, options?: ClientRequest, socketId?: string | number) {
73-
return this.method('delete', url, options, socketId);
63+
delete(url: string, options?: ClientRequest) {
64+
return this.method('delete', url, options);
7465
}
75-
attachSocket(socket: WebSocket) {
66+
onSocketCreated(socket: WebSocket) {
7667
this.socket = socket;
7768
socket.addEventListener('open', () => {
78-
this.pendinMessageStore.map((message) => socket.send(message));
79-
this.pendinMessageStore = [];
69+
this.pendingMessageStore.map((message) => socket.send(message));
70+
this.pendingMessageStore = [];
8071
});
8172
}
8273
async listener(message: ClientResponse) {
@@ -90,3 +81,65 @@ export class Client {
9081
delete this.promiseStore[message._id];
9182
}
9283
}
84+
85+
export class ServerClient {
86+
id: number = -1;
87+
sockets: Set<WebSocket>;
88+
method(method: 'get' | 'post' | 'put' | 'patch' | 'delete', url: string, options: ServerClientRequest = {}) {
89+
const { data, ...remaining } = options;
90+
let sockets = this.sockets,
91+
message = JSON.stringify({ [method]: url, data: data, ...remaining });
92+
sockets = this.sockets;
93+
sockets.forEach((socket) => socket.send(message));
94+
}
95+
constructor(sockets: Set<WebSocket>) {
96+
this.sockets = sockets;
97+
}
98+
get(url: string, options?: ServerClientRequest) {
99+
return this.method('get', url, options);
100+
}
101+
post(url: string, options?: ServerClientRequest) {
102+
return this.method('post', url, options);
103+
}
104+
put(url: string, options?: ServerClientRequest) {
105+
return this.method('put', url, options);
106+
}
107+
patch(url: string, options?: ServerClientRequest) {
108+
return this.method('patch', url, options);
109+
}
110+
delete(url: string, options?: ServerClientRequest) {
111+
return this.method('delete', url, options);
112+
}
113+
}
114+
export class ServerClients {
115+
clients: Map<string | number, ServerClient> = new Map();
116+
listOfAllSockets: Set<WebSocket> = new Set();
117+
118+
add(socket: WebSocket) {
119+
const socketSet = new Set<WebSocket>();
120+
socketSet.add(socket);
121+
if (!('groupId' in socket)) return new ServerClient(socketSet);
122+
const groupId = socket['groupId'];
123+
const existingClient = this.clients.get(groupId);
124+
this.listOfAllSockets.add(socket);
125+
if (existingClient) {
126+
existingClient.sockets.add(socket);
127+
return existingClient;
128+
}
129+
const newClient = new ServerClient(socketSet);
130+
this.clients.set(groupId, newClient);
131+
return newClient;
132+
}
133+
find(id: string | number) {
134+
return this.clients.get(id);
135+
}
136+
remove(socket: WebSocket) {
137+
if (!('groupId' in socket)) return;
138+
const client = this.clients.get(socket['groupId']);
139+
client.sockets.delete(socket);
140+
this.listOfAllSockets.delete(socket);
141+
}
142+
constructor() {
143+
this.clients.set('*', new ServerClient(this.listOfAllSockets));
144+
}
145+
}

0 commit comments

Comments
 (0)