@winkjs/composer 0.6.0 → 0.7.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/README.md +1 -1
- package/package.json +2 -2
- package/src/composer.js +1 -1
- package/src/core/emitter-manager/mqtt/connection.js +209 -0
- package/src/core/emitter-manager/mqtt/constants.js +43 -29
- package/src/core/emitter-manager/mqtt/emitter.js +103 -658
- package/src/core/emitter-manager/mqtt/health.js +151 -0
- package/src/core/emitter-manager/mqtt/index.js +68 -4
- package/src/core/emitter-manager/mqtt/publish.js +292 -0
- package/src/core/emitter-manager/mqtt/resolve-config.js +312 -0
- package/src/core/emitter-manager/mqtt/shutdown-drain.js +247 -0
- package/src/core/emitter-manager/terminal/index.js +16 -1
- package/src/core/env-vars.js +99 -13
- package/src/core/logger/index.js +1 -1
- package/src/core/source-manager/mqtt/client.js +197 -63
- package/src/core/source-manager/mqtt/dedup.js +16 -11
- package/src/core/source-manager/mqtt/index.js +32 -5
- package/src/core/source-manager/mqtt/status.js +168 -56
- package/src/core/storage-manager/questdb/address-policy.js +264 -0
- package/src/core/storage-manager/questdb/delivery-gate.js +218 -0
- package/src/core/storage-manager/questdb/flush-engine.js +852 -0
- package/src/core/storage-manager/questdb/flush-tracker.js +331 -0
- package/src/core/storage-manager/questdb/index.js +574 -847
- package/src/core/storage-manager/questdb/persist-plan.js +128 -167
- package/src/core/storage-manager/questdb/resolve-options.js +403 -0
- package/src/core/storage-manager/questdb/shutdown-drain.js +181 -0
- package/src/core/storage-manager/questdb/skip-warnings.js +180 -0
- package/src/core/test-utils/tcp-proxy.js +194 -1
- package/src/core/utils/address/index.js +362 -0
- package/src/core/utils/address/probe.js +210 -0
- package/src/core/utils/callback-guard/index.js +23 -2
- package/src/core/utils/clock/index.js +34 -0
- package/src/core/utils/jitter/index.js +51 -0
- package/src/core/utils/line-rate/index.js +104 -0
- package/src/core/utils/quantize/index.js +43 -19
- package/src/core/wiring/wire-emitters.js +3 -1
- package/src/core/wiring/wire-storages.js +10 -2
- package/src/flow/flow.js +24 -3
- package/src/flow/run.js +24 -2
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// core/storage-manager/questdb/skip-warnings.js
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview How a skipped value is reported.
|
|
5
|
+
*
|
|
6
|
+
* The persist plan checks every value before a row opens. A value that
|
|
7
|
+
* fails its check is skipped, and the skip is reported through
|
|
8
|
+
* `onWarning`: a column skip lands the column as NULL, a bad designated
|
|
9
|
+
* timestamp skips the whole row. This module builds the functions the
|
|
10
|
+
* plan calls at those two sites. It owns the wording of the lines, the
|
|
11
|
+
* default handler, and the bound on that default.
|
|
12
|
+
*
|
|
13
|
+
* Why the default is bounded (ADR-029, the same rule as the flush loss
|
|
14
|
+
* line). A dead sensor publishes NaN in one column on every row. That
|
|
15
|
+
* is one warning line per row for as long as the sensor is dead: one
|
|
16
|
+
* line a second for a whole night, in a log nobody can read. So the
|
|
17
|
+
* default handler is bounded per column. The first two skips of an
|
|
18
|
+
* episode print in full. Later skips are counted, and one summary
|
|
19
|
+
* line prints per minute with the count, the latest reason, and the
|
|
20
|
+
* latest asset. A quiet minute on that column ends the episode. Rows
|
|
21
|
+
* skipped for a bad designated timestamp share one bound per insight
|
|
22
|
+
* type.
|
|
23
|
+
*
|
|
24
|
+
* The bound is on the default only. A user `onWarning` hears every
|
|
25
|
+
* skip with the full line, because the flow chose to listen. A user
|
|
26
|
+
* handler that throws is strict mode (ADR-027): the throw is the
|
|
27
|
+
* instruction that rejects the row, so it must reach the plan
|
|
28
|
+
* unchanged. Neither is bounded.
|
|
29
|
+
*
|
|
30
|
+
* Nothing here runs on the happy path. The plan calls a warning only
|
|
31
|
+
* when a value failed its check, and a counted skip allocates nothing:
|
|
32
|
+
* the line is rendered only when it prints.
|
|
33
|
+
*
|
|
34
|
+
* @see ADR-027
|
|
35
|
+
* @see ADR-029
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import { createLineBound } from '../../utils/line-rate/index.js';
|
|
39
|
+
import { logger } from '../../logger/index.js';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The bound on the default warning (see the file header). The values
|
|
43
|
+
* match the flush loss line in flush-engine.js, so an operator reads
|
|
44
|
+
* one rhythm.
|
|
45
|
+
*/
|
|
46
|
+
const FULL_WARNING_LINES_PER_EPISODE = 2;
|
|
47
|
+
const WARNING_SUMMARY_INTERVAL_MS = 60000;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Default warning handler for invalid column values: one `logger.warn`
|
|
51
|
+
* line in winkComposer format. The skip sites reach it through the
|
|
52
|
+
* per-column line bound (see the file header), so under a streak it
|
|
53
|
+
* prints the first two skips in full and then one summary per minute.
|
|
54
|
+
*
|
|
55
|
+
* Validation behavior:
|
|
56
|
+
* - null/undefined columns: skip column only (QuestDB stores NULL)
|
|
57
|
+
* - NaN/Infinity in numeric columns: skip column only (QuestDB stores NULL)
|
|
58
|
+
* - Wrong-typed column values (e.g. a number where the column is string-typed):
|
|
59
|
+
* skip column only, never coerce — the warning names the expected and
|
|
60
|
+
* received types
|
|
61
|
+
* - Invalid designatedTimestamp: skip entire row
|
|
62
|
+
*
|
|
63
|
+
* For strict mode (throw on any invalid), provide:
|
|
64
|
+
* { onWarning: (msg) => { throw new Error(msg); } }
|
|
65
|
+
*
|
|
66
|
+
* Future extension: return value may control skip-row vs skip-column behavior.
|
|
67
|
+
*
|
|
68
|
+
* @param {string} message - Warning message describing the issue
|
|
69
|
+
*/
|
|
70
|
+
const defaultOnWarning = function ( message ) {
|
|
71
|
+
logger.warn( `winkComposer/questdb: ${message}` );
|
|
72
|
+
}; // defaultOnWarning()
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Builds the reason text for a value that failed its phase-1 acceptance
|
|
76
|
+
* check. Called only on the rare skip path, so its allocations are
|
|
77
|
+
* acceptable (same budget as the error returns in index.js).
|
|
78
|
+
*
|
|
79
|
+
* @param {*} rawValue - the rejected value
|
|
80
|
+
* @param {boolean} isNumeric - whether the column type is numeric
|
|
81
|
+
* @param {string} expectedType - declared column type, named in the message
|
|
82
|
+
* @returns {string} plain reason text for the skip warning
|
|
83
|
+
*/
|
|
84
|
+
const skipReason = function ( rawValue, isNumeric, expectedType ) {
|
|
85
|
+
if ( rawValue === null ) return 'null';
|
|
86
|
+
if ( rawValue === undefined ) return 'undefined';
|
|
87
|
+
if ( isNumeric && typeof rawValue === 'number' ) {
|
|
88
|
+
if ( Number.isNaN( rawValue ) ) return 'NaN';
|
|
89
|
+
// A finite number can only be rejected by the integer-required
|
|
90
|
+
// types (int64/timestamp/designated timestamp) — float64 accepts
|
|
91
|
+
// every finite number.
|
|
92
|
+
return Number.isFinite( rawValue ) ? 'non-integer' : 'non-finite';
|
|
93
|
+
}
|
|
94
|
+
return `wrong-typed (expected ${expectedType}, received ${typeof rawValue})`;
|
|
95
|
+
}; // skipReason()
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Builds the warning function for one skip site. With a user
|
|
99
|
+
* `onWarning`, every skip reaches it with the full line. With the
|
|
100
|
+
* default, the line bound applies. Both renderers run only when a line
|
|
101
|
+
* prints, so a counted skip allocates nothing.
|
|
102
|
+
*
|
|
103
|
+
* @param {Function|undefined} providedOnWarning - The user's handler, or undefined for the default
|
|
104
|
+
* @param {Function} fullLine - `( rawValue, partitionId ) => string`, without the brand prefix
|
|
105
|
+
* @param {Function} summaryLine - `( count, seconds, rawValue, partitionId ) => string`, without the brand prefix
|
|
106
|
+
* @returns {Function} `( rawValue, partitionId ) => void`
|
|
107
|
+
*/
|
|
108
|
+
const makeSkipWarning = function ( providedOnWarning, fullLine, summaryLine ) {
|
|
109
|
+
if ( providedOnWarning !== undefined ) {
|
|
110
|
+
return function ( rawValue, partitionId ) {
|
|
111
|
+
providedOnWarning( fullLine( rawValue, partitionId ) );
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return createLineBound( {
|
|
115
|
+
fullLines: FULL_WARNING_LINES_PER_EPISODE,
|
|
116
|
+
intervalMs: WARNING_SUMMARY_INTERVAL_MS,
|
|
117
|
+
printFull: function ( rawValue, partitionId ) {
|
|
118
|
+
defaultOnWarning( fullLine( rawValue, partitionId ) );
|
|
119
|
+
},
|
|
120
|
+
printSummary: function ( count, seconds, rawValue, partitionId ) {
|
|
121
|
+
defaultOnWarning( summaryLine( count, seconds, rawValue, partitionId ) );
|
|
122
|
+
}
|
|
123
|
+
} );
|
|
124
|
+
}; // makeSkipWarning()
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Builds the skip warning for one column of one insight type.
|
|
128
|
+
*
|
|
129
|
+
* @param {Function|undefined} providedOnWarning - The user's handler, or undefined for the default
|
|
130
|
+
* @param {string} insightTypeName - Named in every line
|
|
131
|
+
* @param {string} columnName - The column the skip is about
|
|
132
|
+
* @param {boolean} isNumeric - Whether the column type is numeric, for the reason text
|
|
133
|
+
* @param {string} columnType - The declared column type, for the reason text
|
|
134
|
+
* @returns {Function} `( rawValue, partitionId ) => void`
|
|
135
|
+
*/
|
|
136
|
+
const makeColumnWarning = function ( providedOnWarning, insightTypeName, columnName, isNumeric, columnType ) {
|
|
137
|
+
return makeSkipWarning(
|
|
138
|
+
providedOnWarning,
|
|
139
|
+
( rawValue, partitionId ) => (
|
|
140
|
+
`column '${columnName}' is ${skipReason( rawValue, isNumeric, columnType )} ` +
|
|
141
|
+
`in insightType '${insightTypeName}' (asset: ${partitionId}) — column skipped`
|
|
142
|
+
),
|
|
143
|
+
( count, seconds, rawValue, partitionId ) => (
|
|
144
|
+
`column '${columnName}' skipped in ${count} more row(s) of insightType '${insightTypeName}' ` +
|
|
145
|
+
`in the last ${seconds} s (latest: ${skipReason( rawValue, isNumeric, columnType )}, asset: ${partitionId})`
|
|
146
|
+
)
|
|
147
|
+
);
|
|
148
|
+
}; // makeColumnWarning()
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Builds the row-skip warning of one insight type, for a designated
|
|
152
|
+
* timestamp that is missing or not an integer.
|
|
153
|
+
*
|
|
154
|
+
* @param {Function|undefined} providedOnWarning - The user's handler, or undefined for the default
|
|
155
|
+
* @param {string} insightTypeName - Named in every line
|
|
156
|
+
* @param {string} designatedTimestamp - The timestamp column, named in every line
|
|
157
|
+
* @returns {Function} `( tsValue, partitionId ) => void`
|
|
158
|
+
*/
|
|
159
|
+
const makeRowWarning = function ( providedOnWarning, insightTypeName, designatedTimestamp ) {
|
|
160
|
+
return makeSkipWarning(
|
|
161
|
+
providedOnWarning,
|
|
162
|
+
( tsValue, partitionId ) => (
|
|
163
|
+
`designatedTimestamp '${designatedTimestamp}' is ${skipReason( tsValue, true, 'timestamp' )} ` +
|
|
164
|
+
`in insightType '${insightTypeName}' (asset: ${partitionId}) - row skipped`
|
|
165
|
+
),
|
|
166
|
+
( count, seconds, tsValue, partitionId ) => (
|
|
167
|
+
`${count} more row(s) of insightType '${insightTypeName}' skipped for designatedTimestamp ` +
|
|
168
|
+
`'${designatedTimestamp}' in the last ${seconds} s (latest: ${skipReason( tsValue, true, 'timestamp' )}, ` +
|
|
169
|
+
`asset: ${partitionId})`
|
|
170
|
+
)
|
|
171
|
+
);
|
|
172
|
+
}; // makeRowWarning()
|
|
173
|
+
|
|
174
|
+
export {
|
|
175
|
+
defaultOnWarning,
|
|
176
|
+
makeColumnWarning,
|
|
177
|
+
makeRowWarning,
|
|
178
|
+
FULL_WARNING_LINES_PER_EPISODE,
|
|
179
|
+
WARNING_SUMMARY_INTERVAL_MS
|
|
180
|
+
};
|
|
@@ -14,6 +14,15 @@
|
|
|
14
14
|
* "service unreachable" without coupling tests to docker daemon
|
|
15
15
|
* access. The pattern is identical for any TCP-based service.
|
|
16
16
|
*
|
|
17
|
+
* `startBlackHole()` models the other dead-endpoint shape: a server
|
|
18
|
+
* that accepts the connection and never answers. The QuestDB exit test
|
|
19
|
+
* uses it to hold a request on the wire while the process shuts down.
|
|
20
|
+
*
|
|
21
|
+
* `startHttpResponder()` models a service that is up but answers an
|
|
22
|
+
* error. It forwards to the real service or answers a status code,
|
|
23
|
+
* switched on the live server so no request is reset. The HTTP-error
|
|
24
|
+
* hardening spec uses it.
|
|
25
|
+
*
|
|
17
26
|
* Cross-cutting test infrastructure mirrors `src/core/source-manager/test-harness/`
|
|
18
27
|
* — both live under `src/core/` because they're shared across adapter
|
|
19
28
|
* modules.
|
|
@@ -27,6 +36,7 @@
|
|
|
27
36
|
* @module test-utils/tcp-proxy
|
|
28
37
|
*/
|
|
29
38
|
|
|
39
|
+
import http from 'node:http';
|
|
30
40
|
import net from 'node:net';
|
|
31
41
|
|
|
32
42
|
/**
|
|
@@ -58,6 +68,14 @@ export const startProxy = function ( listenPort, targetPort ) {
|
|
|
58
68
|
clientSocket.pipe( targetSocket );
|
|
59
69
|
targetSocket.pipe( clientSocket );
|
|
60
70
|
} );
|
|
71
|
+
// The target side is tracked too. A destroyed client socket does
|
|
72
|
+
// not end its pipe partner. Without this, the proxy's own sockets
|
|
73
|
+
// to the service outlive `stopProxy()` and hold the process open.
|
|
74
|
+
// The QuestDB exit test found that.
|
|
75
|
+
sockets.add( targetSocket );
|
|
76
|
+
targetSocket.on( 'close', function () {
|
|
77
|
+
sockets.delete( targetSocket );
|
|
78
|
+
} );
|
|
61
79
|
// See file-header note on swallowed errors.
|
|
62
80
|
clientSocket.on( 'error', function () {} );
|
|
63
81
|
targetSocket.on( 'error', function () {} );
|
|
@@ -76,7 +94,8 @@ export const startProxy = function ( listenPort, targetPort ) {
|
|
|
76
94
|
|
|
77
95
|
/**
|
|
78
96
|
* Stop a proxy started via `startProxy()`. Force-destroys every
|
|
79
|
-
* tracked
|
|
97
|
+
* tracked socket first, both the client side and the proxy's own
|
|
98
|
+
* side toward the service (sending RST so clients see the
|
|
80
99
|
* disconnect immediately, not at next keepalive timeout), then
|
|
81
100
|
* waits for the server to close cleanly.
|
|
82
101
|
*
|
|
@@ -104,3 +123,177 @@ export const stopProxy = function ( server ) {
|
|
|
104
123
|
} );
|
|
105
124
|
} );
|
|
106
125
|
};
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Start a black hole on `port`: a server that accepts every connection
|
|
129
|
+
* and never answers. It reads and discards whatever the client sends,
|
|
130
|
+
* so the client's write completes and the client then waits for a
|
|
131
|
+
* response that never comes. That is the shape of an endpoint whose
|
|
132
|
+
* process is alive but wedged. A closed port has a different shape:
|
|
133
|
+
* it refuses at once.
|
|
134
|
+
*
|
|
135
|
+
* The hole has a second mode. After `forwardTo( targetPort )` every
|
|
136
|
+
* new connection is relayed to that port, like `startProxy()`. After
|
|
137
|
+
* `swallow()` the hole is back: new connections are swallowed, and a
|
|
138
|
+
* connection that was being relayed goes quiet from that instant. Its
|
|
139
|
+
* service side is dropped and its client side stays open, so a
|
|
140
|
+
* request already on a kept-alive socket waits for an answer that
|
|
141
|
+
* never comes. The switch happens on the live server, so the port
|
|
142
|
+
* never closes and no request on the wire is reset. A port bounce
|
|
143
|
+
* would reset one, and the client reports a reset as a socket error,
|
|
144
|
+
* not as a hang.
|
|
145
|
+
*
|
|
146
|
+
* Returns the `net.Server` once it is listening, with `forwardTo()`
|
|
147
|
+
* and `swallow()` attached; pass it to `stopProxy()` to tear down,
|
|
148
|
+
* sockets included.
|
|
149
|
+
*
|
|
150
|
+
* @param {number} port
|
|
151
|
+
* @returns {Promise<net.Server>}
|
|
152
|
+
*/
|
|
153
|
+
export const startBlackHole = function ( port ) {
|
|
154
|
+
const sockets = new Set();
|
|
155
|
+
// Client socket → service socket, for the connections being relayed.
|
|
156
|
+
const relayed = new Map();
|
|
157
|
+
let targetPort = null;
|
|
158
|
+
|
|
159
|
+
const track = function ( socket ) {
|
|
160
|
+
sockets.add( socket );
|
|
161
|
+
socket.on( 'close', function () {
|
|
162
|
+
sockets.delete( socket );
|
|
163
|
+
} );
|
|
164
|
+
// See file-header note on swallowed errors.
|
|
165
|
+
socket.on( 'error', function () {} );
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const relay = function ( clientSocket, servicePort ) {
|
|
169
|
+
const targetSocket = net.connect( servicePort, '127.0.0.1', function () {
|
|
170
|
+
clientSocket.pipe( targetSocket );
|
|
171
|
+
targetSocket.pipe( clientSocket );
|
|
172
|
+
} );
|
|
173
|
+
track( targetSocket );
|
|
174
|
+
relayed.set( clientSocket, targetSocket );
|
|
175
|
+
clientSocket.on( 'close', function () {
|
|
176
|
+
relayed.delete( clientSocket );
|
|
177
|
+
targetSocket.destroy();
|
|
178
|
+
} );
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const server = net.createServer( function ( clientSocket ) {
|
|
182
|
+
track( clientSocket );
|
|
183
|
+
if ( targetPort === null ) {
|
|
184
|
+
// Consume the request bytes so the client's write completes.
|
|
185
|
+
clientSocket.resume();
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
relay( clientSocket, targetPort );
|
|
189
|
+
} );
|
|
190
|
+
server.forwardTo = function ( servicePort ) {
|
|
191
|
+
targetPort = servicePort;
|
|
192
|
+
};
|
|
193
|
+
server.swallow = function () {
|
|
194
|
+
targetPort = null;
|
|
195
|
+
for ( const [ clientSocket, targetSocket ] of relayed ) {
|
|
196
|
+
clientSocket.unpipe( targetSocket );
|
|
197
|
+
targetSocket.unpipe( clientSocket );
|
|
198
|
+
targetSocket.destroy();
|
|
199
|
+
clientSocket.resume();
|
|
200
|
+
}
|
|
201
|
+
relayed.clear();
|
|
202
|
+
};
|
|
203
|
+
// eslint-disable-next-line no-underscore-dangle
|
|
204
|
+
server._sockets = sockets;
|
|
205
|
+
return new Promise( function ( resolve ) {
|
|
206
|
+
server.listen( port, '127.0.0.1', function () {
|
|
207
|
+
resolve( server );
|
|
208
|
+
} );
|
|
209
|
+
} );
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Start an HTTP responder on `port`. It has two modes, switched on the
|
|
214
|
+
* live server, so the port never closes between them. In answer mode
|
|
215
|
+
* it reads every request and answers it with `statusCode` and a short
|
|
216
|
+
* plain-text body. That is the shape of a service that is up but
|
|
217
|
+
* refuses the request, such as a 400 for a malformed row or a 500 for
|
|
218
|
+
* an internal failure. In forward mode it relays each request to a
|
|
219
|
+
* real service on `127.0.0.1:<upstreamPort>` and returns that
|
|
220
|
+
* service's answer. Every answer closes the connection.
|
|
221
|
+
*
|
|
222
|
+
* Why a mode switch and not a port bounce: closing a server destroys
|
|
223
|
+
* its sockets, so a request on the wire at that instant reads a reset
|
|
224
|
+
* instead of an answer. A switch on a live server resets nothing. Each
|
|
225
|
+
* request gets the mode that was active when it arrived.
|
|
226
|
+
*
|
|
227
|
+
* The QuestDB client treats some codes as retryable and the rest as
|
|
228
|
+
* final; the HTTP-error hardening spec pins both against this
|
|
229
|
+
* responder. Returns the server once it is listening, with two extra
|
|
230
|
+
* methods: `answerWith( statusCode )` and `forwardTo( upstreamPort )`.
|
|
231
|
+
* Pass the server to `stopProxy()` to tear down, sockets included.
|
|
232
|
+
*
|
|
233
|
+
* @param {number} port
|
|
234
|
+
* @param {number} statusCode - answered until `forwardTo()` is called
|
|
235
|
+
* @returns {Promise<http.Server>}
|
|
236
|
+
*/
|
|
237
|
+
export const startHttpResponder = function ( port, statusCode ) {
|
|
238
|
+
const sockets = new Set();
|
|
239
|
+
let mode = { answer: statusCode, forward: null };
|
|
240
|
+
|
|
241
|
+
const answer = function ( req, res, code ) {
|
|
242
|
+
req.resume();
|
|
243
|
+
req.on( 'end', function () {
|
|
244
|
+
res.writeHead( code, { 'Content-Type': 'text/plain', Connection: 'close' } );
|
|
245
|
+
res.end( `responder answered ${code}` );
|
|
246
|
+
} );
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const forward = function ( req, res, upstreamPort ) {
|
|
250
|
+
const headers = Object.assign( {}, req.headers );
|
|
251
|
+
delete headers.connection;
|
|
252
|
+
const upstream = http.request( {
|
|
253
|
+
host: '127.0.0.1',
|
|
254
|
+
port: upstreamPort,
|
|
255
|
+
method: req.method,
|
|
256
|
+
path: req.url,
|
|
257
|
+
headers,
|
|
258
|
+
agent: false
|
|
259
|
+
}, function ( upstreamRes ) {
|
|
260
|
+
res.writeHead( upstreamRes.statusCode, Object.assign( {}, upstreamRes.headers, { connection: 'close' } ) );
|
|
261
|
+
upstreamRes.pipe( res );
|
|
262
|
+
} );
|
|
263
|
+
// See file-header note on swallowed errors.
|
|
264
|
+
upstream.on( 'error', function () {
|
|
265
|
+
res.destroy();
|
|
266
|
+
} );
|
|
267
|
+
req.pipe( upstream );
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
const server = http.createServer( function ( req, res ) {
|
|
271
|
+
const current = mode;
|
|
272
|
+
if ( current.forward === null ) {
|
|
273
|
+
answer( req, res, current.answer );
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
forward( req, res, current.forward );
|
|
277
|
+
} );
|
|
278
|
+
server.answerWith = function ( code ) {
|
|
279
|
+
mode = { answer: code, forward: null };
|
|
280
|
+
};
|
|
281
|
+
server.forwardTo = function ( upstreamPort ) {
|
|
282
|
+
mode = { answer: null, forward: upstreamPort };
|
|
283
|
+
};
|
|
284
|
+
server.on( 'connection', function ( socket ) {
|
|
285
|
+
sockets.add( socket );
|
|
286
|
+
socket.on( 'close', function () {
|
|
287
|
+
sockets.delete( socket );
|
|
288
|
+
} );
|
|
289
|
+
// See file-header note on swallowed errors.
|
|
290
|
+
socket.on( 'error', function () {} );
|
|
291
|
+
} );
|
|
292
|
+
// eslint-disable-next-line no-underscore-dangle
|
|
293
|
+
server._sockets = sockets;
|
|
294
|
+
return new Promise( function ( resolve ) {
|
|
295
|
+
server.listen( port, '127.0.0.1', function () {
|
|
296
|
+
resolve( server );
|
|
297
|
+
} );
|
|
298
|
+
} );
|
|
299
|
+
};
|