@afterlink/server 1.0.0
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/package.json +37 -0
- package/src/Connection.js +146 -0
- package/src/FrameAccumulator.js +35 -0
- package/src/Router.js +252 -0
- package/src/Server.js +101 -0
- package/src/index.js +3 -0
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@afterlink/server",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "AfterLink Protocol Server SDK - TCP server with routing, middleware, and pub/sub",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/AJAYMYTH/AfterLink.git",
|
|
10
|
+
"directory": "packages/server"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"afterlink",
|
|
14
|
+
"server",
|
|
15
|
+
"tcp",
|
|
16
|
+
"protocol",
|
|
17
|
+
"pubsub",
|
|
18
|
+
"messaging",
|
|
19
|
+
"routing",
|
|
20
|
+
"middleware"
|
|
21
|
+
],
|
|
22
|
+
"files": [
|
|
23
|
+
"src/**/*.js"
|
|
24
|
+
],
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"test": "vitest run"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@afterlink/core": "1.0.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"vitest": "^1.6.0"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
const {
|
|
2
|
+
Frame,
|
|
3
|
+
FrameTypes: { HELLO, HELLO_ACK, ERROR },
|
|
4
|
+
Serializer,
|
|
5
|
+
} = require('@afterlink/core');
|
|
6
|
+
const FrameAccumulator = require('./FrameAccumulator');
|
|
7
|
+
|
|
8
|
+
class Connection {
|
|
9
|
+
constructor(socket, router, options = {}) {
|
|
10
|
+
this.socket = socket;
|
|
11
|
+
this.router = router;
|
|
12
|
+
this.session = null;
|
|
13
|
+
this.options = options;
|
|
14
|
+
this._id = `conn_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
15
|
+
this._closed = false;
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
this.accumulator = new FrameAccumulator(this._handleFrame.bind(this));
|
|
19
|
+
} catch (err) {
|
|
20
|
+
this._onError(err);
|
|
21
|
+
socket.destroy();
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
socket.on('data', (data) => this._onData(data));
|
|
26
|
+
socket.on('close', () => this._onClose());
|
|
27
|
+
socket.on('error', (err) => this._onError(err));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
getId() {
|
|
31
|
+
return this._id;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
getSession() {
|
|
35
|
+
return this.session;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
getRemoteAddress() {
|
|
39
|
+
return this.socket.remoteAddress || 'unknown';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
_onData(data) {
|
|
43
|
+
if (this._closed) return;
|
|
44
|
+
try {
|
|
45
|
+
this.accumulator.push(data);
|
|
46
|
+
} catch (err) {
|
|
47
|
+
this._onError(err);
|
|
48
|
+
this.sendError('PROTOCOL_ERROR', err.message);
|
|
49
|
+
this.socket.destroy();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
_handleFrame(frame) {
|
|
54
|
+
if (this._closed) return;
|
|
55
|
+
|
|
56
|
+
if (frame.type === HELLO && !this.session) {
|
|
57
|
+
this._handleHandshake(frame);
|
|
58
|
+
} else if (this.session) {
|
|
59
|
+
this.router.dispatch(frame, this).catch((err) => {
|
|
60
|
+
this._onError(err);
|
|
61
|
+
});
|
|
62
|
+
} else {
|
|
63
|
+
this.sendError('AUTH_REQUIRED', 'Send HELLO first');
|
|
64
|
+
this.socket.destroy();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
_handleHandshake(frame) {
|
|
69
|
+
try {
|
|
70
|
+
const data = Serializer.decode(frame.payload);
|
|
71
|
+
const sessionId = `session_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
72
|
+
|
|
73
|
+
if (this.options.auth && data.auth) {
|
|
74
|
+
this._validateAuth(data.auth);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
this.session = {
|
|
78
|
+
id: sessionId,
|
|
79
|
+
version: data.version || 'AL/1',
|
|
80
|
+
capabilities: data.capabilities || [],
|
|
81
|
+
connectedAt: new Date().toISOString(),
|
|
82
|
+
remoteAddress: this.getRemoteAddress(),
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const ackPayload = Serializer.encode({
|
|
86
|
+
session_id: sessionId,
|
|
87
|
+
server_version: 'AL/1',
|
|
88
|
+
capabilities: ['streaming', 'pubsub', 'compression'],
|
|
89
|
+
});
|
|
90
|
+
this.send(HELLO_ACK, 0, frame.messageId, ackPayload);
|
|
91
|
+
} catch (err) {
|
|
92
|
+
this.sendError('AUTH_INVALID', err.message);
|
|
93
|
+
this.socket.destroy();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
_validateAuth(token) {
|
|
98
|
+
if (this.options.auth?.type === 'jwt' && this.options.auth.secret) {
|
|
99
|
+
const { jwtVerify } = require('jose');
|
|
100
|
+
return jwtVerify(token, new TextEncoder().encode(this.options.auth.secret));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
send(type, flags, messageId, payload) {
|
|
105
|
+
if (this._closed || this.socket.destroyed) return;
|
|
106
|
+
try {
|
|
107
|
+
const frame = Frame.encode(type, flags, messageId, payload);
|
|
108
|
+
this.socket.write(frame);
|
|
109
|
+
} catch (err) {
|
|
110
|
+
this._onError(err);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
sendError(code, message, messageId = 0) {
|
|
115
|
+
if (this._closed || this.socket.destroyed) return;
|
|
116
|
+
try {
|
|
117
|
+
const payload = Serializer.encode({ code, message });
|
|
118
|
+
this.send(ERROR, 0, messageId, payload);
|
|
119
|
+
} catch (err) {
|
|
120
|
+
this._onError(err);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
_onError(err) {
|
|
125
|
+
if (err.code === 'ECONNRESET' || err.code === 'EPIPE') return;
|
|
126
|
+
console.error(`[AfterLink] Connection ${this._id} error:`, err.message);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
_onClose() {
|
|
130
|
+
this._closed = true;
|
|
131
|
+
this.router.onDisconnect(this);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
destroy() {
|
|
135
|
+
this._closed = true;
|
|
136
|
+
if (!this.socket.destroyed) {
|
|
137
|
+
this.socket.destroy();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
isActive() {
|
|
142
|
+
return !this._closed && !this.socket.destroyed && this.session !== null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
module.exports = Connection;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const { Frame } = require('@afterlink/core');
|
|
2
|
+
|
|
3
|
+
class FrameAccumulator {
|
|
4
|
+
constructor(onFrame, options = {}) {
|
|
5
|
+
this.buffer = Buffer.alloc(0);
|
|
6
|
+
this.onFrame = onFrame;
|
|
7
|
+
this.maxBufferSize = options.maxBufferSize || 64 * 1024 * 1024; // 64MB
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
push(data) {
|
|
11
|
+
this.buffer = Buffer.concat([this.buffer, data]);
|
|
12
|
+
|
|
13
|
+
if (this.buffer.length > this.maxBufferSize) {
|
|
14
|
+
this.buffer = Buffer.alloc(0);
|
|
15
|
+
throw new Error(`Accumulator buffer exceeded maximum size of ${this.maxBufferSize} bytes`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
while (this.buffer.length > 0) {
|
|
19
|
+
const frame = Frame.decode(this.buffer);
|
|
20
|
+
if (!frame) break;
|
|
21
|
+
this.buffer = this.buffer.slice(frame.totalSize);
|
|
22
|
+
this.onFrame(frame);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
reset() {
|
|
27
|
+
this.buffer = Buffer.alloc(0);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
getBufferLength() {
|
|
31
|
+
return this.buffer.length;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = FrameAccumulator;
|
package/src/Router.js
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
const {
|
|
2
|
+
FrameTypes: {
|
|
3
|
+
REQUEST,
|
|
4
|
+
RESPONSE,
|
|
5
|
+
ERROR,
|
|
6
|
+
SUBSCRIBE,
|
|
7
|
+
UNSUBSCRIBE,
|
|
8
|
+
PUBLISH,
|
|
9
|
+
},
|
|
10
|
+
Serializer,
|
|
11
|
+
} = require('@afterlink/core');
|
|
12
|
+
|
|
13
|
+
class Router {
|
|
14
|
+
constructor() {
|
|
15
|
+
this.routes = new Map();
|
|
16
|
+
this.middlewares = [];
|
|
17
|
+
this.pubSubBroker = new PubSubBroker();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
register(route, handler, schema = null) {
|
|
21
|
+
if (typeof handler !== 'function') {
|
|
22
|
+
throw new TypeError(`Handler for route '${route}' must be a function`);
|
|
23
|
+
}
|
|
24
|
+
this.routes.set(route, { handler, schema });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
addMiddleware(middleware) {
|
|
28
|
+
if (typeof middleware !== 'function') {
|
|
29
|
+
throw new TypeError('Middleware must be a function');
|
|
30
|
+
}
|
|
31
|
+
this.middlewares.push(middleware);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async dispatch(frame, connection) {
|
|
35
|
+
const { type, messageId, payload } = frame;
|
|
36
|
+
|
|
37
|
+
switch (type) {
|
|
38
|
+
case REQUEST:
|
|
39
|
+
await this._handleRequest(payload, messageId, connection);
|
|
40
|
+
break;
|
|
41
|
+
case SUBSCRIBE:
|
|
42
|
+
this._handleSubscribe(payload, messageId, connection);
|
|
43
|
+
break;
|
|
44
|
+
case UNSUBSCRIBE:
|
|
45
|
+
this._handleUnsubscribe(payload, messageId, connection);
|
|
46
|
+
break;
|
|
47
|
+
case PUBLISH:
|
|
48
|
+
this._handlePublish(payload, connection);
|
|
49
|
+
break;
|
|
50
|
+
default:
|
|
51
|
+
connection.sendError('PROTOCOL_ERROR', `Unexpected frame type: 0x${type.toString(16)}`, messageId);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async _handleRequest(payload, messageId, connection) {
|
|
56
|
+
let route, body;
|
|
57
|
+
try {
|
|
58
|
+
const decoded = Serializer.decode(payload);
|
|
59
|
+
route = decoded.route;
|
|
60
|
+
body = decoded.body || {};
|
|
61
|
+
} catch (err) {
|
|
62
|
+
connection.sendError('PROTOCOL_ERROR', 'Invalid request payload', messageId);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const routeConfig = this.routes.get(route);
|
|
67
|
+
if (!routeConfig) {
|
|
68
|
+
connection.sendError('ROUTE_NOT_FOUND', `Route '${route}' not found`, messageId);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (routeConfig.schema) {
|
|
73
|
+
try {
|
|
74
|
+
routeConfig.schema.parse(body);
|
|
75
|
+
} catch (err) {
|
|
76
|
+
const message = err.errors?.[0]?.message || err.message;
|
|
77
|
+
connection.sendError('VALIDATION_ERROR', message, messageId);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let responseSent = false;
|
|
83
|
+
const req = { body, session: connection.session, route };
|
|
84
|
+
const res = {
|
|
85
|
+
send: (data) => {
|
|
86
|
+
if (responseSent) return;
|
|
87
|
+
responseSent = true;
|
|
88
|
+
try {
|
|
89
|
+
const responsePayload = Serializer.encode({ status: 'ok', body: data });
|
|
90
|
+
connection.send(RESPONSE, 0, messageId, responsePayload);
|
|
91
|
+
} catch (err) {
|
|
92
|
+
connection.sendError('INTERNAL_ERROR', 'Failed to encode response', messageId);
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
await this._runMiddlewares(req, async () => {
|
|
99
|
+
await routeConfig.handler(req, res);
|
|
100
|
+
});
|
|
101
|
+
} catch (err) {
|
|
102
|
+
if (!responseSent) {
|
|
103
|
+
connection.sendError('INTERNAL_ERROR', err.message, messageId);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async _runMiddlewares(req, next) {
|
|
109
|
+
let index = 0;
|
|
110
|
+
const run = async () => {
|
|
111
|
+
if (index >= this.middlewares.length) {
|
|
112
|
+
return next();
|
|
113
|
+
}
|
|
114
|
+
const middleware = this.middlewares[index++];
|
|
115
|
+
await middleware(req, run);
|
|
116
|
+
};
|
|
117
|
+
await run();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
_handleSubscribe(payload, messageId, connection) {
|
|
121
|
+
let topic;
|
|
122
|
+
try {
|
|
123
|
+
topic = Serializer.decode(payload).topic;
|
|
124
|
+
} catch {
|
|
125
|
+
connection.sendError('PROTOCOL_ERROR', 'Invalid subscribe payload', messageId);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!topic || typeof topic !== 'string') {
|
|
130
|
+
connection.sendError('VALIDATION_ERROR', 'Topic must be a non-empty string', messageId);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
this.pubSubBroker.subscribe(topic, connection);
|
|
135
|
+
const ackPayload = Serializer.encode({ topic, sub_id: `s_${Date.now()}` });
|
|
136
|
+
connection.send(RESPONSE, 0, messageId, ackPayload);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
_handleUnsubscribe(payload, messageId, connection) {
|
|
140
|
+
let topic;
|
|
141
|
+
try {
|
|
142
|
+
topic = Serializer.decode(payload).topic;
|
|
143
|
+
} catch {
|
|
144
|
+
connection.sendError('PROTOCOL_ERROR', 'Invalid unsubscribe payload', messageId);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
this.pubSubBroker.unsubscribe(topic, connection);
|
|
149
|
+
const ackPayload = Serializer.encode({ topic });
|
|
150
|
+
connection.send(RESPONSE, 0, messageId, ackPayload);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
_handlePublish(payload, connection) {
|
|
154
|
+
let topic, data;
|
|
155
|
+
try {
|
|
156
|
+
const decoded = Serializer.decode(payload);
|
|
157
|
+
topic = decoded.topic;
|
|
158
|
+
data = decoded.data;
|
|
159
|
+
} catch {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
this.pubSubBroker.publish(topic, data, connection);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
publish(topic, data) {
|
|
167
|
+
this.pubSubBroker.publishToAll(topic, data);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
onDisconnect(connection) {
|
|
171
|
+
this.pubSubBroker.cleanupConnection(connection);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
getRouteCount() {
|
|
175
|
+
return this.routes.size;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
getSubscribers(topic) {
|
|
179
|
+
const subs = this.pubSubBroker.topics.get(topic);
|
|
180
|
+
return subs ? subs.size : 0;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
class PubSubBroker {
|
|
185
|
+
constructor() {
|
|
186
|
+
this.topics = new Map();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
subscribe(topic, connection) {
|
|
190
|
+
if (!this.topics.has(topic)) {
|
|
191
|
+
this.topics.set(topic, new Set());
|
|
192
|
+
}
|
|
193
|
+
this.topics.get(topic).add(connection);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
unsubscribe(topic, connection) {
|
|
197
|
+
const subs = this.topics.get(topic);
|
|
198
|
+
if (subs) {
|
|
199
|
+
subs.delete(connection);
|
|
200
|
+
if (subs.size === 0) {
|
|
201
|
+
this.topics.delete(topic);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
publish(topic, data, excludeConnection) {
|
|
207
|
+
const subs = this.topics.get(topic);
|
|
208
|
+
if (!subs || subs.size === 0) return;
|
|
209
|
+
|
|
210
|
+
let payload;
|
|
211
|
+
try {
|
|
212
|
+
payload = Serializer.encode({ topic, data });
|
|
213
|
+
} catch {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
for (const conn of subs) {
|
|
218
|
+
if (conn !== excludeConnection && conn.isActive()) {
|
|
219
|
+
conn.send(PUBLISH, 0, 0, payload);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
publishToAll(topic, data) {
|
|
225
|
+
const subs = this.topics.get(topic);
|
|
226
|
+
if (!subs || subs.size === 0) return;
|
|
227
|
+
|
|
228
|
+
let payload;
|
|
229
|
+
try {
|
|
230
|
+
payload = Serializer.encode({ topic, data });
|
|
231
|
+
} catch {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
for (const conn of subs) {
|
|
236
|
+
if (conn.isActive()) {
|
|
237
|
+
conn.send(PUBLISH, 0, 0, payload);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
cleanupConnection(connection) {
|
|
243
|
+
for (const [topic, subs] of this.topics) {
|
|
244
|
+
subs.delete(connection);
|
|
245
|
+
if (subs.size === 0) {
|
|
246
|
+
this.topics.delete(topic);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
module.exports = Router;
|
package/src/Server.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
const net = require('net');
|
|
2
|
+
const Connection = require('./Connection');
|
|
3
|
+
const Router = require('./Router');
|
|
4
|
+
|
|
5
|
+
class Server {
|
|
6
|
+
constructor(config = {}) {
|
|
7
|
+
this.config = {
|
|
8
|
+
port: 4000,
|
|
9
|
+
host: '0.0.0.0',
|
|
10
|
+
maxConnections: 10000,
|
|
11
|
+
...config,
|
|
12
|
+
};
|
|
13
|
+
this.router = new Router();
|
|
14
|
+
this.connections = new Set();
|
|
15
|
+
this.tcp = null;
|
|
16
|
+
this._listening = false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
on(route, handler, schema = null) {
|
|
20
|
+
this.router.register(route, handler, schema);
|
|
21
|
+
return this;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
use(middleware) {
|
|
25
|
+
this.router.addMiddleware(middleware);
|
|
26
|
+
return this;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
publish(topic, data) {
|
|
30
|
+
this.router.publish(topic, data);
|
|
31
|
+
return this;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
listen(port = this.config.port) {
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
if (this._listening) {
|
|
37
|
+
return reject(new Error('Server is already listening'));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
this.tcp = net.createServer((socket) => {
|
|
41
|
+
if (this.connections.size >= this.config.maxConnections) {
|
|
42
|
+
socket.destroy();
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
socket.setKeepAlive(true, 60000);
|
|
47
|
+
socket.setNoDelay(true);
|
|
48
|
+
|
|
49
|
+
const conn = new Connection(socket, this.router, {
|
|
50
|
+
auth: this.config.auth,
|
|
51
|
+
});
|
|
52
|
+
this.connections.add(conn);
|
|
53
|
+
socket.on('close', () => this.connections.delete(conn));
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
this.tcp.on('error', (err) => {
|
|
57
|
+
if (err.code === 'EADDRINUSE') {
|
|
58
|
+
reject(new Error(`Port ${port} is already in use`));
|
|
59
|
+
} else {
|
|
60
|
+
console.error('[AfterLink] Server error:', err.message);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
this.tcp.listen(port, this.config.host, () => {
|
|
65
|
+
this._listening = true;
|
|
66
|
+
console.log(`[AfterLink] Server listening on ${this.config.host}:${port}`);
|
|
67
|
+
resolve(this);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
close() {
|
|
73
|
+
return new Promise((resolve) => {
|
|
74
|
+
if (!this.tcp || !this._listening) {
|
|
75
|
+
return resolve();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
this._listening = false;
|
|
79
|
+
|
|
80
|
+
for (const conn of this.connections) {
|
|
81
|
+
conn.destroy();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
this.tcp.close(() => resolve());
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
getConnectionCount() {
|
|
89
|
+
return this.connections.size;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
getRouteCount() {
|
|
93
|
+
return this.router.getRouteCount();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
isListening() {
|
|
97
|
+
return this._listening;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
module.exports = Server;
|
package/src/index.js
ADDED