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 CHANGED
@@ -1,14 +1,264 @@
1
1
  # configurapi-runner-ws
2
2
 
3
- - Use `Event` for an incoming data.
4
- - User `Response` for a regular response data.
5
- - Use `StreamResponse` from `configurapi-handler-ws` for a stream response.
6
- - With a stream response, data will be delivered from the server in chunks.
7
- - A check will have `x-stream: chunk` header.
8
- - The last chunk will have `x-stream: done` header.
9
- - Check `x-stream-id: $id` header for the stream ID. A chunk belonging to the same stream will have the same stream ID.
10
- - connectionId is available at `event.request.headers.connectionId`
11
- - `on_connect` and `on_disconnect` will be called when a new connection is made or closed.
12
- - Use `Request.headers['push-function']` to send an out-of-band message. The function is async and accepts an object to send back to the caller.
13
- - Use `on_connect` to perform authorization.
14
- - `Sec-WebSocket-Protocol` is automatically returned for `on_connect` events.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "configurapi-runner-ws",
3
- "version": "1.14.0",
3
+ "version": "1.16.0",
4
4
  "description": "Websocket runner for configurapi.",
5
5
  "bin": {
6
6
  "configurapi-runner-ws": "src/app.mjs"
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
- throw new SyntaxError(`Invalid JSON payload: '${incomingMessage}'`);
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
- if(data.headers)
215
+ for (const key of Object.keys(data.headers || {}))
216
216
  {
217
- for(let key of Object.keys(data.headers))
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 };