geonix 1.23.6 → 1.30.2
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/LICENSE.md +1 -1
- package/README.md +348 -4
- package/exports.js +0 -2
- package/index.d.ts +292 -237
- package/package.json +11 -8
- package/src/Codec.js +20 -7
- package/src/Connection.js +94 -40
- package/src/Crypto.js +103 -0
- package/src/Gateway.js +155 -70
- package/src/Logger.js +90 -9
- package/src/Registry.js +133 -15
- package/src/Remote.js +15 -6
- package/src/Request.js +117 -80
- package/src/RequestOptions.js +11 -8
- package/src/Service.js +133 -91
- package/src/Stream.js +69 -15
- package/src/Util.js +196 -158
- package/src/WebServer.js +18 -10
- package/test/context.js +0 -35
- package/test/delayedStart.js +0 -24
- package/test/gateway.js +0 -34
- package/test/middleware.js +0 -24
- package/test/package.json +0 -16
- package/test/pubsub.js +0 -29
- package/test/simple.js +0 -29
- package/test/static/index.html +0 -1
- package/test/stream.js +0 -43
- package/test/upload.js +0 -34
- package/test/ws_auth.js +0 -21
package/src/Util.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash, randomBytes } from "crypto";
|
|
2
|
-
import { URL
|
|
3
|
-
import {
|
|
2
|
+
import { URL } from "url";
|
|
3
|
+
import { createRequire } from "module";
|
|
4
4
|
import { join } from "path";
|
|
5
5
|
import { Transform } from "node:stream";
|
|
6
6
|
import { networkInterfaces } from "os";
|
|
@@ -24,7 +24,7 @@ export const sleep = delay => new Promise(resolve => setTimeout(resolve, delay))
|
|
|
24
24
|
*
|
|
25
25
|
* @returns
|
|
26
26
|
*/
|
|
27
|
-
export const
|
|
27
|
+
export const yieldToEventLoop = () => new Promise(resolve => setImmediate(resolve));
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* Parse nats:// URL
|
|
@@ -47,12 +47,29 @@ export function parseURL(url) {
|
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
const BASE62 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
51
|
+
const LOG256_LOG62 = Math.log(256) / Math.log(62); // ≈ 1.3437
|
|
52
|
+
|
|
53
|
+
export function encodeBase62(buffer) {
|
|
54
|
+
if (buffer.length === 0) { return ""; }
|
|
55
|
+
const len = Math.ceil(buffer.length * LOG256_LOG62);
|
|
56
|
+
let n = BigInt("0x" + buffer.toString("hex"));
|
|
57
|
+
const chars = new Array(len);
|
|
58
|
+
for (let i = len - 1; i >= 0; i--) {
|
|
59
|
+
chars[i] = BASE62[Number(n % 62n)];
|
|
60
|
+
n /= 62n;
|
|
61
|
+
}
|
|
62
|
+
return chars.join("");
|
|
63
|
+
}
|
|
64
|
+
|
|
50
65
|
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
66
|
+
* Generates a cryptographically random Base62-encoded ID string.
|
|
67
|
+
* Exported as `randomID` in the public API.
|
|
68
|
+
*
|
|
69
|
+
* @param {number} [size=16] - Number of random bytes to encode (more bytes → longer, more unique ID).
|
|
70
|
+
* @returns {string} URL-safe, Base62-encoded random string.
|
|
54
71
|
*/
|
|
55
|
-
export const picoid = (size =
|
|
72
|
+
export const picoid = (size = 16) => encodeBase62(randomBytes(size));
|
|
56
73
|
|
|
57
74
|
/**
|
|
58
75
|
* Get SHA256 hash of a string or a buffer
|
|
@@ -81,39 +98,24 @@ export const createServerAtPort = (port, pkg, handler) =>
|
|
|
81
98
|
});
|
|
82
99
|
|
|
83
100
|
/**
|
|
84
|
-
* Create TCP or HTTP server at
|
|
85
|
-
* @param {Object} pkg
|
|
86
|
-
* @param {Function} handler
|
|
87
|
-
* @
|
|
88
|
-
* @param {number} poolSize
|
|
89
|
-
* @returns
|
|
101
|
+
* Create TCP or HTTP server at an OS-assigned free port
|
|
102
|
+
* @param {Object} pkg
|
|
103
|
+
* @param {Function} handler
|
|
104
|
+
* @returns
|
|
90
105
|
*/
|
|
91
|
-
export const createServerAtFreePort =
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
} catch {
|
|
99
|
-
// silenty ignore errors
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Create TCP server at random port
|
|
106
|
-
* @param {Function} handler
|
|
107
|
-
* @returns
|
|
108
|
-
*/
|
|
109
|
-
export const createTCPServer = (handler, start = 30000, poolSize = 20000) => createServerAtFreePort(net, handler, start, poolSize);
|
|
106
|
+
export const createServerAtFreePort = (pkg, handler) =>
|
|
107
|
+
new Promise((resolve, reject) => {
|
|
108
|
+
const server = pkg.createServer(handler);
|
|
109
|
+
server.on("error", reject);
|
|
110
|
+
server.listen(0, () => resolve({ server, port: server.address().port }));
|
|
111
|
+
});
|
|
110
112
|
|
|
111
113
|
/**
|
|
112
|
-
* Create
|
|
113
|
-
* @param {Function} handler
|
|
114
|
-
* @returns
|
|
114
|
+
* Create TCP server at an OS-assigned free port
|
|
115
|
+
* @param {Function} handler
|
|
116
|
+
* @returns
|
|
115
117
|
*/
|
|
116
|
-
export const
|
|
118
|
+
export const createTCPServer = (handler) => createServerAtFreePort(net, handler);
|
|
117
119
|
|
|
118
120
|
/**
|
|
119
121
|
* Return number of seconds passed from the start of the day (0-86399)
|
|
@@ -124,39 +126,6 @@ export const getSecondsSinceMidnight = () => {
|
|
|
124
126
|
return Math.floor((date.getTime() - date.setHours(0, 0, 0, 0)) / 1000);
|
|
125
127
|
};
|
|
126
128
|
|
|
127
|
-
/**
|
|
128
|
-
* Parse function body and return array of param names
|
|
129
|
-
* @param {*} fn
|
|
130
|
-
* @returns string[]
|
|
131
|
-
*/
|
|
132
|
-
export const getFunctionParams = (fn) => {
|
|
133
|
-
const code = fn.toString();
|
|
134
|
-
const endParenthesisPosition = code.indexOf(")");
|
|
135
|
-
let params;
|
|
136
|
-
|
|
137
|
-
if (endParenthesisPosition != -1) {
|
|
138
|
-
params = code.substring(code.indexOf("(") + 1, endParenthesisPosition);
|
|
139
|
-
} else {
|
|
140
|
-
params = code.substring(0, code.indexOf("=>"));
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
params = params
|
|
144
|
-
// cleanup spaces
|
|
145
|
-
.replaceAll(" ", "")
|
|
146
|
-
// split into array
|
|
147
|
-
.split(",");
|
|
148
|
-
|
|
149
|
-
// remove potential default values
|
|
150
|
-
for (let index = 0; index < params.length; index++) {
|
|
151
|
-
const defaultValueAssignmentPosition = params[index].indexOf("=");
|
|
152
|
-
if (defaultValueAssignmentPosition != -1) {
|
|
153
|
-
params[index] = params[index].substring(0, defaultValueAssignmentPosition - 1);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
return params;
|
|
158
|
-
};
|
|
159
|
-
|
|
160
129
|
export const proxyHttp = (target, req, res) =>
|
|
161
130
|
new Promise((resolve, reject) => {
|
|
162
131
|
const remoteTarget = `${target}${req.originalUrl}`;
|
|
@@ -188,37 +157,40 @@ export const proxyHttp = (target, req, res) =>
|
|
|
188
157
|
*/
|
|
189
158
|
export const OverlayObject = (object, overlay) => new Proxy(object, { get: (t, p) => overlay[p] !== undefined ? overlay[p] : t[p] });
|
|
190
159
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
160
|
+
/**
|
|
161
|
+
* The version string of the currently installed Geonix package, read from `package.json` at
|
|
162
|
+
* module load time. Equals `"N/A"` when the package metadata cannot be found.
|
|
163
|
+
*
|
|
164
|
+
* @type {string}
|
|
165
|
+
*/
|
|
166
|
+
export const GeonixVersion = (() => {
|
|
167
|
+
try {
|
|
168
|
+
return createRequire(import.meta.url)("../package.json").version ?? "N/A";
|
|
169
|
+
} catch {
|
|
170
|
+
return "N/A";
|
|
171
|
+
}
|
|
172
|
+
})();
|
|
204
173
|
|
|
205
|
-
|
|
174
|
+
/**
|
|
175
|
+
* Chunk a stream into smaller chunks
|
|
176
|
+
*
|
|
177
|
+
* @param {*} chunkSize
|
|
178
|
+
* @returns
|
|
179
|
+
*/
|
|
180
|
+
export const StreamChunker = (chunkSize = 65536) => new Transform({
|
|
181
|
+
transform(chunk, _encoding, done) {
|
|
206
182
|
let offset = 0;
|
|
207
183
|
while (offset < chunk.length) {
|
|
208
184
|
const sliceSize = Math.min(chunkSize, chunk.length - offset);
|
|
209
185
|
this.push(chunk.slice(offset, offset + sliceSize));
|
|
210
186
|
offset += sliceSize;
|
|
211
187
|
}
|
|
212
|
-
|
|
213
188
|
done();
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
chunker._flush = function (done) {
|
|
189
|
+
},
|
|
190
|
+
flush(done) {
|
|
217
191
|
done();
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
return chunker;
|
|
221
|
-
};
|
|
192
|
+
}
|
|
193
|
+
});
|
|
222
194
|
|
|
223
195
|
export async function getFirstItemFromAsyncIterable(asyncIterable) {
|
|
224
196
|
const iterator = asyncIterable[Symbol.asyncIterator]();
|
|
@@ -235,6 +207,10 @@ export function getNetworkAddresses() {
|
|
|
235
207
|
if (addressObject.family === "IPv4") {
|
|
236
208
|
list.push(addressObject.address);
|
|
237
209
|
}
|
|
210
|
+
|
|
211
|
+
if (addressObject.family === "IPv6") {
|
|
212
|
+
list.push(`[${addressObject.address}]`);
|
|
213
|
+
}
|
|
238
214
|
}
|
|
239
215
|
}
|
|
240
216
|
|
|
@@ -246,15 +222,24 @@ export function isIterable(obj) {
|
|
|
246
222
|
}
|
|
247
223
|
|
|
248
224
|
/**
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
* @
|
|
252
|
-
*
|
|
253
|
-
*
|
|
225
|
+
* Parses a `multipart/form-data` request body into an array of part objects. Each part
|
|
226
|
+
* exposes parsed headers (e.g. `content-disposition`), a `name`, an optional `filename`,
|
|
227
|
+
* and a `body` {@link import('stream').Readable}.
|
|
228
|
+
*
|
|
229
|
+
* By default parts are streamed through temporary files on disk; set `options.useMemory` to
|
|
230
|
+
* `true` to buffer them in memory instead.
|
|
231
|
+
*
|
|
232
|
+
* @param {import('http').IncomingMessage} req - Incoming HTTP request with a `multipart/form-data` content-type.
|
|
233
|
+
* @param {object} [_options] - Parsing options.
|
|
234
|
+
* @param {boolean} [_options.useMemory=false] - Buffer parts in memory instead of temp files.
|
|
235
|
+
* @param {number} [_options.maxFileSize] - Maximum allowed size in bytes for a single part.
|
|
236
|
+
* @param {number} [_options.maxFiles] - Maximum number of parts allowed.
|
|
237
|
+
* @returns {Promise<Array<{ name: string|null, filename: string|null, headers: object, body: import('stream').Readable, size: number }>>}
|
|
238
|
+
* @throws {Error} If the content-type is not `multipart/form-data` or a size/count limit is exceeded.
|
|
254
239
|
*/
|
|
255
240
|
export async function parseMultipart(req, _options) {
|
|
256
241
|
if (!req.headers["content-type"]?.startsWith("multipart/form-data")) {
|
|
257
|
-
throw
|
|
242
|
+
throw Error("Invalid content type (multipart/form-data expected)");
|
|
258
243
|
}
|
|
259
244
|
|
|
260
245
|
const BUFFER_SIZE = 1024 * 1024;
|
|
@@ -272,15 +257,41 @@ export async function parseMultipart(req, _options) {
|
|
|
272
257
|
options.useMemory = true;
|
|
273
258
|
}
|
|
274
259
|
|
|
275
|
-
const
|
|
260
|
+
const boundaryValue = req.headers["content-type"].match(/boundary=([^;,\s]+)/)?.[1];
|
|
261
|
+
if (!boundaryValue) {
|
|
262
|
+
throw Error("parseMultipart: missing boundary in content-type");
|
|
263
|
+
}
|
|
264
|
+
const boundary = Buffer.from("\r\n--" + boundaryValue);
|
|
276
265
|
|
|
277
|
-
await
|
|
266
|
+
await yieldToEventLoop();
|
|
278
267
|
|
|
279
268
|
let lastChunk = Buffer.from("\r\n");
|
|
280
269
|
let activePart;
|
|
281
|
-
|
|
270
|
+
|
|
271
|
+
const cleanup = async () => {
|
|
272
|
+
for (const part of parts) {
|
|
273
|
+
if (part.bodyFile) {
|
|
274
|
+
try {
|
|
275
|
+
part.body.destroy();
|
|
276
|
+
} catch {
|
|
277
|
+
// ignore errors
|
|
278
|
+
}
|
|
279
|
+
try {
|
|
280
|
+
await unlink(part.bodyFile);
|
|
281
|
+
} catch {
|
|
282
|
+
// ignore errors
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
};
|
|
282
287
|
|
|
283
288
|
const write = (chunk) => {
|
|
289
|
+
if (options.maxFileSize !== undefined) {
|
|
290
|
+
activePart.size += chunk.length;
|
|
291
|
+
if (activePart.size > options.maxFileSize) {
|
|
292
|
+
throw Error(`parseMultipart: part exceeds maxFileSize of ${options.maxFileSize} bytes`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
284
295
|
if (options.useMemory) {
|
|
285
296
|
activePart.body.push(chunk);
|
|
286
297
|
} else {
|
|
@@ -289,75 +300,95 @@ export async function parseMultipart(req, _options) {
|
|
|
289
300
|
};
|
|
290
301
|
|
|
291
302
|
const newPart = () => {
|
|
292
|
-
|
|
303
|
+
if (options.maxFiles !== undefined && parts.length >= options.maxFiles) {
|
|
304
|
+
throw Error(`parseMultipart: exceeded maxFiles limit of ${options.maxFiles}`);
|
|
305
|
+
}
|
|
293
306
|
const bodyFile = tempFilename();
|
|
294
307
|
activePart = {
|
|
295
308
|
headers: {},
|
|
296
309
|
bodyFile: options.useMemory ? undefined : bodyFile,
|
|
297
|
-
body: options.useMemory ? [] : createWriteStream(bodyFile, { flags: "wx" })
|
|
310
|
+
body: options.useMemory ? [] : createWriteStream(bodyFile, { flags: "wx" }),
|
|
311
|
+
size: 0
|
|
298
312
|
};
|
|
299
313
|
parts.push(activePart);
|
|
300
314
|
};
|
|
301
315
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
316
|
+
try {
|
|
317
|
+
while (stream.readable) {
|
|
318
|
+
// next next chunk
|
|
319
|
+
let chunk = stream.read(BUFFER_SIZE);
|
|
305
320
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
321
|
+
if (!chunk) {
|
|
322
|
+
await yieldToEventLoop();
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
310
325
|
|
|
311
|
-
|
|
326
|
+
let combined = Buffer.concat([lastChunk, chunk]);
|
|
327
|
+
let lookbehindSet = false;
|
|
328
|
+
|
|
329
|
+
while (combined.length >= boundary.length + 2) {
|
|
330
|
+
const boundaryIndex = combined.indexOf(boundary);
|
|
331
|
+
|
|
332
|
+
if (boundaryIndex === -1) {
|
|
333
|
+
// Keep only the last boundary.length-1 bytes as lookbehind so a
|
|
334
|
+
// boundary that straddles a read boundary is not split across chunks.
|
|
335
|
+
const safeLength = combined.length - (boundary.length - 1);
|
|
336
|
+
if (activePart && safeLength > 0) {
|
|
337
|
+
write(combined.subarray(0, safeLength));
|
|
338
|
+
}
|
|
339
|
+
lastChunk = combined.subarray(safeLength);
|
|
340
|
+
lookbehindSet = true;
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
312
343
|
|
|
313
|
-
|
|
314
|
-
const boundaryIndex = combined.indexOf(boundary);
|
|
315
|
-
const isLastBoundary = combined[boundaryIndex + boundary.length] === 45 && combined[boundaryIndex + boundary.length + 1] === 45;
|
|
344
|
+
const isLastBoundary = combined[boundaryIndex + boundary.length] === 45 && combined[boundaryIndex + boundary.length + 1] === 45;
|
|
316
345
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
}
|
|
346
|
+
if (boundaryIndex > 0) {
|
|
347
|
+
write(combined.subarray(0, boundaryIndex));
|
|
348
|
+
}
|
|
321
349
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
350
|
+
if (isLastBoundary) {
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
325
353
|
|
|
326
|
-
|
|
327
|
-
combined = combined.subarray(boundaryIndex + boundary.length + 2);
|
|
328
|
-
done = true;
|
|
329
|
-
break;
|
|
330
|
-
}
|
|
354
|
+
newPart();
|
|
331
355
|
|
|
332
|
-
|
|
356
|
+
const endOfHeaders = combined.indexOf(END_OF_HEADERS, boundaryIndex);
|
|
333
357
|
|
|
334
|
-
|
|
358
|
+
if (endOfHeaders === -1) {
|
|
359
|
+
throw Error("parseMultipart: malformed part — missing header terminator");
|
|
360
|
+
}
|
|
335
361
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
362
|
+
activePart.headers = combined
|
|
363
|
+
.subarray(boundaryIndex + boundary.length + 2, endOfHeaders).toString()
|
|
364
|
+
.split("\r\n")
|
|
365
|
+
.reduce((acc, val) => {
|
|
366
|
+
const [header, value] = val.split(": ");
|
|
367
|
+
acc[header.toLowerCase()] = value;
|
|
368
|
+
return acc;
|
|
369
|
+
}, Object.create(null));
|
|
344
370
|
|
|
345
|
-
|
|
371
|
+
combined = combined.subarray(endOfHeaders + END_OF_HEADERS.length);
|
|
346
372
|
|
|
347
|
-
|
|
348
|
-
|
|
373
|
+
lastChunk = combined;
|
|
374
|
+
}
|
|
349
375
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
376
|
+
// Carry the unprocessed remainder into the next read so it gets
|
|
377
|
+
// prepended to the next chunk. Skip when the lookbehind was already
|
|
378
|
+
// set inside the boundaryIndex === -1 branch above.
|
|
379
|
+
if (!lookbehindSet) {
|
|
380
|
+
lastChunk = combined;
|
|
381
|
+
}
|
|
354
382
|
}
|
|
383
|
+
} catch (e) {
|
|
384
|
+
await cleanup();
|
|
385
|
+
throw e;
|
|
355
386
|
}
|
|
356
387
|
|
|
357
388
|
for (const part of parts) {
|
|
358
389
|
// extract name and filename from content-disposition header
|
|
359
390
|
if (part.headers["content-disposition"]) {
|
|
360
|
-
const [, name] = part.headers["content-disposition"].match(/name="([^"]+)"/);
|
|
391
|
+
const [, name] = part.headers["content-disposition"].match(/name="([^"]+)"/) || [];
|
|
361
392
|
const [, filename] = part.headers["content-disposition"].match(/filename="([^"]+)"/) || [];
|
|
362
393
|
part.name = name ?? null;
|
|
363
394
|
part.filename = filename ?? null;
|
|
@@ -383,24 +414,13 @@ export async function parseMultipart(req, _options) {
|
|
|
383
414
|
}
|
|
384
415
|
|
|
385
416
|
export function tempFilename() {
|
|
386
|
-
return join(tmpdir(), `${
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
export function randomSafeId(size = 12) {
|
|
390
|
-
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
391
|
-
let result = "";
|
|
392
|
-
|
|
393
|
-
for (let i = 0; i < size; i++) {
|
|
394
|
-
result += charset.charAt(Math.floor(Math.random() * charset.length));
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
return result;
|
|
417
|
+
return join(tmpdir(), `${picoid(12)}.gxtmp`);
|
|
398
418
|
}
|
|
399
419
|
|
|
400
420
|
export function deepMerge(target, ...source) {
|
|
401
421
|
for (const src of source) {
|
|
402
|
-
for (const key
|
|
403
|
-
if (src[key] instanceof Object) {
|
|
422
|
+
for (const key of Object.keys(src)) {
|
|
423
|
+
if (src[key] instanceof Object && !Array.isArray(src[key])) {
|
|
404
424
|
if (!target[key]) {
|
|
405
425
|
target[key] = {};
|
|
406
426
|
}
|
|
@@ -423,4 +443,22 @@ export function cleanupWebsocketUrl(url) {
|
|
|
423
443
|
} catch {
|
|
424
444
|
return url;
|
|
425
445
|
}
|
|
426
|
-
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export async function fetchWithTimeout(url, options = {}, timeout = 500) {
|
|
449
|
+
const ac = new AbortController();
|
|
450
|
+
const timer = setTimeout(() => ac.abort(), timeout);
|
|
451
|
+
try {
|
|
452
|
+
return await fetch(url, { ...options, signal: ac.signal });
|
|
453
|
+
} finally {
|
|
454
|
+
clearTimeout(timer);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
export function withTimeout(promise, timeout) {
|
|
459
|
+
let timeoutId;
|
|
460
|
+
const timer = new Promise((_, reject) => {
|
|
461
|
+
timeoutId = setTimeout(() => reject(Error("Timeout")), timeout);
|
|
462
|
+
});
|
|
463
|
+
return Promise.race([promise, timer]).finally(() => clearTimeout(timeoutId));
|
|
464
|
+
}
|
package/src/WebServer.js
CHANGED
|
@@ -3,13 +3,21 @@ import express, { Router } from "express";
|
|
|
3
3
|
import expressWs from "express-ws";
|
|
4
4
|
import { createServerAtFreePort, createServerAtPort, sleep } from "./Util.js";
|
|
5
5
|
import * as http from "http";
|
|
6
|
-
import { Service } from "./Service.js";
|
|
7
6
|
import * as path from "path";
|
|
8
7
|
import { logger } from "./Logger.js";
|
|
9
8
|
import { activeStreams } from "./Stream.js";
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Creates an Express router that serves static files from `root`. Supports an optional URL
|
|
12
|
+
* prefix strip via `options.root` and falls back to `index.html` for unmatched paths when
|
|
13
|
+
* `options.indexOn404` is `true` (useful for single-page applications).
|
|
14
|
+
*
|
|
15
|
+
* @param {string} root - Filesystem path to the directory containing static assets.
|
|
16
|
+
* @param {object} [options] - Options forwarded to `express.static`, plus:
|
|
17
|
+
* @param {string} [options.root] - URL prefix to strip before serving.
|
|
18
|
+
* @param {boolean} [options.indexOn404] - If `true`, respond with `index.html` for all 404s.
|
|
19
|
+
* @returns {import('express').Router}
|
|
20
|
+
*/
|
|
13
21
|
export const ServeStatic = (root, options = {}) => {
|
|
14
22
|
const router = Router();
|
|
15
23
|
const absoluteRoot = path.resolve(root);
|
|
@@ -34,7 +42,6 @@ export const ServeStatic = (root, options = {}) => {
|
|
|
34
42
|
|
|
35
43
|
if (options.indexOn404) {
|
|
36
44
|
router.get("*", (req, res) => {
|
|
37
|
-
logger.info(path.join(absoluteRoot, "index.html"));
|
|
38
45
|
res.sendFile(path.join(absoluteRoot, "index.html"));
|
|
39
46
|
});
|
|
40
47
|
}
|
|
@@ -58,14 +65,15 @@ class WebServer {
|
|
|
58
65
|
this.#started = true;
|
|
59
66
|
|
|
60
67
|
let srv;
|
|
61
|
-
|
|
62
|
-
|
|
68
|
+
const localPort = process.env.GX_LOCAL_PORT || process.env.LOCAL_PORT;
|
|
69
|
+
if (localPort) {
|
|
70
|
+
srv = await createServerAtPort(localPort, http, this.#app);
|
|
63
71
|
} else {
|
|
64
72
|
srv = await createServerAtFreePort(http, this.#app);
|
|
65
73
|
}
|
|
66
74
|
|
|
67
75
|
if (!srv) {
|
|
68
|
-
throw
|
|
76
|
+
throw Error("gx.webserver.start: unable to start");
|
|
69
77
|
}
|
|
70
78
|
|
|
71
79
|
this.#server = srv.server;
|
|
@@ -74,7 +82,7 @@ class WebServer {
|
|
|
74
82
|
expressWs(this.#app, srv.server);
|
|
75
83
|
|
|
76
84
|
// stream endpoint
|
|
77
|
-
this.#app.get("/!!
|
|
85
|
+
this.#app.get("/!!_gx/stream/:id", (req, res) => {
|
|
78
86
|
const id = req.params.id;
|
|
79
87
|
|
|
80
88
|
if (activeStreams[id]) {
|
|
@@ -86,8 +94,8 @@ class WebServer {
|
|
|
86
94
|
}
|
|
87
95
|
});
|
|
88
96
|
|
|
89
|
-
this.#app.get(
|
|
90
|
-
res.send({ status: "healthy"
|
|
97
|
+
this.#app.get("/!!_gx/health", (req, res) => {
|
|
98
|
+
res.send({ status: "healthy" });
|
|
91
99
|
});
|
|
92
100
|
|
|
93
101
|
// middleware to handle dynamic routers
|
package/test/context.js
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { Remote, Service } from "../exports.js";
|
|
2
|
-
import { sleep } from "../src/Util.js";
|
|
3
|
-
|
|
4
|
-
class TimeService extends Service {
|
|
5
|
-
|
|
6
|
-
#timestamp() {
|
|
7
|
-
return new Date().toISOString();
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
getCurrentTime() {
|
|
11
|
-
const [prefix] = this.context;
|
|
12
|
-
|
|
13
|
-
return `${prefix} ${this.#timestamp()}`;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
class ApplicationService extends Service {
|
|
19
|
-
|
|
20
|
-
#timeService = Remote("TimeService", "prefix");
|
|
21
|
-
|
|
22
|
-
async onStart() {
|
|
23
|
-
while (true) {
|
|
24
|
-
const time = await this.#timeService.getCurrentTime();
|
|
25
|
-
|
|
26
|
-
console.log("TIME =", time);
|
|
27
|
-
|
|
28
|
-
await sleep(1000);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
TimeService.start();
|
|
35
|
-
ApplicationService.start();
|
package/test/delayedStart.js
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { Gateway, Service, streamToBuffer } from "../exports.js";
|
|
2
|
-
import { parseMultipart, sleep } from "../src/Util.js";
|
|
3
|
-
|
|
4
|
-
class TestService extends Service {
|
|
5
|
-
|
|
6
|
-
"GET /test/"(req, res) {
|
|
7
|
-
res.send("Hello World");
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
class Application extends Service {
|
|
13
|
-
|
|
14
|
-
"GET /"(req, res) {
|
|
15
|
-
res.send("app");
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
Application.start();
|
|
21
|
-
|
|
22
|
-
await sleep(3000);
|
|
23
|
-
|
|
24
|
-
TestService.start();
|
package/test/gateway.js
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { Gateway, Service, streamToBuffer } from "../exports.js";
|
|
2
|
-
import { parseMultipart } from "../src/Util.js";
|
|
3
|
-
|
|
4
|
-
// class TestService extends Service {
|
|
5
|
-
|
|
6
|
-
// "GET /"(req, res) {
|
|
7
|
-
// res.send("Hello World");
|
|
8
|
-
// }
|
|
9
|
-
|
|
10
|
-
// async "POST /upload"(req, res) {
|
|
11
|
-
// const parts = await parseMultipart(req, { useMemory: false });
|
|
12
|
-
|
|
13
|
-
// for (const part of parts) {
|
|
14
|
-
// console.log(part.body);
|
|
15
|
-
// }
|
|
16
|
-
|
|
17
|
-
// res.send("OK");
|
|
18
|
-
// }
|
|
19
|
-
|
|
20
|
-
// }
|
|
21
|
-
|
|
22
|
-
// TestService.start({
|
|
23
|
-
// middleware: {
|
|
24
|
-
// raw: true,
|
|
25
|
-
// json: false,
|
|
26
|
-
// cookies: false,
|
|
27
|
-
// }
|
|
28
|
-
// });
|
|
29
|
-
// Gateway.start({
|
|
30
|
-
// beforeRequest: (req, res) => {
|
|
31
|
-
// res.set("X-Test", "Test");
|
|
32
|
-
// }
|
|
33
|
-
// });
|
|
34
|
-
Gateway.start();
|
package/test/middleware.js
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { Router } from "express";
|
|
2
|
-
import { ServeStatic, Service } from "../exports.js";
|
|
3
|
-
|
|
4
|
-
function special(req, res) {
|
|
5
|
-
console.log("THIS:", this);
|
|
6
|
-
|
|
7
|
-
res.send(`Hello ${this.value}`);
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
class ApplicationService extends Service {
|
|
11
|
-
|
|
12
|
-
value = "World!";
|
|
13
|
-
|
|
14
|
-
async onStart() {
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
"GET *" = [special, ServeStatic("test/static", { indexOn404: true })];
|
|
18
|
-
// "GET *" = [(req, res) => {
|
|
19
|
-
// res.send(`Hello ${this.value}`);
|
|
20
|
-
// }];
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
ApplicationService.start();
|