Skip to content

Commit 647971e

Browse files
committed
Add pub/sub
1 parent f2dc880 commit 647971e

18 files changed

+118
-47
lines changed

client/build/asset-manifest.json

+7-7
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
11
{
22
"files": {
33
"main.css": "/static/css/main.85225e57.chunk.css",
4-
"main.js": "/static/js/main.02bf0d2e.chunk.js",
5-
"main.js.map": "/static/js/main.02bf0d2e.chunk.js.map",
4+
"main.js": "/static/js/main.90c51a67.chunk.js",
5+
"main.js.map": "/static/js/main.90c51a67.chunk.js.map",
66
"runtime-main.js": "/static/js/runtime-main.120840cb.js",
77
"runtime-main.js.map": "/static/js/runtime-main.120840cb.js.map",
88
"static/css/2.1a02f21c.chunk.css": "/static/css/2.1a02f21c.chunk.css",
9-
"static/js/2.13365d78.chunk.js": "/static/js/2.13365d78.chunk.js",
10-
"static/js/2.13365d78.chunk.js.map": "/static/js/2.13365d78.chunk.js.map",
9+
"static/js/2.0a039c31.chunk.js": "/static/js/2.0a039c31.chunk.js",
10+
"static/js/2.0a039c31.chunk.js.map": "/static/js/2.0a039c31.chunk.js.map",
1111
"index.html": "/index.html",
1212
"static/css/2.1a02f21c.chunk.css.map": "/static/css/2.1a02f21c.chunk.css.map",
1313
"static/css/main.85225e57.chunk.css.map": "/static/css/main.85225e57.chunk.css.map",
14-
"static/js/2.13365d78.chunk.js.LICENSE.txt": "/static/js/2.13365d78.chunk.js.LICENSE.txt"
14+
"static/js/2.0a039c31.chunk.js.LICENSE.txt": "/static/js/2.0a039c31.chunk.js.LICENSE.txt"
1515
},
1616
"entrypoints": [
1717
"static/js/runtime-main.120840cb.js",
1818
"static/css/2.1a02f21c.chunk.css",
19-
"static/js/2.13365d78.chunk.js",
19+
"static/js/2.0a039c31.chunk.js",
2020
"static/css/main.85225e57.chunk.css",
21-
"static/js/main.02bf0d2e.chunk.js"
21+
"static/js/main.90c51a67.chunk.js"
2222
]
2323
}

client/build/index.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Web site created using create-react-app"/><title>Node.JS Redis chat</title><link href="/static/css/2.1a02f21c.chunk.css" rel="stylesheet"><link href="/static/css/main.85225e57.chunk.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><script>!function(e){function t(t){for(var n,l,i=t[0],f=t[1],a=t[2],p=0,s=[];p<i.length;p++)l=i[p],Object.prototype.hasOwnProperty.call(o,l)&&o[l]&&s.push(o[l][0]),o[l]=0;for(n in f)Object.prototype.hasOwnProperty.call(f,n)&&(e[n]=f[n]);for(c&&c(t);s.length;)s.shift()();return u.push.apply(u,a||[]),r()}function r(){for(var e,t=0;t<u.length;t++){for(var r=u[t],n=!0,i=1;i<r.length;i++){var f=r[i];0!==o[f]&&(n=!1)}n&&(u.splice(t--,1),e=l(l.s=r[0]))}return e}var n={},o={1:0},u=[];function l(t){if(n[t])return n[t].exports;var r=n[t]={i:t,l:!1,exports:{}};return e[t].call(r.exports,r,r.exports,l),r.l=!0,r.exports}l.m=e,l.c=n,l.d=function(e,t,r){l.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},l.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},l.t=function(e,t){if(1&t&&(e=l(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(l.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var n in e)l.d(r,n,function(t){return e[t]}.bind(null,n));return r},l.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return l.d(t,"a",t),t},l.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},l.p="/";var i=this.webpackJsonpclient=this.webpackJsonpclient||[],f=i.push.bind(i);i.push=t,i=i.slice();for(var a=0;a<i.length;a++)t(i[a]);var c=f;r()}([])</script><script src="/static/js/2.13365d78.chunk.js"></script><script src="/static/js/main.02bf0d2e.chunk.js"></script></body></html>
1+
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Web site created using create-react-app"/><title>Node.JS Redis chat</title><link href="/static/css/2.1a02f21c.chunk.css" rel="stylesheet"><link href="/static/css/main.85225e57.chunk.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><script>!function(e){function t(t){for(var n,l,i=t[0],f=t[1],a=t[2],p=0,s=[];p<i.length;p++)l=i[p],Object.prototype.hasOwnProperty.call(o,l)&&o[l]&&s.push(o[l][0]),o[l]=0;for(n in f)Object.prototype.hasOwnProperty.call(f,n)&&(e[n]=f[n]);for(c&&c(t);s.length;)s.shift()();return u.push.apply(u,a||[]),r()}function r(){for(var e,t=0;t<u.length;t++){for(var r=u[t],n=!0,i=1;i<r.length;i++){var f=r[i];0!==o[f]&&(n=!1)}n&&(u.splice(t--,1),e=l(l.s=r[0]))}return e}var n={},o={1:0},u=[];function l(t){if(n[t])return n[t].exports;var r=n[t]={i:t,l:!1,exports:{}};return e[t].call(r.exports,r,r.exports,l),r.l=!0,r.exports}l.m=e,l.c=n,l.d=function(e,t,r){l.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},l.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},l.t=function(e,t){if(1&t&&(e=l(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(l.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var n in e)l.d(r,n,function(t){return e[t]}.bind(null,n));return r},l.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return l.d(t,"a",t),t},l.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},l.p="/";var i=this.webpackJsonpclient=this.webpackJsonpclient||[],f=i.push.bind(i);i.push=t,i=i.slice();for(var a=0;a<i.length;a++)t(i[a]);var c=f;r()}([])</script><script src="/static/js/2.0a039c31.chunk.js"></script><script src="/static/js/main.90c51a67.chunk.js"></script></body></html>

client/build/static/js/2.13365d78.chunk.js renamed to client/build/static/js/2.0a039c31.chunk.js

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

client/build/static/js/2.13365d78.chunk.js.map renamed to client/build/static/js/2.0a039c31.chunk.js.map

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

client/build/static/js/main.02bf0d2e.chunk.js

-2
This file was deleted.

client/build/static/js/main.02bf0d2e.chunk.js.map

-1
This file was deleted.

client/build/static/js/main.90c51a67.chunk.js

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

client/build/static/js/main.90c51a67.chunk.js.map

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

client/src/components/Chat/use-chat-handlers.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// @ts-check
22
import { useCallback } from "react";
33
import { useEffect, useState, useRef } from "react";
4-
import { addRoom, getMessages, getUsers } from "../../api";
4+
import { addRoom, getMessages } from "../../api";
55
import { useAppState } from "../../state";
66
import { parseRoomName, populateUsersFromLoadedMessages } from "../../utils";
77

client/src/hooks.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ const useSocket = (user, dispatch) => {
7979
});
8080
dispatch({
8181
type: "append message",
82-
payload: { id: message.roomId, message },
82+
payload: { id: message.roomId === undefined ? "0" : message.roomId, message },
8383
});
8484
});
8585
} else {

client/src/state.js

+3
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,9 @@ const reducer = (state, action) => {
8989
};
9090
}
9191
case "append message":
92+
if (state.rooms[action.payload.id] === undefined) {
93+
return state;
94+
}
9295
return {
9396
...state,
9497
rooms: {

marketplace.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
],
1818
"language": ["JavaScript"],
1919
"redis_commands": ["INCR", "SET", "ZRANGEBYSCORE"],
20-
"redis_features": [],
20+
"redis_features": ["PubSub"],
2121
"redis_modules": [],
2222
"app_image_urls": [
2323
"https://github.com/redis-developer/basic-redis-chat-app-demo-nodejs/blob/main/docs/screenshot001.png?raw=true"

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"dotenv": "^8.2.0",
1212
"express": "^4.17.1",
1313
"express-session": "^1.17.1",
14+
"ip": "^1.1.5",
1415
"moment": "^2.29.1",
1516
"node-random-name": "^1.0.1",
1617
"redis": "^3.0.2",

server/config.js

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// @ts-check
2+
3+
/** Get default port argument. */
4+
let DEFAULT_PORT = 4000;
5+
try {
6+
const newPort = parseInt(process.argv[2]);
7+
DEFAULT_PORT = isNaN(newPort) ? DEFAULT_PORT : newPort;
8+
} catch (e) {
9+
}
10+
11+
const PORT = process.env.PORT || DEFAULT_PORT;
12+
13+
const ipAddress = require('ip').address();
14+
15+
const SERVER_ID = `${ipAddress}:${PORT}`;
16+
17+
module.exports = { PORT, SERVER_ID };

server/index.js

+52-17
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,16 @@ const {
2020
smembers,
2121
sismember,
2222
srem,
23+
sub,
2324
auth: runRedisAuth,
2425
} = require("./redis");
2526
const { createUser, makeUsernameKey, createPrivateRoom, sanitise, getMessages } = require("./utils");
2627
const { createDemoData } = require("./demo-data");
27-
28-
/** Get default port argument. */
29-
let DEFAULT_PORT = 4000;
30-
try {
31-
const newPort = parseInt(process.argv[2]);
32-
DEFAULT_PORT = isNaN(newPort) ? DEFAULT_PORT : newPort;
33-
} catch (e) {
34-
}
28+
const { PORT, SERVER_ID } = require("./config");
3529

3630
const app = express();
3731
const server = require("http").createServer(app);
32+
3833
/** @type {SocketIO.Server} */
3934
const io =
4035
/** @ts-ignore */
@@ -47,12 +42,41 @@ const sessionMiddleware = session({
4742
resave: true,
4843
});
4944

50-
function auth(req, res, next) {
45+
const auth = (req, res, next) => {
5146
if (!req.session.user) {
5247
return res.sendStatus(403);
5348
}
5449
next();
55-
}
50+
};
51+
52+
const publish = (type, data) => {
53+
const outgoing = {
54+
serverId: SERVER_ID,
55+
type,
56+
data
57+
};
58+
redisClient.publish('MESSAGES', JSON.stringify(outgoing));
59+
};
60+
61+
const initPubSub = () => {
62+
/** We don't use channels here, since the contained message contains all the necessary data. */
63+
sub.on('message', (_, message) => {
64+
/**
65+
* @type {{
66+
* serverId: string;
67+
* type: string;
68+
* data: object;
69+
* }}
70+
**/
71+
const { serverId, type, data } = JSON.parse(message);
72+
/** We don't handle the pub/sub messages if the server is the same */
73+
if (serverId === SERVER_ID) {
74+
return;
75+
}
76+
io.emit(type, data);
77+
});
78+
sub.subscribe('MESSAGES');
79+
};
5680

5781
/** Initialize the app */
5882
(async () => {
@@ -84,6 +108,8 @@ async function runApp() {
84108
app.use(bodyParser.json());
85109
app.use("/", express.static(path.dirname(__dirname) + "/client/build"));
86110

111+
initPubSub();
112+
87113
/** Store session in redis. */
88114
app.use(sessionMiddleware);
89115
io.use((socket, next) => {
@@ -103,14 +129,19 @@ async function runApp() {
103129
}
104130
const userId = socket.request.session.user.id;
105131
await sadd("online_users", userId);
106-
socket.broadcast.emit("user.connected", {
132+
133+
const msg = {
107134
...socket.request.session.user,
108135
online: true,
109-
});
136+
};
137+
138+
publish("user.connected", msg);
139+
socket.broadcast.emit("user.connected", msg);
110140

111141
socket.on("room.join", (id) => {
112142
socket.join(`room:${id}`);
113143
});
144+
114145
socket.on(
115146
"message",
116147
/**
@@ -140,25 +171,30 @@ async function runApp() {
140171
const roomHasMessages = await exists(roomKey);
141172
if (isPrivate && !roomHasMessages) {
142173
const ids = message.roomId.split(":");
143-
socket.broadcast.emit(`show.room`, {
174+
const msg = {
144175
id: message.roomId,
145176
names: [
146177
await hmget(`user:${ids[0]}`, "username"),
147178
await hmget(`user:${ids[1]}`, "username"),
148179
],
149-
});
180+
};
181+
publish("show.room", msg);
182+
socket.broadcast.emit(`show.room`, msg);
150183
}
151184
await zadd(roomKey, "" + message.date, messageString);
185+
publish("message", message);
152186
io.to(roomKey).emit("message", message);
153187
}
154188
);
155189
socket.on("disconnect", async () => {
156190
const userId = socket.request.session.user.id;
157191
await srem("online_users", userId);
158-
socket.broadcast.emit("user.disconnected", {
192+
const msg = {
159193
...socket.request.session.user,
160194
online: false,
161-
});
195+
};
196+
publish("disconnect", msg);
197+
socket.broadcast.emit("user.disconnected", msg);
162198
});
163199
});
164200

@@ -335,6 +371,5 @@ async function runApp() {
335371
res.status(200).send(rooms);
336372
});
337373

338-
const PORT = process.env.PORT || DEFAULT_PORT;
339374
server.listen(PORT, () => console.log(`Listening on ${PORT}...`));
340375
}

server/redis.js

+23-13
Original file line numberDiff line numberDiff line change
@@ -7,28 +7,38 @@ const password = process.env.REDIS_PASSWORD || null;
77

88
const [host, port] = endpoint.split(":");
99

10-
/** @type {import('redis').RedisClient} */
11-
const client = redis.createClient(+port, host);
12-
13-
function resolvePromise(resolve, reject) {
10+
const resolvePromise = (resolve, reject) => {
1411
return (err, data) => {
1512
if (err) {
1613
reject(err);
1714
}
1815
resolve(data);
1916
};
20-
}
17+
};
18+
19+
const auth = (client) => new Promise((a, b) => {
20+
if (password === null) {
21+
a(true);
22+
} else {
23+
client.auth(password, resolvePromise(a, b));
24+
}
25+
});
26+
27+
/** @type {import('redis').RedisClient} */
28+
const client = redis.createClient(+port, host);
29+
30+
/** @type {import('redis').RedisClient} */
31+
const sub = redis.createClient(+port, host, password === null ? undefined : {
32+
password
33+
});
2134

2235
module.exports = {
2336
client,
24-
auth: () =>
25-
new Promise((a, b) => {
26-
if (password === null) {
27-
a(true);
28-
} else {
29-
client.auth(password, resolvePromise(a, b));
30-
}
31-
}),
37+
sub,
38+
auth: async () => {
39+
await auth(client);
40+
await auth(sub);
41+
},
3242
incr: (key = "key") =>
3343
new Promise((a, b) => client.incr(key, resolvePromise(a, b))),
3444
decr: (key = "key") =>

yarn.lock

+5
Original file line numberDiff line numberDiff line change
@@ -801,6 +801,11 @@ ini@~1.3.0:
801801
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
802802
integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
803803

804+
ip@^1.1.5:
805+
version "1.1.5"
806+
resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a"
807+
integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=
808+
804809
805810
version "1.9.1"
806811
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"

0 commit comments

Comments
 (0)