@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.
- package/.eslintrc.json +387 -0
- package/dist/LICENSE +674 -0
- package/dist/README.md +139 -0
- package/dist/package.json +51 -0
- package/{types.d.ts → dist/types.d.ts} +0 -5
- package/jest.config.js +11 -0
- package/package.json +3 -3
- package/src/abstracts/abstractRequest.test.ts +49 -0
- package/src/abstracts/abstractRequest.ts +56 -0
- package/src/abstracts/abstractResponse.ts +26 -0
- package/src/abstracts/middleware.test.ts +75 -0
- package/src/abstracts/middleware.ts +50 -0
- package/src/channel/channel.test.ts +501 -0
- package/src/channel/channel.ts +305 -0
- package/src/channel/eventRequest.test.ts +37 -0
- package/src/channel/eventRequest.ts +27 -0
- package/src/channel/eventResponse.test.ts +249 -0
- package/src/channel/eventResponse.ts +172 -0
- package/src/client/channel.test.ts +799 -0
- package/src/client/channel.ts +342 -0
- package/src/client.ts +124 -0
- package/src/endpoint/endpoint.test.ts +825 -0
- package/src/endpoint/endpoint.ts +304 -0
- package/src/endpoint/response.ts +106 -0
- package/src/enums.ts +52 -0
- package/src/errors/pondError.ts +32 -0
- package/src/express.ts +58 -0
- package/src/index.ts +3 -0
- package/src/lobby/JoinRequest.test.ts +48 -0
- package/src/lobby/JoinResponse.test.ts +162 -0
- package/src/lobby/joinRequest.ts +32 -0
- package/src/lobby/joinResponse.ts +146 -0
- package/src/lobby/lobby.ts +182 -0
- package/src/matcher/matcher.test.ts +103 -0
- package/src/matcher/matcher.ts +105 -0
- package/src/node.ts +33 -0
- package/src/presence/presence.ts +127 -0
- package/src/presence/presenceEngine.test.ts +143 -0
- package/src/server/pondSocket.ts +153 -0
- package/src/subjects/subject.test.ts +163 -0
- package/src/subjects/subject.ts +137 -0
- package/src/typedefs.d.ts +451 -0
- package/src/types.d.ts +89 -0
- package/tsconfig.build.json +7 -0
- package/tsconfig.json +12 -0
- /package/{abstracts → dist/abstracts}/abstractRequest.js +0 -0
- /package/{abstracts → dist/abstracts}/abstractResponse.js +0 -0
- /package/{abstracts → dist/abstracts}/middleware.js +0 -0
- /package/{channel → dist/channel}/channel.js +0 -0
- /package/{channel → dist/channel}/eventRequest.js +0 -0
- /package/{channel → dist/channel}/eventResponse.js +0 -0
- /package/{client → dist/client}/channel.js +0 -0
- /package/{client.d.ts → dist/client.d.ts} +0 -0
- /package/{client.js → dist/client.js} +0 -0
- /package/{endpoint → dist/endpoint}/endpoint.js +0 -0
- /package/{endpoint → dist/endpoint}/response.js +0 -0
- /package/{enums.js → dist/enums.js} +0 -0
- /package/{errors → dist/errors}/pondError.js +0 -0
- /package/{express.d.ts → dist/express.d.ts} +0 -0
- /package/{express.js → dist/express.js} +0 -0
- /package/{index.d.ts → dist/index.d.ts} +0 -0
- /package/{index.js → dist/index.js} +0 -0
- /package/{lobby → dist/lobby}/joinRequest.js +0 -0
- /package/{lobby → dist/lobby}/joinResponse.js +0 -0
- /package/{lobby → dist/lobby}/lobby.js +0 -0
- /package/{matcher → dist/matcher}/matcher.js +0 -0
- /package/{node.d.ts → dist/node.d.ts} +0 -0
- /package/{node.js → dist/node.js} +0 -0
- /package/{presence → dist/presence}/presence.js +0 -0
- /package/{server → dist/server}/pondSocket.js +0 -0
- /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
|
+
}
|