@winkjs/composer 0.5.1 → 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.
Files changed (125) hide show
  1. package/README.md +1 -1
  2. package/package.json +3 -3
  3. package/src/composer.js +1 -1
  4. package/src/core/batch/aoo-encoder.js +4 -4
  5. package/src/core/batch/sliding-window.js +2 -2
  6. package/src/core/codec/index.js +4 -4
  7. package/src/core/emitter-manager/mqtt/connection.js +209 -0
  8. package/src/core/emitter-manager/mqtt/constants.js +43 -29
  9. package/src/core/emitter-manager/mqtt/emitter.js +108 -633
  10. package/src/core/emitter-manager/mqtt/health.js +151 -0
  11. package/src/core/emitter-manager/mqtt/index.js +68 -4
  12. package/src/core/emitter-manager/mqtt/publish.js +292 -0
  13. package/src/core/emitter-manager/mqtt/resolve-config.js +312 -0
  14. package/src/core/emitter-manager/mqtt/shutdown-drain.js +247 -0
  15. package/src/core/emitter-manager/terminal/assert-columns.js +1 -1
  16. package/src/core/emitter-manager/terminal/index.js +20 -5
  17. package/src/core/env-vars.js +143 -16
  18. package/src/core/logger/index.js +174 -0
  19. package/src/core/mqtt-protocol.js +2 -2
  20. package/src/core/partition-manager/get-stats.js +31 -0
  21. package/src/core/partition-manager/index.js +1 -0
  22. package/src/core/partition-manager/init.js +15 -0
  23. package/src/core/partition-manager/update.js +143 -87
  24. package/src/core/partition-manager/validate-flow.js +1 -1
  25. package/src/core/semantics/loader.js +1 -1
  26. package/src/core/semantics/warnings.js +5 -3
  27. package/src/core/shutdown-manager/index.js +71 -21
  28. package/src/core/source-manager/csv/start.js +109 -35
  29. package/src/core/source-manager/mqtt/client.js +276 -83
  30. package/src/core/source-manager/mqtt/dedup.js +19 -14
  31. package/src/core/source-manager/mqtt/index.js +33 -6
  32. package/src/core/source-manager/mqtt/status.js +211 -46
  33. package/src/core/source-manager/record-shape.js +55 -0
  34. package/src/core/source-manager/test-harness/field-generator.js +1 -1
  35. package/src/core/source-manager/test-harness/start.js +52 -10
  36. package/src/core/source-manager/test-harness/validate.js +1 -1
  37. package/src/core/storage-manager/questdb/address-policy.js +264 -0
  38. package/src/core/storage-manager/questdb/assert-columns.js +2 -2
  39. package/src/core/storage-manager/questdb/delivery-gate.js +218 -0
  40. package/src/core/storage-manager/questdb/ensure-tables.js +1 -1
  41. package/src/core/storage-manager/questdb/flush-engine.js +852 -0
  42. package/src/core/storage-manager/questdb/flush-tracker.js +331 -0
  43. package/src/core/storage-manager/questdb/index.js +592 -827
  44. package/src/core/storage-manager/questdb/persist-plan.js +147 -162
  45. package/src/core/storage-manager/questdb/resolve-options.js +403 -0
  46. package/src/core/storage-manager/questdb/shutdown-drain.js +181 -0
  47. package/src/core/storage-manager/questdb/skip-warnings.js +180 -0
  48. package/src/core/test-utils/tcp-proxy.js +194 -1
  49. package/src/core/tunable/helpers.js +1 -1
  50. package/src/core/utils/address/index.js +362 -0
  51. package/src/core/utils/address/probe.js +210 -0
  52. package/src/core/utils/callback-guard/index.js +176 -0
  53. package/src/core/utils/clock/index.js +34 -0
  54. package/src/core/utils/half-life/index.js +4 -4
  55. package/src/core/utils/jitter/index.js +51 -0
  56. package/src/core/utils/line-rate/index.js +104 -0
  57. package/src/core/utils/quantize/index.js +43 -19
  58. package/src/core/utils/validate/composer-validators.js +2 -2
  59. package/src/core/utils/validate/index.js +1 -1
  60. package/src/core/utils/validate/validate.js +1 -1
  61. package/src/core/wiring/assert-handle.js +5 -5
  62. package/src/core/wiring/assert-module.js +2 -2
  63. package/src/core/wiring/wire-emitters.js +7 -4
  64. package/src/core/wiring/wire-linear-graph.js +1 -1
  65. package/src/core/wiring/wire-node.js +6 -6
  66. package/src/core/wiring/wire-semantics.js +1 -1
  67. package/src/core/wiring/wire-storages.js +14 -5
  68. package/src/flow/driver.js +31 -6
  69. package/src/flow/expand-single-input-array.js +4 -4
  70. package/src/flow/flow-api-schemas.js +4 -4
  71. package/src/flow/flow.js +62 -41
  72. package/src/flow/for-each.js +12 -11
  73. package/src/flow/get-target-array.js +2 -2
  74. package/src/flow/load-node-module.js +1 -1
  75. package/src/flow/make-node-method.js +4 -4
  76. package/src/flow/resolve-pick-by-field.js +1 -1
  77. package/src/flow/run.js +200 -40
  78. package/src/flow/serialize.js +1 -1
  79. package/src/flow/spec-builder.js +1 -1
  80. package/src/flow/validate.js +1 -1
  81. package/src/nodes/butterworth-filter/init.js +5 -4
  82. package/src/nodes/categorize/update.js +3 -1
  83. package/src/nodes/controller/update.js +2 -1
  84. package/src/nodes/dwell-time-tracker/update.js +2 -1
  85. package/src/nodes/emit-if/helpers.js +57 -11
  86. package/src/nodes/emit-if/init.js +1 -1
  87. package/src/nodes/emit-if/update.js +8 -11
  88. package/src/nodes/page-hinkley/update.js +2 -1
  89. package/src/nodes/pass-if/update.js +3 -1
  90. package/src/nodes/persist-if/helpers.js +60 -4
  91. package/src/nodes/persist-if/init.js +1 -1
  92. package/src/nodes/persist-if/update.js +8 -16
  93. package/src/nodes/persistence-check/update.js +2 -1
  94. package/src/nodes/sanitize/update.js +4 -2
  95. package/src/nodes/swing-watch/update.js +3 -2
  96. package/src/nodes/threshold/update.js +2 -1
  97. package/src/nodes/trend/compute-confidence.js +4 -4
  98. package/src/nodes/trend/update.js +2 -1
  99. package/src/nodes/winnow/update.js +3 -1
  100. package/src/tools/training/features.js +13 -13
  101. package/src/tools/training/logistic.js +1 -1
  102. package/src/tools/training/metrics.js +2 -2
  103. package/src/tools/training/scale.js +2 -2
  104. package/src/tools/training/validate.js +7 -7
  105. package/src/core/utils/console-formatter/index.js +0 -207
  106. package/src/nodes/archive/csv/index.js +0 -6
  107. package/src/nodes/archive/csv/init.js +0 -36
  108. package/src/nodes/archive/csv/load.js +0 -70
  109. package/src/nodes/archive/csv/reset.js +0 -14
  110. package/src/nodes/archive/csv/simple-parser.js +0 -216
  111. package/src/nodes/archive/csv/test.js +0 -173
  112. package/src/nodes/archive/csv/update.js +0 -63
  113. package/src/nodes/sse-emitter/index.js +0 -7
  114. package/src/nodes/sse-emitter/init.js +0 -105
  115. package/src/nodes/sse-emitter/introspect.js +0 -134
  116. package/src/nodes/sse-emitter/publish-to.js +0 -6
  117. package/src/nodes/sse-emitter/recompute.js +0 -16
  118. package/src/nodes/sse-emitter/reset.js +0 -19
  119. package/src/nodes/sse-emitter/update.js +0 -69
  120. package/src/nodes/sse-emitter/utils/broadcast-heartbeat.js +0 -36
  121. package/src/nodes/sse-emitter/utils/build-message.js +0 -31
  122. package/src/nodes/sse-emitter/utils/connection-manager.js +0 -45
  123. package/src/nodes/sse-emitter/utils/endpoint-initializer.js +0 -20
  124. package/src/nodes/sse-emitter/utils/health-monitor.js +0 -38
  125. package/src/nodes/sse-emitter/utils/qos-handler.js +0 -41
@@ -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 client socket first (sending RST so clients see the
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
+ };
@@ -72,7 +72,7 @@ export const lookupByField = function ( field, map, defaultVal ) {
72
72
  export const pickByField = function ( map ) {
73
73
  const fn = () => {
74
74
  throw new Error(
75
- 'WinkComposer/flow: pickByField is resolved at build time inside forEach; ' +
75
+ 'winkComposer/flow: pickByField is resolved at build time inside forEach; ' +
76
76
  'it has no runtime value. Use it only for a forEach option.'
77
77
  );
78
78
  };