@koeroesi86/node-worker-express 20.10.7-db6bdc3 → 22.3.12-fd2e588

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/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Node Worker Express [![Build status](https://travis-ci.org/Koeroesi86/node-worker-express.svg?branch=master)](https://travis-ci.org/Koeroesi86/node-worker-express)
1
+ # Node Worker Express [![Build Status](https://app.travis-ci.com/Koeroesi86/node-worker-express.svg?branch=master)](https://app.travis-ci.com/Koeroesi86/node-worker-express)
2
2
 
3
3
  Express JS library for [@koeroesi86/node-worker](https://www.npmjs.com/package/@koeroesi86/node-worker)
4
4
 
@@ -2,7 +2,9 @@ const WORKER_EVENT = {
2
2
  REQUEST: 'WORKER_REQUEST',
3
3
  REQUEST_ACKNOWLEDGE: 'WORKER_REQUEST_ACK',
4
4
  RESPONSE: 'WORKER_RESPONSE',
5
+ RESPONSE_EMIT: 'WORKER_RESPONSE_EMIT',
5
6
  RESPONSE_ACKNOWLEDGE: 'WORKER_RESPONSE_ACK',
7
+ WS_MESSAGE_RECEIVE: 'WS_MESSAGE_RECEIVE',
6
8
  WS_MESSAGE_SEND: 'WS_MESSAGE_SEND',
7
9
  WS_CONNECTION_CLOSE: 'WS_CONNECTION_CLOSE',
8
10
  WS_CONNECTION_CLOSE_ACKNOWLEDGE: 'WS_CONNECTION_CLOSE_ACK',
@@ -9,7 +9,7 @@ const createDigest = key => createHash('sha1')
9
9
  .digest('base64');
10
10
 
11
11
  module.exports = (event, callback) => {
12
- if (event.protocol === 'WS') {
12
+ if (event.protocol === 'WS' && event.frame === undefined) {
13
13
  const key = (event.headers['sec-websocket-key'] || '').trim();
14
14
  const digest = createDigest(key);
15
15
 
@@ -32,15 +32,22 @@ module.exports = (event, callback) => {
32
32
  });
33
33
 
34
34
  timers[key] = setInterval(() => {
35
+ const frame = JSON.stringify({ now: Date.now() })
36
+ console.log('outgoing message:', frame);
35
37
  callback({
36
38
  sendWsMessage: true,
37
- frame: JSON.stringify({ now: Date.now() }),
39
+ frame,
38
40
  });
39
41
  }, 1000);
40
42
  return;
41
43
  }
42
44
  }
43
45
 
46
+ if (event.protocol === 'WS' && event.frame !== undefined) {
47
+ console.log('incoming message:', event.frame);
48
+ return;
49
+ }
50
+
44
51
  callback({
45
52
  statusCode: 400,
46
53
  headers: {
@@ -2,9 +2,14 @@ const path = require('path');
2
2
  const fs = require('fs');
3
3
  const crypto = require('crypto');
4
4
  const { execSync } = require('child_process');
5
+ const { Writable } = require('stream');
5
6
 
6
7
  let timer;
7
8
 
9
+ const features = {
10
+ enableEmit: false,
11
+ };
12
+
8
13
  function debounce(fn = () => {}, timeout = 0) {
9
14
  if (timer) clearTimeout(timer);
10
15
  timer = setTimeout(() => {
@@ -133,8 +138,7 @@ function getCharset(body, fileName) {
133
138
  return 'iso-8859-1';
134
139
 
135
140
  try {
136
- const out = execSync(`file -b --mime-encoding ${fileName}`).toString('utf8').trim();
137
- return out;
141
+ return execSync(`file -b --mime-encoding ${fileName}`).toString('utf8').trim();
138
142
  } catch (e) {
139
143
  //
140
144
  }
@@ -169,19 +173,54 @@ const staticWorker = (event, callback = () => {}) => {
169
173
  isModified = false;
170
174
  }
171
175
  }
172
-
173
- callback({
176
+ const response = {
174
177
  statusCode: isModified ? 200 : 304,
175
178
  headers: {
176
179
  'Content-Type': `${contentType}${charset ? `; charset=${charset}` : ''}`,
177
180
  'Cache-Control': 'public, max-age=0',
178
- 'Content-Length': bodyBuffer.length,
181
+ 'Content-Length': bodyBuffer.byteLength,
179
182
  'ETag': currentEtag,
180
183
  ...(stats.mtime && { 'Last-Modified': lastModified.toUTCString() }),
181
184
  },
182
- body: bodyBuffer.toString('base64'),
185
+ body: isModified ? bodyBuffer.toString('base64') : '',
183
186
  isBase64Encoded: true,
184
- });
187
+ };
188
+
189
+ if (features.enableEmit && isModified) {
190
+ const writer = new Writable({
191
+ /**
192
+ * @param {Buffer} chunk
193
+ * @param {string} encoding
194
+ * @param {Function} next
195
+ */
196
+ write(chunk, encoding, next) {
197
+ response.emit = true;
198
+ response.isBase64Encoded = true;
199
+ response.body = chunk.toString('base64');
200
+ // console.log('sending', chunk.length, 'bytes')
201
+ callback(response);
202
+ next();
203
+ },
204
+ destroy() {
205
+ response.isBase64Encoded = false;
206
+ response.emit = true;
207
+ response.body = null;
208
+ callback(response);
209
+ },
210
+ });
211
+
212
+ fs.createReadStream(fileName, {
213
+ // encoding: 'binary',
214
+ // encoding: 'utf-8',
215
+ emitClose: false,
216
+ autoClose: false,
217
+ // highWaterMark: 16,
218
+ start: 0,
219
+ end: Infinity,
220
+ }).pipe(writer);
221
+ } else {
222
+ callback(response);
223
+ }
185
224
  } else {
186
225
  callback({
187
226
  statusCode: 404,
package/index.d.ts CHANGED
@@ -25,6 +25,8 @@ declare namespace Middleware {
25
25
  remoteAddress: string;
26
26
  body: string;
27
27
  rootPath: string;
28
+ closed?: boolean;
29
+ frame?: string;
28
30
  }
29
31
 
30
32
  type WorkerInputEvent = {
@@ -43,6 +45,7 @@ declare namespace Middleware {
43
45
  statusCode: number;
44
46
  headers?: { [key: string]: string };
45
47
  isBase64Encoded?: boolean;
48
+ emit?: boolean;
46
49
  body?: string;
47
50
  }
48
51
 
@@ -51,7 +54,7 @@ declare namespace Middleware {
51
54
  frame: string;
52
55
  }
53
56
 
54
- function InvokableWorker(event: RequestEvent, callback: (e: ResponseEvent) => void): void;
57
+ type InvokableWorker = (event: RequestEvent, callback: (e: ResponseEvent) => void) => void;
55
58
  }
56
59
 
57
60
  declare module '@koeroesi86/node-worker-express' {
@@ -1,5 +1,14 @@
1
- const bodyParser = (config = { limitRequestBody: 1000000 }) => (request, response, next) => {
1
+ const ignoredMethods = ['get', 'delete', 'options', 'head'];
2
+
3
+ const bodyParser = (config = { limitRequestBody: 1000000, shouldError: true }) => (request, response, next) => {
2
4
  let body = '';
5
+
6
+ if (ignoredMethods.includes(request.method.toLowerCase())) {
7
+ request.body = body;
8
+ next();
9
+ return;
10
+ }
11
+
3
12
  request.on('data', data => {
4
13
  body += data;
5
14
 
@@ -7,12 +16,17 @@ const bodyParser = (config = { limitRequestBody: 1000000 }) => (request, respons
7
16
  // 1e6 === 1 * Math.pow(10, 6) === 1 * 1000000 ~~~ 1MB
8
17
  if (body.length > (config.limitRequestBody || 1000000)) {
9
18
  body = "";
10
- response.writeHead(413, { 'Content-Type': 'text/plain' });
11
- response.end();
12
- request.connection.destroy();
13
- // next();
19
+
20
+ if (config.shouldError) {
21
+ response.writeHead(413, { 'Content-Type': 'text/plain' });
22
+ response.end();
23
+ request.connection.destroy();
24
+ } else {
25
+ next();
26
+ }
14
27
  }
15
28
  });
29
+
16
30
  request.on('end', () => {
17
31
  request.body = body;
18
32
  next();
@@ -4,149 +4,22 @@ const fs = require('fs');
4
4
  const url = require('url');
5
5
  const { WORKER_EVENT } = require('../constants');
6
6
  const WorkerPool = require('../utils/workerPool');
7
- const bodyParser = require('./bodyParser');
7
+ const isWebSocket = require('../utils/isWebSocket');
8
+ const parseWsMessage = require('../utils/parseWsMessage');
9
+ const constructWsMessage = require('../utils/constructWsMessage');
10
+ const getClientIp = require('../utils/getClientIp');
11
+ const createBodyParser = require('./bodyParser');
8
12
 
9
- const FORBIDDEN_PATHS = [
13
+ const ForbiddenPaths = [
10
14
  '..'
11
15
  ];
12
16
 
13
- const PROTOCOLS = {
14
- HTTP: 'HTTP',
15
- WEBSOCKET: 'WS',
17
+ const Protocols = {
18
+ http: 'HTTP',
19
+ websocket: 'WS',
16
20
  };
17
21
 
18
- const isWebSocket = request => {
19
- if (request.method !== 'GET') return false;
20
-
21
- const connection = request.headers.connection || '';
22
- const upgrade = request.headers.upgrade || '';
23
-
24
- return request.method === 'GET' &&
25
- connection.toLowerCase().split(/ *, */).indexOf('upgrade') >= 0 &&
26
- upgrade.toLowerCase() === 'websocket';
27
- };
28
-
29
- /**
30
- * TODO: improve
31
- * @param {Buffer} data
32
- * @returns {string}
33
- */
34
- const parseWsMessage = data => {
35
- const dl = data[1] & 127;
36
- let ifm = 2;
37
- if (dl === 126) {
38
- ifm = 4;
39
- } else if (dl === 127) {
40
- ifm = 10;
41
- }
42
- let i = ifm + 4;
43
- const masks = data.slice(ifm, i);
44
- let index = 0;
45
- let output = "";
46
- const l = data.length;
47
- while (i < l) {
48
- output += String.fromCharCode(data[i++] ^ masks[index++ % 4]);
49
- }
50
- return output;
51
- };
52
-
53
- /**
54
- * TODO: improve
55
- * @param {String} text
56
- * @returns {Buffer}
57
- */
58
- const constructWsMessage = text => {
59
- const jsonByteLength = Buffer.byteLength(text);
60
- // Note: we're not supporting > 65535 byte payloads at this stage
61
- const lengthByteCount = jsonByteLength < 126 ? 0 : 2;
62
- const payloadLength = lengthByteCount === 0 ? jsonByteLength : 126;
63
- const buffer = Buffer.alloc(2 + lengthByteCount + jsonByteLength);
64
- // Write out the first byte, using opcode `1` to indicate that the message
65
- // payload contains text data
66
- buffer.writeUInt8(0b10000001, 0);
67
- buffer.writeUInt8(payloadLength, 1);
68
- // Write the length of the JSON payload to the second byte
69
- let payloadOffset = 2;
70
- if (lengthByteCount > 0) {
71
- buffer.writeUInt16BE(jsonByteLength, 2);
72
- payloadOffset += lengthByteCount;
73
- }
74
- // Write the JSON data to the data buffer
75
- buffer.write(text, payloadOffset);
76
- return buffer;
77
- };
78
-
79
- /**
80
- * @param {module:http~IncomingMessage} request
81
- * @returns {string}
82
- */
83
- const getClientIp = request => {
84
- if (request.headers['x-client-ip']) {
85
- return request.headers['x-client-ip'];
86
- }
87
-
88
- if (request.headers['x-forwarded-for']) {
89
- return request.headers['x-forwarded-for'];
90
- }
91
-
92
- if (request.headers['cf-connecting-ip']) {
93
- return request.headers['cf-connecting-ip'];
94
- }
95
-
96
- if (request.headers['fastly-client-ip']) {
97
- return request.headers['fastly-client-ip'];
98
- }
99
-
100
- if (request.headers['true-client-ip']) {
101
- return request.headers['true-client-ip'];
102
- }
103
-
104
- if (request.headers['x-real-ip']) {
105
- return request.headers['x-real-ip'];
106
- }
107
-
108
- if (request.headers['x-cluster-client-ip']) {
109
- return request.headers['x-cluster-client-ip'];
110
- }
111
-
112
- if (request.headers['x-forwarded']) {
113
- return request.headers['x-forwarded'];
114
- }
115
-
116
- if (request.headers['forwarded-for']) {
117
- return request.headers['forwarded-for'];
118
- }
119
-
120
- if (request.headers.forwarded) {
121
- return request.headers.forwarded;
122
- }
123
-
124
- if (request.connection) {
125
- if (request.connection.remoteAddress) {
126
- return request.connection.remoteAddress;
127
- }
128
-
129
- if (request.connection.socket && request.connection.socket.remoteAddress) {
130
- return request.connection.socket.remoteAddress;
131
- }
132
- }
133
-
134
- if (request.socket && request.socket.remoteAddress) {
135
- return request.socket.remoteAddress;
136
- }
137
-
138
- if (request.info && request.info.remoteAddress) {
139
- return request.info.remoteAddress;
140
- }
141
-
142
- if (request.requestContext && request.requestContext.identity && request.requestContext.identity.sourceIp) {
143
- return request.requestContext.identity.sourceIp;
144
- }
145
-
146
- return '';
147
- };
148
-
149
- const DEFAULT_OPTIONS = {
22
+ const DefaultOptions = {
150
23
  root: null,
151
24
  limit: 0,
152
25
  limitPerPath: 0,
@@ -159,6 +32,11 @@ const DEFAULT_OPTIONS = {
159
32
  },
160
33
  onExit: () => {
161
34
  },
35
+ onForbiddenPath: (request, response) => {
36
+ response.writeHead(500, { 'Content-Type': 'text/plain' });
37
+ response.end();
38
+ request.connection.destroy();
39
+ },
162
40
  index: [],
163
41
  env: {},
164
42
  staticWorker: path.resolve(__dirname, '../examples/staticWorker.js'),
@@ -172,7 +50,7 @@ const DEFAULT_OPTIONS = {
172
50
  */
173
51
  const workerMiddleware = (options) => {
174
52
  const config = {
175
- ...DEFAULT_OPTIONS,
53
+ ...DefaultOptions,
176
54
  ...(options && options),
177
55
  };
178
56
  if (!config.root) {
@@ -184,39 +62,51 @@ const workerMiddleware = (options) => {
184
62
  onExit: config.onExit,
185
63
  idleCheckTimeout: config.idleCheckTimeout,
186
64
  });
65
+ const bodyParser = createBodyParser({ limitRequestBody: config.limitRequestBody });
66
+ const aliasCache = {};
67
+ const workerCache = {};
187
68
 
188
- return (request, response, next) => {
69
+ return async (request, response, next) => {
189
70
  const {
190
71
  query: queryStringParameters,
191
72
  pathname
192
73
  } = url.parse(request.url, true);
193
-
194
- Promise.resolve()
195
- .then(() => Promise.race([
196
- new Promise(r => setTimeout(r, config.limitRequestTimeout)),
197
- new Promise(r => bodyParser({ limitRequestBody: config.limitRequestBody })(request, response, r)),
198
- ]))
199
- .then(() => {
200
- let isIndex = false;
201
- const pathFragments = pathname.split(/\//gi).filter(Boolean);
202
- let currentPathFragments = pathFragments.slice();
203
-
204
- if (currentPathFragments.find(p => FORBIDDEN_PATHS.includes(p))) {
205
- response.writeHead(500, { 'Content-Type': 'text/plain' });
206
- response.end();
207
- request.connection.destroy();
208
- return next();
74
+ request.pathFragments = pathname.split(/\//gi).filter(Boolean);
75
+
76
+ try {
77
+ let isWorker = false;
78
+ const pathFragments = pathname.split(/\//gi).filter(Boolean);
79
+ let currentPathFragments = pathFragments.slice(0);
80
+ let pathExists = false;
81
+
82
+ await new Promise((resolve, reject) => {
83
+ if (request.pathFragments.find(p => ForbiddenPaths.includes(p))) {
84
+ config.onForbiddenPath(request, response);
85
+ reject();
209
86
  }
87
+ resolve();
88
+ });
210
89
 
211
- let pathExists = false;
212
- let indexPath = path.join(rootPath, ...currentPathFragments);
213
-
214
- for (let i = currentPathFragments.length; i >= 0; i--) {
90
+ await Promise.race([
91
+ new Promise((res, rej) => setTimeout(rej, config.limitRequestTimeout)),
92
+ new Promise(res => bodyParser(request, response, res)),
93
+ ]);
94
+ let indexPath;
95
+
96
+ if (aliasCache[pathname] && fs.existsSync(aliasCache[pathname])) {
97
+ isWorker = true;
98
+ indexPath = aliasCache[pathname];
99
+ } else if (workerCache[pathname] && fs.existsSync(workerCache[pathname])) {
100
+ isWorker = true;
101
+ indexPath = workerCache[pathname];
102
+ } else {
103
+ indexPath = path.join(rootPath, ...currentPathFragments)
104
+
105
+ for (let i = currentPathFragments.length; i >= 0 && !pathExists; i--) {
215
106
  if (pathExists) continue;
216
107
  currentPathFragments.splice(i);
217
108
  const currentPath = path.join(rootPath, ...currentPathFragments);
218
109
  pathExists = fs.existsSync(currentPath);
219
-
220
110
  if (!pathExists) continue;
221
111
  const currentStats = fs.statSync(currentPath);
222
112
 
@@ -225,138 +115,173 @@ const workerMiddleware = (options) => {
225
115
  pathExists = !!config.index.find(indexFile => {
226
116
  const checkIndexFilePath = path.join(currentPath, indexFile);
227
117
  if (fs.existsSync(checkIndexFilePath)) {
228
- isIndex = true;
118
+ isWorker = true;
229
119
  indexPath = checkIndexFilePath;
120
+ aliasCache[pathname] = checkIndexFilePath;
121
+ setTimeout(() => {
122
+ aliasCache[pathname] = null;
123
+ delete aliasCache[pathname];
124
+ }, 5000);
230
125
  return true;
231
126
  }
232
127
  });
233
128
  if (pathExists) break;
234
129
  } else if (pathExists && currentStats.isFile()) {
235
130
  if (config.index.includes(currentPathFragments[currentPathFragments.length - 1])) {
236
- isIndex = true;
131
+ isWorker = true;
132
+ workerCache[pathname] = currentPath;
133
+ setTimeout(() => {
134
+ workerCache[pathname] = null;
135
+ delete workerCache[pathname];
136
+ }, 5000);
237
137
  }
238
138
  }
239
139
  }
140
+ }
141
+
142
+ /** @type {RequestEvent} */
143
+ const event = {
144
+ httpMethod: request.method.toUpperCase(),
145
+ protocol: isWebSocket(request) ? Protocols.websocket : Protocols.http,
146
+ path: pathname,
147
+ pathFragments: pathFragments,
148
+ queryStringParameters: JSON.parse(JSON.stringify(queryStringParameters)),
149
+ headers: request.headers,
150
+ remoteAddress: getClientIp(request),
151
+ body: `${request.body}`,
152
+ rootPath: rootPath,
153
+ };
154
+
155
+ const worker = isWorker ? await workerPool.getWorker(
156
+ indexPath,
157
+ {
158
+ stdio: [ 'pipe', 'pipe', 'pipe', 'ipc' ],
159
+ env: { ...process.env, ...config.env },
160
+ cwd: config.cwd,
161
+ },
162
+ typeof config.limitPerPath === "function" ? config.limitPerPath(indexPath) : config.limitPerPath
163
+ ) : await workerPool.getWorker(
164
+ config.staticWorker,
165
+ {
166
+ cwd: process.cwd(),
167
+ stdio: [ 'pipe', 'pipe', 'pipe', 'ipc' ],
168
+ },
169
+ 1
170
+ );
171
+
172
+ const requestId = uuid();
173
+
174
+ let firstReceived = false;
175
+ const requestSocketListener = data => {
176
+ const frame = parseWsMessage(data);
177
+
178
+ // TODO: filter open frame better
179
+ if (data.length === 8 && !firstReceived) {
180
+ firstReceived = true;
181
+ return;
182
+ }
240
183
 
241
- /** @type {RequestEvent} */
242
- const event = {
243
- httpMethod: request.method.toUpperCase(),
244
- protocol: isWebSocket(request) ? PROTOCOLS.WEBSOCKET : PROTOCOLS.HTTP,
245
- path: pathname,
246
- pathFragments: pathFragments,
247
- queryStringParameters: JSON.parse(JSON.stringify(queryStringParameters)),
248
- headers: request.headers,
249
- remoteAddress: getClientIp(request),
250
- body: `${request.body}`,
251
- rootPath: rootPath,
252
- };
253
-
254
- Promise.resolve()
255
- .then(() => isIndex
256
- ? workerPool.getWorker(
257
- indexPath,
258
- {
259
- stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
260
- env: { PATH: process.env.PATH, ...config.env },
261
- cwd: config.cwd,
262
- },
263
- typeof config.limitPerPath === "function" ? config.limitPerPath(indexPath) : config.limitPerPath
264
- )
265
- : workerPool.getWorker(
266
- config.staticWorker,
267
- {
268
- cwd: process.cwd(),
269
- stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
270
- },
271
- 1
272
- )
273
- )
274
- .then(worker => {
275
- // worker.busy = true;
276
- const requestId = uuid();
277
-
278
- const requestSocketListener = data => {
279
- console.info(`[${requestId}] [ws data in] ${parseWsMessage(data)}`);
280
- };
281
- if (event.protocol === PROTOCOLS.WEBSOCKET) {
282
- request.socket.on('data', requestSocketListener);
283
- }
284
-
285
- worker.instance.stdout.off('data', config.onStdout);
286
- worker.instance.stdout.on('data', config.onStdout);
287
-
288
- worker.instance.stderr.off('data', config.onStderr);
289
- worker.instance.stderr.on('data', config.onStderr);
184
+ firstReceived = true;
290
185
 
291
- const messageListener = responseEvent => {
292
- if (responseEvent.requestId === requestId) {
293
- if (responseEvent.type === WORKER_EVENT.RESPONSE) {
294
- worker.postMessage({
295
- type: WORKER_EVENT.RESPONSE_ACKNOWLEDGE,
296
- requestId,
297
- });
298
- /** @type {ResponseEvent|WSFrameEvent} event */
299
- const { event } = responseEvent;
300
- const bufferEncoding = event.isBase64Encoded ? 'base64' : 'utf8';
186
+ worker.postMessage({
187
+ type: WORKER_EVENT.WS_MESSAGE_RECEIVE,
188
+ requestId,
189
+ event: { ...event, frame }
190
+ });
191
+ };
192
+ if (event.protocol === Protocols.websocket) {
193
+ request.socket.on('data', requestSocketListener);
194
+ }
301
195
 
302
- response.writeHead(event.statusCode, event.headers);
303
- response.write(Buffer.from(event.body, bufferEncoding));
304
- response.end();
305
- }
196
+ worker.instance.stdout.off('data', config.onStdout);
197
+ worker.instance.stdout.on('data', config.onStdout);
306
198
 
307
- if (responseEvent.type === WORKER_EVENT.WS_MESSAGE_SEND) {
308
- console.info(`[${requestId}] [ws data out] ${responseEvent.event.frame}`);
309
- request.socket.write(constructWsMessage(responseEvent.event.frame));
310
- }
199
+ worker.instance.stderr.off('data', config.onStderr);
200
+ worker.instance.stderr.on('data', config.onStderr);
311
201
 
312
- if (responseEvent.type === WORKER_EVENT.REQUEST_ACKNOWLEDGE) {
313
- worker.busy = false;
314
- }
315
- }
316
- };
202
+ const messageListener = responseEvent => {
203
+ if (responseEvent.requestId === requestId) {
204
+ if (responseEvent.type === WORKER_EVENT.RESPONSE) {
205
+ worker.postMessage({
206
+ type: WORKER_EVENT.RESPONSE_ACKNOWLEDGE,
207
+ requestId,
208
+ });
209
+ /** @type {ResponseEvent|WSFrameEvent} event */
210
+ const { event } = responseEvent;
211
+ const bufferEncoding = event.isBase64Encoded ? 'base64' : 'utf8';
317
212
 
318
- const requestCloseListener = () => {
319
- worker.postMessage({
320
- type: WORKER_EVENT.WS_CONNECTION_CLOSE,
321
- requestId,
322
- event
323
- });
324
- if (request.socket && request.socket.off) request.socket.off('close', requestCloseListener);
325
- worker.removeEventListener('message', messageListener);
326
- };
327
- request.socket.on('close', requestCloseListener);
213
+ response.writeHead(event.statusCode, event.headers);
214
+ response.write(Buffer.from(event.body, bufferEncoding));
215
+ response.end();
216
+ }
328
217
 
329
- worker.addEventListener('message', messageListener);
218
+ if (responseEvent.type === WORKER_EVENT.RESPONSE_EMIT) {
330
219
  worker.postMessage({
331
- type: WORKER_EVENT.REQUEST,
220
+ type: WORKER_EVENT.RESPONSE_ACKNOWLEDGE,
332
221
  requestId,
333
- event,
334
222
  });
223
+ /** @type {ResponseEvent|WSFrameEvent} event */
224
+ const { event } = responseEvent;
225
+ const bufferEncoding = event.isBase64Encoded ? 'base64' : 'utf8';
335
226
 
336
- const cleanupConnection = () => {
337
- worker.removeEventListener('message', messageListener);
338
- if (request && request.off) {
339
- request.off('close', cleanupConnection);
340
- request.off('aborted', cleanupConnection);
341
- }
342
-
343
- if (request.socket && request.socket.off) {
344
- request.socket.off('data', requestSocketListener);
345
- request.socket.off('close', requestCloseListener);
346
- }
347
- };
348
- request.on('aborted', cleanupConnection);
349
- if (event.protocol === PROTOCOLS.HTTP) {
350
- request.on('close', cleanupConnection);
351
- response.on('finish', cleanupConnection)
227
+ if (!response.headersSent) {
228
+ response.writeHead(event.statusCode, event.headers);
352
229
  }
353
- });
354
- })
355
- .catch(e => {
356
- response.writeHead(500, { 'Content-Type': 'text/plain' });
357
- response.write(`${e}`);
358
- response.end();
230
+ if (event.body !== null) {
231
+ response.write(Buffer.from(event.body, bufferEncoding).toString());
232
+ } else {
233
+ response.end();
234
+ }
235
+ }
236
+
237
+ if (responseEvent.type === WORKER_EVENT.WS_MESSAGE_SEND) {
238
+ request.socket.write(constructWsMessage(responseEvent.event.frame));
239
+ }
240
+
241
+ if (responseEvent.type === WORKER_EVENT.REQUEST_ACKNOWLEDGE) {
242
+ worker.busy = false;
243
+ }
244
+ }
245
+ };
246
+
247
+ const requestCloseListener = () => {
248
+ worker.postMessage({
249
+ type: WORKER_EVENT.WS_CONNECTION_CLOSE,
250
+ requestId,
251
+ event
252
+ });
253
+ if (request.socket && request.socket.off) request.socket.off('close', requestCloseListener);
254
+ worker.removeEventListener('message', messageListener);
255
+ };
256
+ request.socket.on('close', requestCloseListener);
257
+
258
+ worker.addEventListener('message', messageListener);
259
+ worker.postMessage({
260
+ type: WORKER_EVENT.REQUEST,
261
+ requestId,
262
+ event,
359
263
  });
264
+
265
+ const cleanupConnection = () => {
266
+ worker.removeEventListener('message', messageListener);
267
+ if (request && request.off) {
268
+ request.off('close', cleanupConnection);
269
+ request.off('aborted', cleanupConnection);
270
+ }
271
+
272
+ if (request.socket && request.socket.off) {
273
+ request.socket.off('data', requestSocketListener);
274
+ request.socket.off('close', requestCloseListener);
275
+ }
276
+ };
277
+ request.on('aborted', cleanupConnection);
278
+ if (event.protocol === Protocols.http) {
279
+ request.on('close', cleanupConnection);
280
+ response.on('finish', cleanupConnection)
281
+ }
282
+ } catch (e) {
283
+ next(e);
284
+ }
360
285
  };
361
286
  };
362
287
 
@@ -15,7 +15,22 @@ function messageListener(message) {
15
15
  const callback = responseEvent => {
16
16
  /** @type {WorkerOutputEvent} e */
17
17
  const e = {
18
- type: responseEvent.sendWsMessage ? WORKER_EVENT.WS_MESSAGE_SEND : WORKER_EVENT.RESPONSE,
18
+ type: WORKER_EVENT.RESPONSE,
19
+ requestId: message.requestId,
20
+ event: responseEvent,
21
+ };
22
+ if (responseEvent.sendWsMessage) e.type = WORKER_EVENT.WS_MESSAGE_SEND;
23
+ if (responseEvent.emit) e.type = WORKER_EVENT.RESPONSE_EMIT;
24
+ process.send(e);
25
+ };
26
+ worker(message.event, callback);
27
+ }
28
+
29
+ if (message.type === WORKER_EVENT.WS_MESSAGE_RECEIVE) {
30
+ const callback = responseEvent => {
31
+ /** @type {WorkerOutputEvent} e */
32
+ const e = {
33
+ type: WORKER_EVENT.WS_MESSAGE_SEND,
19
34
  requestId: message.requestId,
20
35
  event: responseEvent,
21
36
  }
@@ -23,6 +38,7 @@ function messageListener(message) {
23
38
  };
24
39
  worker(message.event, callback);
25
40
  }
41
+
26
42
  if (message.type === WORKER_EVENT.WS_CONNECTION_CLOSE) {
27
43
  /** @type {ResponseEvent} responseEvent */
28
44
  worker({ ...message.event, closed: true }, responseEvent => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@koeroesi86/node-worker-express",
3
- "version": "v20.10.7-db6bdc3",
3
+ "version": "v22.03.12-fd2e588",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "author": {
@@ -19,12 +19,12 @@
19
19
  "license": "MIT",
20
20
  "dependencies": {
21
21
  "@koeroesi86/node-worker": "^1.0.3-6bc3bc7",
22
- "uuid": "^7.0.3"
22
+ "uuid": "^8.3.2"
23
23
  },
24
24
  "devDependencies": {
25
- "@types/express": "^4.17.6",
26
- "@types/node": "^13.11.1",
27
- "express": "^4.17.1",
28
- "nodemon": "^2.0.3"
25
+ "@types/express": "^4.17.13",
26
+ "@types/node": "^17.0.21",
27
+ "express": "^4.17.3",
28
+ "nodemon": "^2.0.15"
29
29
  }
30
30
  }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * TODO: improve
3
+ * @param {String} text
4
+ * @returns {Buffer}
5
+ */
6
+ const constructWsMessage = text => {
7
+ const jsonByteLength = Buffer.byteLength(text);
8
+ // Note: we're not supporting > 65535 byte payloads at this stage
9
+ const lengthByteCount = jsonByteLength < 126 ? 0 : 2;
10
+ const payloadLength = lengthByteCount === 0 ? jsonByteLength : 126;
11
+ const buffer = Buffer.alloc(2 + lengthByteCount + jsonByteLength);
12
+ // Write out the first byte, using opcode `1` to indicate that the message
13
+ // payload contains text data
14
+ buffer.writeUInt8(0b10000001, 0);
15
+ buffer.writeUInt8(payloadLength, 1);
16
+ // Write the length of the JSON payload to the second byte
17
+ let payloadOffset = 2;
18
+ if (lengthByteCount > 0) {
19
+ buffer.writeUInt16BE(jsonByteLength, 2);
20
+ payloadOffset += lengthByteCount;
21
+ }
22
+ // Write the JSON data to the data buffer
23
+ buffer.write(text, payloadOffset);
24
+ return buffer;
25
+ };
26
+
27
+ module.exports = constructWsMessage;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * @param {module:http~IncomingMessage} request
3
+ * @returns {string}
4
+ */
5
+ const getClientIp = request => {
6
+ if (request.headers['x-client-ip']) {
7
+ return request.headers['x-client-ip'];
8
+ }
9
+
10
+ if (request.headers['x-forwarded-for']) {
11
+ return request.headers['x-forwarded-for'];
12
+ }
13
+
14
+ if (request.headers['cf-connecting-ip']) {
15
+ return request.headers['cf-connecting-ip'];
16
+ }
17
+
18
+ if (request.headers['fastly-client-ip']) {
19
+ return request.headers['fastly-client-ip'];
20
+ }
21
+
22
+ if (request.headers['true-client-ip']) {
23
+ return request.headers['true-client-ip'];
24
+ }
25
+
26
+ if (request.headers['x-real-ip']) {
27
+ return request.headers['x-real-ip'];
28
+ }
29
+
30
+ if (request.headers['x-cluster-client-ip']) {
31
+ return request.headers['x-cluster-client-ip'];
32
+ }
33
+
34
+ if (request.headers['x-forwarded']) {
35
+ return request.headers['x-forwarded'];
36
+ }
37
+
38
+ if (request.headers['forwarded-for']) {
39
+ return request.headers['forwarded-for'];
40
+ }
41
+
42
+ if (request.headers.forwarded) {
43
+ return request.headers.forwarded;
44
+ }
45
+
46
+ if (request.connection) {
47
+ if (request.connection.remoteAddress) {
48
+ return request.connection.remoteAddress;
49
+ }
50
+
51
+ if (request.connection.socket && request.connection.socket.remoteAddress) {
52
+ return request.connection.socket.remoteAddress;
53
+ }
54
+ }
55
+
56
+ if (request.socket && request.socket.remoteAddress) {
57
+ return request.socket.remoteAddress;
58
+ }
59
+
60
+ if (request.info && request.info.remoteAddress) {
61
+ return request.info.remoteAddress;
62
+ }
63
+
64
+ if (request.requestContext && request.requestContext.identity && request.requestContext.identity.sourceIp) {
65
+ return request.requestContext.identity.sourceIp;
66
+ }
67
+
68
+ return '';
69
+ };
70
+
71
+ module.exports = getClientIp;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @param {module:http~IncomingMessage} request
3
+ * @returns {boolean}
4
+ */
5
+ const isWebSocket = request => {
6
+ if (request.method !== 'GET') return false;
7
+
8
+ const connection = request.headers.connection || '';
9
+ const upgrade = request.headers.upgrade || '';
10
+
11
+ return request.method === 'GET' &&
12
+ connection.toLowerCase().split(/ *, */).indexOf('upgrade') >= 0 &&
13
+ upgrade.toLowerCase() === 'websocket';
14
+ };
15
+
16
+ module.exports = isWebSocket;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * TODO: improve
3
+ * @param {Buffer} data
4
+ * @returns {string}
5
+ */
6
+ const parseWsMessage = data => {
7
+ const dl = data[1] & 127;
8
+ let ifm = 2;
9
+ if (dl === 126) {
10
+ ifm = 4;
11
+ } else if (dl === 127) {
12
+ ifm = 10;
13
+ }
14
+ let i = ifm + 4;
15
+ const masks = data.slice(ifm, i);
16
+ let index = 0;
17
+ let output = "";
18
+ const l = data.length;
19
+ while (i < l) {
20
+ output += String.fromCharCode(data[i++] ^ masks[index++ % 4]);
21
+ }
22
+ return output;
23
+ };
24
+
25
+ module.exports = parseWsMessage;
@@ -14,7 +14,7 @@ const createWorkerCommand = workerPath => {
14
14
  switch (ext) {
15
15
  case '.js':
16
16
  default:
17
- return `node ${path.resolve(__dirname, '../middleware/workerInvoke.js')} ${workerPath}`;
17
+ return `node --expose-gc ${path.resolve(__dirname, '../middleware/workerInvoke.js')} ${workerPath}`;
18
18
  }
19
19
  };
20
20
 
@@ -67,36 +67,34 @@ class WorkerPool {
67
67
  || (this.overallLimit > 0 && this.getWorkerCount() >= this.overallLimit);
68
68
  }
69
69
 
70
- getWorker(workerPath, options = {}, limit = 0) {
70
+ async getWorker(workerPath, options = {}, limit = 0) {
71
71
  const nonBusyId = this.getNonBusyId(workerPath);
72
72
  // TODO: tidy up
73
73
  if (nonBusyId !== undefined) {
74
- return Promise.resolve(this.workers[workerPath][nonBusyId]);
74
+ return this.workers[workerPath][nonBusyId];
75
75
  } else if (this.isBeyondLimit(workerPath, limit) || this._creating) {
76
- return Promise.resolve()
77
- .then(() => new Promise(r => setTimeout(r, this.idleCheckTimeout)))
78
- .then(() => this.getWorker(workerPath, options, limit));
76
+ await new Promise(r => setTimeout(r, this.idleCheckTimeout));
77
+ return this.getWorker(workerPath, options, limit);
79
78
  }
80
79
 
81
80
  this._creating = true;
82
- return Promise.resolve()
83
- .then(() => {
84
- const id = uuid();
85
- const instance = new Worker(createWorkerCommand(workerPath), options);
86
-
87
- instance.addEventListenerOnce('close', code => {
88
- this.workers[workerPath][id] = null;
89
- delete this.workers[workerPath][id];
90
- this.onExit(code, workerPath, id);
91
- });
92
- this.workers[workerPath] = {
93
- ...(this.workers[workerPath] && this.workers[workerPath]),
94
- [id]: instance,
95
- };
96
-
97
- this._creating = false;
98
- return Promise.resolve(instance);
99
- });
81
+ const id = uuid();
82
+ const instance = new Worker(createWorkerCommand(workerPath), options);
83
+
84
+ if (!this.workers[workerPath]) {
85
+ this.workers[workerPath] = {};
86
+ }
87
+
88
+ instance.addEventListenerOnce('close', code => {
89
+ this.workers[workerPath][id] = null;
90
+ delete this.workers[workerPath][id];
91
+ this.onExit(code, workerPath, id);
92
+ });
93
+
94
+ this.workers[workerPath][id] = instance;
95
+
96
+ this._creating = false;
97
+ return instance;
100
98
  }
101
99
  }
102
100