@eleven-am/pondsocket 0.1.56 → 0.1.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/.eslintrc.json +387 -0
  2. package/dist/LICENSE +674 -0
  3. package/dist/README.md +139 -0
  4. package/dist/package.json +51 -0
  5. package/{types.d.ts → dist/types.d.ts} +0 -5
  6. package/jest.config.js +11 -0
  7. package/package.json +3 -3
  8. package/src/abstracts/abstractRequest.test.ts +49 -0
  9. package/src/abstracts/abstractRequest.ts +56 -0
  10. package/src/abstracts/abstractResponse.ts +26 -0
  11. package/src/abstracts/middleware.test.ts +75 -0
  12. package/src/abstracts/middleware.ts +50 -0
  13. package/src/channel/channel.test.ts +501 -0
  14. package/src/channel/channel.ts +305 -0
  15. package/src/channel/eventRequest.test.ts +37 -0
  16. package/src/channel/eventRequest.ts +27 -0
  17. package/src/channel/eventResponse.test.ts +249 -0
  18. package/src/channel/eventResponse.ts +172 -0
  19. package/src/client/channel.test.ts +799 -0
  20. package/src/client/channel.ts +342 -0
  21. package/src/client.ts +124 -0
  22. package/src/endpoint/endpoint.test.ts +825 -0
  23. package/src/endpoint/endpoint.ts +304 -0
  24. package/src/endpoint/response.ts +106 -0
  25. package/src/enums.ts +52 -0
  26. package/src/errors/pondError.ts +32 -0
  27. package/src/express.ts +58 -0
  28. package/src/index.ts +3 -0
  29. package/src/lobby/JoinRequest.test.ts +48 -0
  30. package/src/lobby/JoinResponse.test.ts +162 -0
  31. package/src/lobby/joinRequest.ts +32 -0
  32. package/src/lobby/joinResponse.ts +146 -0
  33. package/src/lobby/lobby.ts +182 -0
  34. package/src/matcher/matcher.test.ts +103 -0
  35. package/src/matcher/matcher.ts +105 -0
  36. package/src/node.ts +33 -0
  37. package/src/presence/presence.ts +127 -0
  38. package/src/presence/presenceEngine.test.ts +143 -0
  39. package/src/server/pondSocket.ts +153 -0
  40. package/src/subjects/subject.test.ts +163 -0
  41. package/src/subjects/subject.ts +137 -0
  42. package/src/typedefs.d.ts +451 -0
  43. package/src/types.d.ts +89 -0
  44. package/tsconfig.build.json +7 -0
  45. package/tsconfig.json +12 -0
  46. /package/{abstracts → dist/abstracts}/abstractRequest.js +0 -0
  47. /package/{abstracts → dist/abstracts}/abstractResponse.js +0 -0
  48. /package/{abstracts → dist/abstracts}/middleware.js +0 -0
  49. /package/{channel → dist/channel}/channel.js +0 -0
  50. /package/{channel → dist/channel}/eventRequest.js +0 -0
  51. /package/{channel → dist/channel}/eventResponse.js +0 -0
  52. /package/{client → dist/client}/channel.js +0 -0
  53. /package/{client.d.ts → dist/client.d.ts} +0 -0
  54. /package/{client.js → dist/client.js} +0 -0
  55. /package/{endpoint → dist/endpoint}/endpoint.js +0 -0
  56. /package/{endpoint → dist/endpoint}/response.js +0 -0
  57. /package/{enums.js → dist/enums.js} +0 -0
  58. /package/{errors → dist/errors}/pondError.js +0 -0
  59. /package/{express.d.ts → dist/express.d.ts} +0 -0
  60. /package/{express.js → dist/express.js} +0 -0
  61. /package/{index.d.ts → dist/index.d.ts} +0 -0
  62. /package/{index.js → dist/index.js} +0 -0
  63. /package/{lobby → dist/lobby}/joinRequest.js +0 -0
  64. /package/{lobby → dist/lobby}/joinResponse.js +0 -0
  65. /package/{lobby → dist/lobby}/lobby.js +0 -0
  66. /package/{matcher → dist/matcher}/matcher.js +0 -0
  67. /package/{node.d.ts → dist/node.d.ts} +0 -0
  68. /package/{node.js → dist/node.js} +0 -0
  69. /package/{presence → dist/presence}/presence.js +0 -0
  70. /package/{server → dist/server}/pondSocket.js +0 -0
  71. /package/{subjects → dist/subjects}/subject.js +0 -0
@@ -0,0 +1,105 @@
1
+ // eslint-disable-next-line import/no-unresolved
2
+ import { Params, PondPath, EventParams } from '../types';
3
+
4
+ /**
5
+ * @desc Returns the {key: value} matches of a string
6
+ * @param path - the string to create the regex from
7
+ * @param address - the pattern to match
8
+ *
9
+ * @example
10
+ * /api/:id should match /api/123 and return { id: 123 }
11
+ * /api/:id/:name should match /api/123/abc and return { id: 123, name: abc }
12
+ * hello:id should match hello:123 and return { id: 123 }
13
+ * @private
14
+ */
15
+ function matchPath (path: string, address: string) {
16
+ const pathParts = path.split('/');
17
+ const addressParts = address.split('/');
18
+
19
+ const params: Record<string, string> = {};
20
+ const length = Math.max(pathParts.length, addressParts.length);
21
+
22
+ for (let i = 0; i < length; i++) {
23
+ const pathPart = pathParts[i];
24
+ const addressPart = addressParts[i];
25
+
26
+ if (pathPart === undefined || addressPart === undefined) {
27
+ return null;
28
+ } else if (pathPart === '*') {
29
+ return params as Params<typeof path>;
30
+ } else if (pathPart.startsWith(':')) {
31
+ params[pathPart.slice(1)] = addressPart;
32
+ } else if (pathPart !== addressPart) {
33
+ return null;
34
+ }
35
+ }
36
+
37
+ return params as Params<typeof path>;
38
+ }
39
+
40
+ /**
41
+ * @desc Given a Regex expression it returns an empty object if the address matches
42
+ * @param regex - The regex to match the address against
43
+ * @param address - The address ot match
44
+ */
45
+ function matchRegex (regex: RegExp, address: string) {
46
+ if (address.match(regex)) {
47
+ return {};
48
+ }
49
+
50
+ return null;
51
+ }
52
+
53
+ /**
54
+ * @desc Creates an object from the params of a path
55
+ * @param address - the path to create the object from
56
+ *
57
+ * @example
58
+ * /api/id?name=abc should return { name: 'abc' }
59
+ * /api/id?name=abc&age=123 should return { name: 'abc', age: '123' }
60
+ */
61
+ function getQuery (address: string) {
62
+ const obj: { [p: string]: string } = {};
63
+ const params = address.split('?')[1];
64
+
65
+ if (params) {
66
+ params.split('&').forEach((param) => {
67
+ const [key, value] = param.split('=');
68
+
69
+ obj[key] = value;
70
+ });
71
+ }
72
+
73
+ return obj;
74
+ }
75
+
76
+ /**
77
+ * @desc Generates a pond request resolver object
78
+ * @param path - the path to resolve
79
+ * @param address - the address to resolve
80
+ */
81
+ export function parseAddress <Path extends string> (path: PondPath<Path>, address: string) {
82
+ let params: Record<string, string> | null;
83
+ const [paramsPath] = address.split('?');
84
+
85
+ if (typeof path === 'string') {
86
+ params = matchPath(path, paramsPath);
87
+
88
+ if (params === null) {
89
+ return null;
90
+ }
91
+ } else {
92
+ params = matchRegex(path, paramsPath);
93
+
94
+ if (params === null) {
95
+ return null;
96
+ }
97
+ }
98
+
99
+ const query = getQuery(address);
100
+
101
+ return {
102
+ params,
103
+ query,
104
+ } as EventParams<Path>;
105
+ }
package/src/node.ts ADDED
@@ -0,0 +1,33 @@
1
+ import PondSocketClient from './client';
2
+ // eslint-disable-next-line import/no-unresolved
3
+ import { ChannelEvent } from './types';
4
+
5
+ const WebSocket = require('websocket').w3cwebsocket as typeof import('websocket').w3cwebsocket;
6
+
7
+ export default class PondClient extends PondSocketClient {
8
+ /**
9
+ * @desc Connects to the server and returns the socket.
10
+ */
11
+ public connect (backoff = 1) {
12
+ const socket = new WebSocket(this._address.toString());
13
+
14
+ socket.onopen = () => {
15
+ this._connectionState.publish(true);
16
+ };
17
+
18
+ socket.onmessage = (message) => {
19
+ const data = JSON.parse(message.data as string) as ChannelEvent;
20
+
21
+ this._broadcaster.publish(data);
22
+ };
23
+
24
+ socket.onerror = () => {
25
+ this._connectionState.publish(false);
26
+ setTimeout(() => {
27
+ this.connect(backoff * 2);
28
+ }, backoff * 1000);
29
+ };
30
+
31
+ this._socket = socket;
32
+ }
33
+ }
@@ -0,0 +1,127 @@
1
+ import { ChannelEngine } from '../channel/channel';
2
+ import { PresenceEventTypes, SystemSender, ServerActions } from '../enums';
3
+ import { PresenceError } from '../errors/pondError';
4
+ // eslint-disable-next-line import/no-unresolved
5
+ import { PondPresence, UserPresences, PresencePayload } from '../types';
6
+
7
+ export class PresenceEngine {
8
+ readonly #presenceMap: Map<string, PondPresence>;
9
+
10
+ readonly #channel: ChannelEngine;
11
+
12
+ constructor (channel: ChannelEngine) {
13
+ this.#channel = channel;
14
+ this.#presenceMap = new Map<string, PondPresence>();
15
+ }
16
+
17
+ /**
18
+ * @desc Lists all the presence of the users
19
+ */
20
+ public getPresence (): UserPresences {
21
+ return Array.from(this.#presenceMap.entries())
22
+ .reduce((acc, [key, value]) => {
23
+ acc[key] = value;
24
+
25
+ return acc;
26
+ }, {} as UserPresences);
27
+ }
28
+
29
+ /**
30
+ * @desc Returns the presence of a user
31
+ * @param userId - The id of the user
32
+ */
33
+ public getUserPresence (userId: string): PondPresence | undefined {
34
+ return this.#presenceMap.get(userId);
35
+ }
36
+
37
+ /**
38
+ * @desc Tracks a presence
39
+ * @param presenceKey - The key of the presence
40
+ * @param presence - The presence
41
+ */
42
+ public trackPresence (presenceKey: string, presence: PondPresence) {
43
+ if (!this.#presenceMap.has(presenceKey)) {
44
+ this.#presenceMap.set(presenceKey, presence);
45
+ this.#publish(PresenceEventTypes.JOIN, {
46
+ changed: presence,
47
+ presence: Array.from(this.#presenceMap.values()),
48
+ });
49
+ } else {
50
+ const code = 400;
51
+ const message = `PresenceEngine: Presence with key ${presenceKey} already exists`;
52
+
53
+ throw new PresenceError(message, code, this.#channel.name, PresenceEventTypes.JOIN);
54
+ }
55
+ }
56
+
57
+ /**
58
+ * @desc Removes a presence from the presence engine
59
+ * @param presenceKey - The key of the presence
60
+ * @param graceful - Whether to gracefully remove the presence
61
+ */
62
+ public removePresence (presenceKey: string, graceful = false) {
63
+ const presence = this.#presenceMap.get(presenceKey);
64
+
65
+ if (presence) {
66
+ this.#presenceMap.delete(presenceKey);
67
+ this.#publish(PresenceEventTypes.LEAVE, {
68
+ changed: presence,
69
+ presence: Array.from(this.#presenceMap.values()),
70
+ });
71
+ } else if (!graceful) {
72
+ const code = 404;
73
+ const message = `PresenceEngine: Presence with key ${presenceKey} does not exist`;
74
+
75
+ throw new PresenceError(message, code, this.#channel.name, PresenceEventTypes.LEAVE);
76
+ }
77
+ }
78
+
79
+ /**
80
+ * @desc Updates a presence
81
+ * @param presenceKey - The key of the presence
82
+ * @param presence - The new presence
83
+ */
84
+ public updatePresence (presenceKey: string, presence: PondPresence) {
85
+ const oldPresence = this.#presenceMap.get(presenceKey);
86
+
87
+ if (oldPresence) {
88
+ const newPresence = {
89
+ ...oldPresence,
90
+ ...presence,
91
+ };
92
+
93
+ this.#presenceMap.set(presenceKey, newPresence);
94
+ this.#publish(PresenceEventTypes.UPDATE, {
95
+ changed: newPresence,
96
+ presence: Array.from(this.#presenceMap.values()),
97
+ });
98
+ } else {
99
+ const code = 404;
100
+ const message = `PresenceEngine: Presence with key ${presenceKey} does not exist`;
101
+
102
+ throw new PresenceError(message, code, this.#channel.name, PresenceEventTypes.UPDATE);
103
+ }
104
+ }
105
+
106
+ /**
107
+ * @desc Publishes a presence event to all users in the channel
108
+ * @param event - The event type
109
+ * @param payload - The payload of the event
110
+ * @private
111
+ */
112
+ #publish (event: PresenceEventTypes, payload: PresencePayload) {
113
+ const recipients = Array.from(this.#presenceMap.keys());
114
+
115
+ if (recipients.length === 0) {
116
+ return;
117
+ }
118
+
119
+ this.#channel.sendMessage(
120
+ SystemSender.CHANNEL,
121
+ recipients,
122
+ ServerActions.PRESENCE,
123
+ event,
124
+ payload,
125
+ );
126
+ }
127
+ }
@@ -0,0 +1,143 @@
1
+ import { PresenceEngine } from './presence';
2
+ import { ChannelEngine } from '../channel/channel';
3
+ import { createChannelEngine } from '../channel/eventResponse.test';
4
+ import { PresenceEventTypes, SystemSender, ServerActions } from '../enums';
5
+ // eslint-disable-next-line import/no-unresolved
6
+ import { PondPresence } from '../types';
7
+
8
+ describe('PresenceEngine', () => {
9
+ let presenceEngine: PresenceEngine;
10
+ let presence: PondPresence;
11
+ let presenceKey: string;
12
+ let channel: ChannelEngine;
13
+
14
+ beforeEach(() => {
15
+ channel = createChannelEngine();
16
+ channel.addUser('presenceKey', { assign: 'assign' }, () => {
17
+ // do nothing
18
+ });
19
+ presenceEngine = new PresenceEngine(channel);
20
+ presence = {
21
+ id: 'id',
22
+ name: 'name',
23
+ color: 'color',
24
+ type: 'type',
25
+ location: 'location',
26
+ };
27
+ presenceKey = 'presenceKey';
28
+ });
29
+
30
+ describe('trackPresence', () => {
31
+ it('should insert a presence into the presence engine', () => {
32
+ // spy on the channel sendMessage method
33
+ jest.spyOn(channel, 'sendMessage');
34
+ presenceEngine.trackPresence(presenceKey, presence);
35
+ expect(channel.sendMessage).toHaveBeenCalledWith(
36
+ SystemSender.CHANNEL,
37
+ ['presenceKey'],
38
+ ServerActions.PRESENCE,
39
+ PresenceEventTypes.JOIN,
40
+ {
41
+ changed: presence,
42
+ presence: [presence],
43
+ },
44
+ );
45
+ });
46
+
47
+ it('should throw an error if the presence already exists', () => {
48
+ presenceEngine.trackPresence(presenceKey, presence);
49
+ expect(() => presenceEngine.trackPresence(presenceKey, presence)).toThrowError(`PresenceEngine: Presence with key ${presenceKey} already exists`);
50
+ });
51
+ });
52
+
53
+ describe('updatePresence', () => {
54
+ it('should update a presence', () => {
55
+ presenceEngine.trackPresence(presenceKey, presence);
56
+ const newPresence = {
57
+ id: 'id',
58
+ name: 'name',
59
+ color: 'color',
60
+ type: 'type',
61
+ location: 'location',
62
+ };
63
+
64
+ jest.spyOn(channel, 'sendMessage');
65
+ presenceEngine.updatePresence(presenceKey, newPresence);
66
+ expect(channel.sendMessage).toHaveBeenCalledWith(
67
+ SystemSender.CHANNEL,
68
+ ['presenceKey'],
69
+ ServerActions.PRESENCE,
70
+ PresenceEventTypes.UPDATE,
71
+ {
72
+ changed: {
73
+ ...presence,
74
+ ...newPresence,
75
+ },
76
+ presence: [newPresence],
77
+ },
78
+ );
79
+ });
80
+
81
+ it('should throw an error if the presence does not exist', () => {
82
+ expect(() => presenceEngine.updatePresence(presenceKey, presence)).toThrowError(`PresenceEngine: Presence with key ${presenceKey} does not exist`);
83
+ });
84
+ });
85
+
86
+ describe('removePresence', () => {
87
+ it('should remove a presence from the presence engine', () => {
88
+ const listener = jest.spyOn(channel, 'sendMessage');
89
+
90
+ // before we can track a presence, we need make sure the user is in the channel
91
+ channel.addUser('presenceKey2', { assign: 'assign' }, () => {
92
+ // do nothing
93
+ });
94
+ presenceEngine.trackPresence(presenceKey, presence);
95
+ presenceEngine.trackPresence('presenceKey2', {
96
+ ...presence,
97
+ key: 'presence2',
98
+ });
99
+
100
+ expect(listener).toHaveBeenCalledTimes(2);
101
+
102
+ // clear the mock
103
+ listener.mockClear();
104
+
105
+ presenceEngine.removePresence(presenceKey);
106
+
107
+ expect(listener).toHaveBeenCalledTimes(1);
108
+ expect(listener).toHaveBeenCalledWith(
109
+ SystemSender.CHANNEL,
110
+ ['presenceKey2'],
111
+ ServerActions.PRESENCE,
112
+ PresenceEventTypes.LEAVE,
113
+ {
114
+ changed: presence,
115
+ presence: [
116
+ {
117
+ ...presence,
118
+ key: 'presence2',
119
+ },
120
+ ],
121
+ },
122
+ );
123
+
124
+ listener.mockClear();
125
+ presenceEngine.removePresence('presenceKey2');
126
+ expect(listener).toHaveBeenCalledTimes(0);
127
+ });
128
+
129
+ it('should throw an error if the presence does not exist', () => {
130
+ expect(() => presenceEngine.removePresence(presenceKey)).toThrowError(`PresenceEngine: Presence with key ${presenceKey} does not exist`);
131
+ });
132
+ });
133
+
134
+ describe('getPresence', () => {
135
+ it('should return the presence', () => {
136
+ presenceEngine.trackPresence(presenceKey, presence);
137
+ const data: any = {};
138
+
139
+ data[presenceKey] = presence;
140
+ expect(presenceEngine.getPresence()).toEqual(data);
141
+ });
142
+ });
143
+ });
@@ -0,0 +1,153 @@
1
+ import { Server as HTTPServer, IncomingHttpHeaders } from 'http';
2
+
3
+ import { WebSocketServer, WebSocket } from 'ws';
4
+
5
+ import { Middleware } from '../abstracts/middleware';
6
+ import { Endpoint } from '../endpoint/endpoint';
7
+ import { ConnectionResponse } from '../endpoint/response';
8
+ import { ServerActions, SystemSender, ErrorTypes } from '../enums';
9
+ import { parseAddress } from '../matcher/matcher';
10
+ // eslint-disable-next-line import/no-unresolved
11
+ import { PondPath, IncomingConnection } from '../types';
12
+
13
+ interface SocketRequest {
14
+ id: string;
15
+ headers: IncomingHttpHeaders;
16
+ address: string;
17
+ }
18
+
19
+ export class PondSocket {
20
+ readonly #server: HTTPServer;
21
+
22
+ readonly #socketServer: WebSocketServer;
23
+
24
+ readonly #middleware: Middleware<SocketRequest, WebSocket>;
25
+
26
+ constructor (server?: HTTPServer, socketServer?: WebSocketServer) {
27
+ this.#server = server ?? new HTTPServer();
28
+ this.#socketServer = socketServer ?? new WebSocketServer({ noServer: true });
29
+ this.#middleware = new Middleware();
30
+ this.#init();
31
+ }
32
+
33
+ /**
34
+ * @desc Specifies the port to listen on
35
+ * @param args - the arguments to pass to the server
36
+ */
37
+ public listen (...args: any[]) {
38
+ return this.#server.listen(...args);
39
+ }
40
+
41
+ /**
42
+ * @desc Closes the server
43
+ * @param callback - the callback to call when the server is closed
44
+ */
45
+ public close (callback?: () => void) {
46
+ return this.#server.close(callback);
47
+ }
48
+
49
+ /**
50
+ * @desc Accepts a new socket upgrade request on the provided endpoint using the handler function to authenticate the socket
51
+ * @param path - the pattern to accept || can also be a regex
52
+ * @param handler - the handler function to authenticate the socket
53
+ * @example
54
+ * const endpoint = pond.createEndpoint('/api/socket', (req, res) => {
55
+ * const token = req.query.token;
56
+ * if (!token)
57
+ * return res.reject('No token provided');
58
+ * res.accept({
59
+ * assign: {
60
+ * token
61
+ * }
62
+ * });
63
+ * })
64
+ */
65
+ public createEndpoint<Path extends string> (path: PondPath<Path>, handler: (request: IncomingConnection<Path>, response: ConnectionResponse) => void | Promise<void>) {
66
+ const endpoint = new Endpoint(this.#socketServer);
67
+
68
+ this.#middleware.use((req, socket, next) => {
69
+ const event = parseAddress(path, req.address);
70
+
71
+ if (event) {
72
+ const request: IncomingConnection<Path> = {
73
+ ...event,
74
+ headers: req.headers,
75
+ address: req.address,
76
+ id: req.id,
77
+ };
78
+
79
+ const response = new ConnectionResponse(socket, endpoint, request.id);
80
+
81
+ return handler(request, response);
82
+ }
83
+
84
+ return next();
85
+ });
86
+
87
+ return endpoint;
88
+ }
89
+
90
+ /**
91
+ * @desc managed the heartbeat of the socket server
92
+ * @private
93
+ */
94
+ #manageHeartbeat () {
95
+ this.#socketServer.on('connection', (socket: WebSocket & { isAlive?: boolean }) => {
96
+ socket.on('pong', () => {
97
+ socket.isAlive = true;
98
+ });
99
+ });
100
+
101
+ const interval = setInterval(() => {
102
+ this.#socketServer.clients.forEach((socket: WebSocket & { isAlive?: boolean }) => {
103
+ if (socket.isAlive === false) {
104
+ return socket.terminate();
105
+ }
106
+
107
+ socket.isAlive = false;
108
+ socket.ping();
109
+ });
110
+ }, 30000);
111
+
112
+ this.#socketServer.on('close', () => clearInterval(interval));
113
+ }
114
+
115
+ /**
116
+ * @desc initialises the socket server
117
+ * @private
118
+ */
119
+ #init () {
120
+ this.#manageHeartbeat();
121
+
122
+ this.#server.on('error', (error) => {
123
+ throw new Error(error.message);
124
+ });
125
+
126
+ this.#server.on('upgrade', (req, socket, head) => {
127
+ const clientId = req.headers['sec-websocket-key'] as string;
128
+ const request: SocketRequest = {
129
+ id: clientId,
130
+ headers: req.headers,
131
+ address: req.url || '',
132
+ };
133
+
134
+ this.#socketServer.handleUpgrade(req, socket, head, (socket) => {
135
+ this.#socketServer.emit('connection', socket);
136
+ this.#middleware.run(request, socket, () => {
137
+ const message = {
138
+ action: ServerActions.ERROR,
139
+ event: ErrorTypes.HANDLER_NOT_FOUND,
140
+ channelName: SystemSender.ENDPOINT,
141
+ payload: {
142
+ message: 'No endpoint found',
143
+ code: 404,
144
+ },
145
+ };
146
+
147
+ socket.send(JSON.stringify(message));
148
+ socket.close();
149
+ });
150
+ });
151
+ });
152
+ }
153
+ }