configurapi-runner-ws 1.14.0 → 1.16.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 +262 -12
- package/package.json +1 -1
- package/src/index.js +8 -0
- package/src/wsAdapter.js +5 -7
- package/src/wsPostbackFunction.js +36 -0
package/README.md
CHANGED
|
@@ -1,14 +1,264 @@
|
|
|
1
1
|
# configurapi-runner-ws
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
-
|
|
14
|
-
- `
|
|
3
|
+
This runner allows you to run **Configurapi** locally using a native
|
|
4
|
+
Node.js WebSocket server.
|
|
5
|
+
|
|
6
|
+
It mirrors the behavior of `configurapi-runner-lambda-ws`, but runs
|
|
7
|
+
entirely on your local machine (or any Node.js host) without AWS.
|
|
8
|
+
|
|
9
|
+
------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
## Overview
|
|
12
|
+
|
|
13
|
+
- Use `Event` for incoming WebSocket messages.
|
|
14
|
+
- Use `Response` for regular response data.
|
|
15
|
+
- Use `StreamResponse` (from `configurapi-handler-ws`) for streaming
|
|
16
|
+
responses.
|
|
17
|
+
- Includes an optional HTTP management server.
|
|
18
|
+
|
|
19
|
+
------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
## Running Locally
|
|
22
|
+
|
|
23
|
+
Example:
|
|
24
|
+
|
|
25
|
+
``` js
|
|
26
|
+
const HttpRunner = require('configurapi-runner-ws');
|
|
27
|
+
|
|
28
|
+
const runner = new HttpRunner();
|
|
29
|
+
|
|
30
|
+
runner.run({
|
|
31
|
+
port: 8000,
|
|
32
|
+
configPath: './config.yaml'
|
|
33
|
+
});
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Default ports:
|
|
37
|
+
|
|
38
|
+
- HTTP: `8000`
|
|
39
|
+
- HTTPS: `8443` (if `key` and `cert` provided)
|
|
40
|
+
- Management server: `9100`
|
|
41
|
+
|
|
42
|
+
------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
## Streaming Responses
|
|
45
|
+
|
|
46
|
+
When returning a `StreamResponse`:
|
|
47
|
+
|
|
48
|
+
- Data is delivered in chunks.
|
|
49
|
+
|
|
50
|
+
- Each chunk includes:
|
|
51
|
+
|
|
52
|
+
x-stream: chunk
|
|
53
|
+
|
|
54
|
+
- The final frame includes:
|
|
55
|
+
|
|
56
|
+
x-stream: done
|
|
57
|
+
|
|
58
|
+
- Each stream includes:
|
|
59
|
+
|
|
60
|
+
x-stream-id: <id>
|
|
61
|
+
|
|
62
|
+
All chunks belonging to the same stream share the same `x-stream-id`.
|
|
63
|
+
|
|
64
|
+
The local runner supports backpressure handling using
|
|
65
|
+
`ws.bufferedAmount`.
|
|
66
|
+
|
|
67
|
+
------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
## Connection Lifecycle
|
|
70
|
+
|
|
71
|
+
### on_connect
|
|
72
|
+
|
|
73
|
+
Triggered when a new WebSocket connection is established.
|
|
74
|
+
|
|
75
|
+
Use this event for:
|
|
76
|
+
|
|
77
|
+
- Authentication
|
|
78
|
+
- Authorization
|
|
79
|
+
- Protocol negotiation
|
|
80
|
+
|
|
81
|
+
If `on_connect` returns:
|
|
82
|
+
|
|
83
|
+
statusCode >= 400
|
|
84
|
+
|
|
85
|
+
the connection is closed with:
|
|
86
|
+
|
|
87
|
+
1008 (Policy Violation)
|
|
88
|
+
|
|
89
|
+
Example:
|
|
90
|
+
|
|
91
|
+
``` js
|
|
92
|
+
if (response.statusCode >= 400) {
|
|
93
|
+
ws.close(1008, 'Policy Violation');
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`Sec-WebSocket-Protocol` is automatically echoed back during
|
|
98
|
+
`on_connect`.
|
|
99
|
+
|
|
100
|
+
------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
### on_disconnect
|
|
103
|
+
|
|
104
|
+
Triggered when a WebSocket connection closes.
|
|
105
|
+
|
|
106
|
+
Use this for:
|
|
107
|
+
|
|
108
|
+
- Cleanup
|
|
109
|
+
- Resource release
|
|
110
|
+
- Logging
|
|
111
|
+
|
|
112
|
+
------------------------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
## Request Object Details
|
|
115
|
+
|
|
116
|
+
Inside your Configurapi handler:
|
|
117
|
+
|
|
118
|
+
- `event.request.headers['connection-id']`
|
|
119
|
+
- Generated UUID for each WebSocket connection
|
|
120
|
+
- `event.request.headers['push-function']`
|
|
121
|
+
- Async function to send an out-of-band message to the same client
|
|
122
|
+
|
|
123
|
+
Example:
|
|
124
|
+
|
|
125
|
+
``` js
|
|
126
|
+
await event.request.headers['push-function'](
|
|
127
|
+
new Response({ hello: "world" }, 200)
|
|
128
|
+
);
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
- `event.request.params`
|
|
132
|
+
- Available if provided in the client message
|
|
133
|
+
- `event.request.query`
|
|
134
|
+
- Comes from the WebSocket message body
|
|
135
|
+
- `event.request.payload`
|
|
136
|
+
- Only set if `payload` exists in the client message
|
|
137
|
+
|
|
138
|
+
If invalid JSON is received, the raw message is placed in `payload`.
|
|
139
|
+
|
|
140
|
+
------------------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
## Message Format Example
|
|
143
|
+
|
|
144
|
+
Client sends:
|
|
145
|
+
|
|
146
|
+
``` json
|
|
147
|
+
{
|
|
148
|
+
"name": "getData",
|
|
149
|
+
"params": { "id": 123 },
|
|
150
|
+
"payload": { "foo": "bar" },
|
|
151
|
+
"headers": {
|
|
152
|
+
"message-id": "abc-123"
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
The runner:
|
|
158
|
+
|
|
159
|
+
- Creates a `Configurapi.Event`
|
|
160
|
+
- Routes to event `getData`
|
|
161
|
+
- Echoes `message-id` in the response
|
|
162
|
+
|
|
163
|
+
------------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
## Management Server
|
|
166
|
+
|
|
167
|
+
A management HTTP server runs on port `9100` by default. The postback URL can be found from `event.request.headers['postback-url']`.
|
|
168
|
+
|
|
169
|
+
Built-in internal routes:
|
|
170
|
+
|
|
171
|
+
- `list_@connections`
|
|
172
|
+
- Returns active WebSocket connection IDs
|
|
173
|
+
- `post_@connection`
|
|
174
|
+
- Sends a message to a specific connection
|
|
175
|
+
|
|
176
|
+
Example:
|
|
177
|
+
|
|
178
|
+
``` bash
|
|
179
|
+
curl http://localhost:9100/list_@connections
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Post back to connection:
|
|
183
|
+
|
|
184
|
+
``` bash
|
|
185
|
+
curl -X POST http://localhost:9100/post_@connection?id=<connectionId>
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
------------------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
## Using wsPostbackFunction
|
|
191
|
+
|
|
192
|
+
You can also use `wsPostbackFunction` directly when communicating through the local HTTP management bridge.
|
|
193
|
+
|
|
194
|
+
The postback URL can be found from `event.request.headers['postback-url']`.
|
|
195
|
+
|
|
196
|
+
### Import:
|
|
197
|
+
|
|
198
|
+
```
|
|
199
|
+
const { wsPostbackFunction } = require('configurapi-runner-ws');
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Example:
|
|
203
|
+
|
|
204
|
+
```
|
|
205
|
+
await wsPostbackFunction(
|
|
206
|
+
`http://localhost:9100/@connections/${connectionId}`,
|
|
207
|
+
{
|
|
208
|
+
statusCode: 200,
|
|
209
|
+
headers: { 'message-id': '123' },
|
|
210
|
+
body: { message: 'Hello' }
|
|
211
|
+
}
|
|
212
|
+
);
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Error Handling
|
|
216
|
+
|
|
217
|
+
- `SyntaxError` → 400
|
|
218
|
+
- All other errors → 500
|
|
219
|
+
- `message-id` header is preserved in error responses
|
|
220
|
+
|
|
221
|
+
------------------------------------------------------------------------
|
|
222
|
+
|
|
223
|
+
## HTTPS Support
|
|
224
|
+
|
|
225
|
+
If `key` and `cert` are provided:
|
|
226
|
+
|
|
227
|
+
``` js
|
|
228
|
+
runner.run({
|
|
229
|
+
sPort: 8443,
|
|
230
|
+
key: fs.readFileSync('./server.key'),
|
|
231
|
+
cert: fs.readFileSync('./server.cert')
|
|
232
|
+
});
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
the server runs securely over HTTPS + WSS.
|
|
236
|
+
|
|
237
|
+
------------------------------------------------------------------------
|
|
238
|
+
|
|
239
|
+
## Transport Differences from Lambda Runner
|
|
240
|
+
|
|
241
|
+
-----------------------------------------------------------------------
|
|
242
|
+
Local Runner Lambda Runner
|
|
243
|
+
----------------------------------- -----------------------------------
|
|
244
|
+
Uses native WebSocket Uses API Gateway WebSocket
|
|
245
|
+
|
|
246
|
+
Close with `1008` for connect Return HTTP 4xx/5xx from `$connect`
|
|
247
|
+
rejection
|
|
248
|
+
|
|
249
|
+
Backpressure via Frame queue with retry logic
|
|
250
|
+
`ws.bufferedAmount`
|
|
251
|
+
|
|
252
|
+
Direct `ws.send()` API Gateway `postToConnection()`
|
|
253
|
+
-----------------------------------------------------------------------
|
|
254
|
+
|
|
255
|
+
Functionally, both runners behave the same at the Configurapi level.
|
|
256
|
+
|
|
257
|
+
------------------------------------------------------------------------
|
|
258
|
+
|
|
259
|
+
## Best Practices
|
|
260
|
+
|
|
261
|
+
- Use `on_connect` for authentication.
|
|
262
|
+
- Always include `name` in client messages.
|
|
263
|
+
- Use `StreamResponse` for progressive or large responses.
|
|
264
|
+
- Monitor active connections via the management server.
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -139,6 +139,12 @@ module.exports = class HttpRunner extends events.EventEmitter
|
|
|
139
139
|
|
|
140
140
|
this.emit(LogLevel.Trace, `[connect] ${connectionId}`);
|
|
141
141
|
|
|
142
|
+
const protocol = req.headers['x-forwarded-proto'] || (req.socket.encrypted ? 'https' : 'http');
|
|
143
|
+
const host = req.headers.host;
|
|
144
|
+
const path = req.url.split('?')[0];
|
|
145
|
+
|
|
146
|
+
ws.postbackUrl = `${protocol}://${host}${path}/@connections/${connectionId}`;
|
|
147
|
+
|
|
142
148
|
ws.on("message", async (data) =>
|
|
143
149
|
{
|
|
144
150
|
const incomingMessage = typeof data === "string" ? data : data.toString("utf8");
|
|
@@ -360,3 +366,5 @@ module.exports = class HttpRunner extends events.EventEmitter
|
|
|
360
366
|
});
|
|
361
367
|
}
|
|
362
368
|
};
|
|
369
|
+
|
|
370
|
+
Object.assign(module.exports, require('./wsPostbackFunction'));
|
package/src/wsAdapter.js
CHANGED
|
@@ -199,7 +199,7 @@ module.exports = {
|
|
|
199
199
|
}
|
|
200
200
|
catch(e)
|
|
201
201
|
{
|
|
202
|
-
|
|
202
|
+
data.payload = incomingMessage;
|
|
203
203
|
}
|
|
204
204
|
}
|
|
205
205
|
|
|
@@ -207,20 +207,18 @@ module.exports = {
|
|
|
207
207
|
|
|
208
208
|
request.method = '';
|
|
209
209
|
request.headers = {};
|
|
210
|
-
request.name = data.name || undefined;
|
|
210
|
+
request.name = data.name || data?.action || undefined;
|
|
211
211
|
request.query = data.query || {};
|
|
212
212
|
request.params = data.params || {};
|
|
213
213
|
request.payload = data.payload || undefined;
|
|
214
214
|
|
|
215
|
-
|
|
215
|
+
for (const key of Object.keys(data.headers || {}))
|
|
216
216
|
{
|
|
217
|
-
|
|
218
|
-
{
|
|
219
|
-
request.headers[key.toLowerCase()] = data.headers[key];
|
|
220
|
-
}
|
|
217
|
+
request.headers[key.toLowerCase()] = data.headers[key];
|
|
221
218
|
}
|
|
222
219
|
|
|
223
220
|
request.headers['connection-id'] = ws.connectionId;
|
|
221
|
+
request.headers['postback-url'] = ws.postbackUrl;
|
|
224
222
|
request.headers['push-function'] = async (data)=>await this.write(ws, data);
|
|
225
223
|
|
|
226
224
|
request.headers['sec-websocket-protocol'] = ws.protocol
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const ErrorResponse = require('configurapi').ErrorResponse;
|
|
2
|
+
|
|
3
|
+
async function wsPostbackFunction(endpoint, response)
|
|
4
|
+
{
|
|
5
|
+
let message = response;
|
|
6
|
+
|
|
7
|
+
if(message?.statusCode && message?.headers)
|
|
8
|
+
{
|
|
9
|
+
message = message.body;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let resp = await fetch(endpoint, {
|
|
13
|
+
method: 'POST',
|
|
14
|
+
headers: response.headers || {},
|
|
15
|
+
body: typeof message === 'object' ? JSON.stringify(message) : message
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
if(resp.status >= 400)
|
|
19
|
+
{
|
|
20
|
+
const contentType = resp.headers.get("content-type") || "";
|
|
21
|
+
let errorBody = undefined;
|
|
22
|
+
|
|
23
|
+
try
|
|
24
|
+
{
|
|
25
|
+
errorBody = contentType.includes("application/json") ? await resp.json() : await resp.text();
|
|
26
|
+
}
|
|
27
|
+
catch(e)
|
|
28
|
+
{
|
|
29
|
+
errorBody = await resp.text().catch(() => undefined);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
throw new ErrorResponse(errorBody, resp.status);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = { wsPostbackFunction };
|