ferrings 0.1.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.
@@ -0,0 +1,340 @@
1
+ 'use strict';
2
+
3
+ const { EventEmitter } = require('node:events');
4
+
5
+ function createTcpTransportExports(UringTcpServer) {
6
+ class IoUringTcpConnection extends EventEmitter {
7
+ constructor(server, event) {
8
+ super();
9
+ this.id = event.connectionId;
10
+ this.remoteAddress = event.remoteAddress || event.remoteAddr;
11
+ this.remoteFamily = event.remoteFamily;
12
+ this.remotePort = event.remotePort;
13
+ this.destroyed = false;
14
+ this._server = server;
15
+ }
16
+
17
+ write(data) {
18
+ if (this.destroyed) return false;
19
+ return this._server._send(this.id, toBuffer(data));
20
+ }
21
+
22
+ end(data) {
23
+ if (this.destroyed) return false;
24
+ if (data === undefined || data === null) {
25
+ const accepted = this._server._closeConnection(this.id);
26
+ if (accepted) this.destroyed = true;
27
+ return accepted;
28
+ }
29
+ const accepted = this._server._sendAndClose(this.id, toBuffer(data));
30
+ if (accepted) this.destroyed = true;
31
+ return accepted;
32
+ }
33
+
34
+ destroy() {
35
+ if (this.destroyed) return false;
36
+ const accepted = this._server._closeConnection(this.id);
37
+ if (accepted) this.destroyed = true;
38
+ return accepted;
39
+ }
40
+ }
41
+
42
+ class IoUringTcpTransportServer extends EventEmitter {
43
+ constructor(options, connectionListener) {
44
+ super();
45
+ if (typeof options === 'function') {
46
+ connectionListener = options;
47
+ options = undefined;
48
+ }
49
+ this._baseOptions = options ? { ...options } : {};
50
+ this._native = null;
51
+ this._connections = new Map();
52
+ this._info = null;
53
+ this._keepAlive = null;
54
+ this._keepAliveRefed = true;
55
+ this._closed = true;
56
+ if (connectionListener) {
57
+ this.on('connection', connectionListener);
58
+ }
59
+ }
60
+
61
+ start(...args) {
62
+ if (this._info) {
63
+ throw new Error('server is already running');
64
+ }
65
+ const { options, callback } = parseListenArgs(this._baseOptions, args);
66
+ this._native = new UringTcpServer(options);
67
+ const info = this._native.startBatch((events) => {
68
+ for (const event of events) {
69
+ this._handleEvent(event);
70
+ }
71
+ });
72
+ this._info = info;
73
+ this._closed = false;
74
+ this._ensureKeepAlive();
75
+ if (callback) callback(info);
76
+ this.emit('listening', info);
77
+ return info;
78
+ }
79
+
80
+ listen(...args) {
81
+ this.start(...args);
82
+ return this;
83
+ }
84
+
85
+ close(callback) {
86
+ if (this._closed) {
87
+ if (callback) callback();
88
+ return this;
89
+ }
90
+ this._closed = true;
91
+ for (const connection of this._connections.values()) {
92
+ connection.destroyed = true;
93
+ connection.emit('close');
94
+ }
95
+ this._connections.clear();
96
+ if (this._native) {
97
+ this._native.stop();
98
+ }
99
+ this._native = null;
100
+ this._clearKeepAlive();
101
+ this._info = null;
102
+ if (callback) callback();
103
+ this.emit('close');
104
+ return this;
105
+ }
106
+
107
+ stop() {
108
+ this.close();
109
+ }
110
+
111
+ info() {
112
+ return this._native ? this._native.info() : null;
113
+ }
114
+
115
+ address() {
116
+ const info = this.info() || this._info;
117
+ if (!info) return null;
118
+ return {
119
+ address: info.host,
120
+ family: info.host.includes(':') ? 'IPv6' : 'IPv4',
121
+ port: info.port
122
+ };
123
+ }
124
+
125
+ connections() {
126
+ return [...this._connections.values()];
127
+ }
128
+
129
+ getConnections(callback) {
130
+ if (typeof callback !== 'function') {
131
+ throw new TypeError('callback must be a function');
132
+ }
133
+ process.nextTick(() => {
134
+ callback(null, this._connectionCount());
135
+ });
136
+ return this;
137
+ }
138
+
139
+ sendBatch(sends) {
140
+ const batch = normalizeBatchSends(sends);
141
+ return this._native ? this._native.sendBatch(batch.sends) : false;
142
+ }
143
+
144
+ sendBatchAndClose(sends) {
145
+ const batch = normalizeBatchSends(sends);
146
+ if (!this._native) return false;
147
+ const accepted = this._native.sendBatchAndClose(batch.sends);
148
+ if (accepted) {
149
+ for (const connection of batch.connections) {
150
+ connection.destroyed = true;
151
+ }
152
+ }
153
+ return accepted;
154
+ }
155
+
156
+ ref() {
157
+ this._keepAliveRefed = true;
158
+ if (this._keepAlive && typeof this._keepAlive.ref === 'function') {
159
+ this._keepAlive.ref();
160
+ }
161
+ return this;
162
+ }
163
+
164
+ unref() {
165
+ this._keepAliveRefed = false;
166
+ if (this._keepAlive && typeof this._keepAlive.unref === 'function') {
167
+ this._keepAlive.unref();
168
+ }
169
+ return this;
170
+ }
171
+
172
+ _handleEvent(event) {
173
+ if (event.eventType === 'connect') {
174
+ const connection = new IoUringTcpConnection(this, event);
175
+ this._connections.set(connection.id, connection);
176
+ this.emit('connection', connection);
177
+ return;
178
+ }
179
+
180
+ const connection = this._connectionFor(event);
181
+ if (!connection) return;
182
+
183
+ if (event.eventType === 'data') {
184
+ connection.emit('data', event.data);
185
+ this.emit('data', connection, event.data);
186
+ } else if (event.eventType === 'close') {
187
+ this._connections.delete(connection.id);
188
+ connection.destroyed = true;
189
+ connection.emit('close');
190
+ this.emit('connectionClose', connection);
191
+ }
192
+ }
193
+
194
+ _connectionFor(event) {
195
+ let connection = this._connections.get(event.connectionId);
196
+ if (!connection && event.eventType === 'data') {
197
+ connection = new IoUringTcpConnection(this, event);
198
+ this._connections.set(connection.id, connection);
199
+ this.emit('connection', connection);
200
+ }
201
+ return connection;
202
+ }
203
+
204
+ _send(connectionId, data) {
205
+ return this._native ? this._native.send(connectionId, data) : false;
206
+ }
207
+
208
+ _sendAndClose(connectionId, data) {
209
+ return this._native ? this._native.sendAndClose(connectionId, data) : false;
210
+ }
211
+
212
+ _closeConnection(connectionId) {
213
+ return this._native ? this._native.closeConnection(connectionId) : false;
214
+ }
215
+
216
+ _connectionCount() {
217
+ const info = this.info();
218
+ return info ? info.activeConnections : this._connections.size;
219
+ }
220
+
221
+ _ensureKeepAlive() {
222
+ if (this._keepAlive) return;
223
+ this._keepAlive = setInterval(() => {}, 1 << 30);
224
+ if (!this._keepAliveRefed && typeof this._keepAlive.unref === 'function') {
225
+ this._keepAlive.unref();
226
+ }
227
+ }
228
+
229
+ _clearKeepAlive() {
230
+ if (!this._keepAlive) return;
231
+ clearInterval(this._keepAlive);
232
+ this._keepAlive = null;
233
+ }
234
+ }
235
+
236
+ return {
237
+ IoUringTcpConnection,
238
+ IoUringTcpTransportServer,
239
+ createTcpServer(options, connectionListener) {
240
+ return new IoUringTcpTransportServer(options, connectionListener);
241
+ }
242
+ };
243
+ }
244
+
245
+ function parseListenArgs(baseOptions, args) {
246
+ const options = { ...baseOptions };
247
+ let callback;
248
+ const values = [...args];
249
+ if (typeof values[values.length - 1] === 'function') {
250
+ callback = values.pop();
251
+ }
252
+
253
+ if (values.length === 1 && isPlainObject(values[0])) {
254
+ Object.assign(options, values[0]);
255
+ return { options, callback };
256
+ }
257
+
258
+ if (values.length > 0 && values[0] !== undefined && values[0] !== null) {
259
+ options.port = normalizePort(values[0]);
260
+ }
261
+ if (values.length > 1 && values[1] !== undefined && values[1] !== null) {
262
+ if (typeof values[1] === 'string' && !isNumericString(values[1])) {
263
+ options.host = values[1];
264
+ } else if (values.length === 2) {
265
+ options.backlog = normalizeBacklog(values[1]);
266
+ }
267
+ }
268
+ if (values.length > 2 && values[2] !== undefined && values[2] !== null) {
269
+ options.backlog = normalizeBacklog(values[2]);
270
+ }
271
+
272
+ return { options, callback };
273
+ }
274
+
275
+ function isPlainObject(value) {
276
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
277
+ }
278
+
279
+ function normalizePort(value) {
280
+ if (typeof value === 'string' && value.trim() !== '') {
281
+ return Number(value);
282
+ }
283
+ return value;
284
+ }
285
+
286
+ function normalizeBacklog(value) {
287
+ if (typeof value === 'string' && value.trim() !== '') {
288
+ return Number(value);
289
+ }
290
+ return value;
291
+ }
292
+
293
+ function isNumericString(value) {
294
+ return value.trim() !== '' && Number.isFinite(Number(value));
295
+ }
296
+
297
+ function normalizeBatchSends(sends) {
298
+ if (!Array.isArray(sends)) {
299
+ throw new TypeError('sends must be an array');
300
+ }
301
+
302
+ const connections = [];
303
+ const normalized = sends.map((send) => {
304
+ if (!isPlainObject(send)) {
305
+ throw new TypeError('each send must be an object');
306
+ }
307
+
308
+ const connection = send.connection;
309
+ const connectionId =
310
+ send.connectionId !== undefined && send.connectionId !== null
311
+ ? send.connectionId
312
+ : connection && connection.id;
313
+ if (!Number.isInteger(connectionId) || connectionId < 0 || connectionId > 0xffffffff) {
314
+ throw new RangeError('send connectionId must be a uint32');
315
+ }
316
+ if (connection && typeof connection === 'object') {
317
+ connections.push(connection);
318
+ }
319
+ return {
320
+ connectionId,
321
+ data: toBuffer(send.data)
322
+ };
323
+ });
324
+
325
+ return {
326
+ sends: normalized,
327
+ connections
328
+ };
329
+ }
330
+
331
+ function toBuffer(data) {
332
+ if (Buffer.isBuffer(data)) return data;
333
+ if (typeof data === 'string') return Buffer.from(data);
334
+ if (data instanceof Uint8Array) {
335
+ return Buffer.from(data.buffer, data.byteOffset, data.byteLength);
336
+ }
337
+ throw new TypeError('data must be a Buffer, string, or Uint8Array');
338
+ }
339
+
340
+ module.exports = createTcpTransportExports;