configurapi-runner-ws 1.0.0 → 1.0.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.
Files changed (2) hide show
  1. package/package.json +2 -1
  2. package/src/wsAdapter.js +114 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "configurapi-runner-ws",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Websocket runner for configurapi.",
5
5
  "bin": {
6
6
  "configurapi-runner-ws": "src/app.mjs"
@@ -29,6 +29,7 @@
29
29
  "dependencies": {
30
30
  "commander": "^14.0.1",
31
31
  "configurapi": "^1.9.0",
32
+ "configurapi-handler-ws": "^1.0.0",
32
33
  "dotenv": "^17.2.3",
33
34
  "one-liner": "^1.3.0",
34
35
  "ws": "^8.18.3"
package/src/wsAdapter.js CHANGED
@@ -1,5 +1,8 @@
1
1
  const Configurapi = require('configurapi');
2
2
  const { WebSocket } = require('ws');
3
+ const StreamResponse = require('configurapi-handler-ws').StreamResponse;
4
+ const Response = require('configurapi').Response;
5
+ const { randomUUID } = require("node:crypto");
3
6
 
4
7
  module.exports = {
5
8
  write: async function(ws, message)
@@ -11,16 +14,120 @@ module.exports = {
11
14
  {
12
15
  try
13
16
  {
14
- ws.send(JSON.stringify(message), (err) => {
15
- if(err)
17
+ if(message instanceof StreamResponse)
18
+ {
19
+ const stream = message.body;
20
+ const streamId = randomUUID();
21
+
22
+ // Guard: need a readable stream
23
+ if(!stream || typeof stream.on !== 'function')
16
24
  {
17
- return reject(err);
25
+ return reject(new Error('StreamResponse.stream is not a readable stream.'));
18
26
  }
19
- else
27
+
28
+ const HIGH_WATER = 64 * 1024;
29
+ const BATCH_BYTES = 1024;
30
+ let bucket = '';
31
+ let bucketBytes = 0;
32
+ let flushTimer = undefined;
33
+
34
+ const flush = () =>
20
35
  {
21
- return resolve();
22
- }
23
- });
36
+ if (bucketBytes === 0) return;
37
+
38
+ // Backpressure: if kernel buffer is large, pause until it drops.
39
+ if (ws.bufferedAmount > HIGH_WATER)
40
+ {
41
+ stream.pause();
42
+ const check = () => {
43
+ if (ws.readyState !== WebSocket.OPEN) return cleanup();
44
+ if (ws.bufferedAmount <= HIGH_WATER) return stream.resume();
45
+ setTimeout(check, 100);
46
+ };
47
+ setTimeout(check, 100);
48
+ }
49
+
50
+ const payload = { chunk: bucket };
51
+ const headers = { 'X-Stream': 'chunk', 'X-Stream-ID': streamId, 'Content-Type': 'application/json' };
52
+ const response = new Response(bucket, 200, headers);
53
+
54
+ ws.send(JSON.stringify(response), (err) =>
55
+ {
56
+ if (err) { cleanup(); return reject(err); }
57
+ });
58
+
59
+ bucket = '';
60
+ bucketBytes = 0;
61
+ };
62
+
63
+ const cleanup = () =>
64
+ {
65
+ try {
66
+ if (flushTimer) { clearInterval(flushTimer); flushTimer = undefined; }
67
+ stream.removeListener('data', onData);
68
+ stream.removeListener('end', onEnd);
69
+ stream.removeListener('error', onErr);
70
+ } catch(_) {}
71
+ };
72
+
73
+ const onData = (chunk) =>
74
+ {
75
+ if (ws.readyState !== WebSocket.OPEN) { cleanup(); return; }
76
+
77
+ const text = Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk);
78
+ bucket += text;
79
+ bucketBytes += Buffer.byteLength(text);
80
+
81
+ if (bucketBytes >= BATCH_BYTES || text.includes('\n')) {
82
+ flush();
83
+ }
84
+
85
+ if (!flushTimer) {
86
+ flushTimer = setInterval(() => { if (bucketBytes > 0) flush(); }, 50);
87
+ }
88
+ };
89
+
90
+ const onEnd = () =>
91
+ {
92
+ // final batch before done
93
+ if (bucketBytes > 0) flush();
94
+
95
+ if (ws.readyState === WebSocket.OPEN)
96
+ {
97
+ const headers = { 'X-Stream-Id': streamId, 'X-Stream': 'done' };
98
+ const response = new Response('', 204, headers);
99
+
100
+ ws.send(JSON.stringify(response), (err) =>
101
+ {
102
+ cleanup();
103
+ return err ? reject(err) : resolve();
104
+ });
105
+ }
106
+ else
107
+ {
108
+ cleanup();
109
+ return resolve();
110
+ }
111
+ };
112
+
113
+ const onErr = (e) =>
114
+ {
115
+ cleanup();
116
+ return reject(e);
117
+ };
118
+
119
+ stream.on('data', onData);
120
+ stream.on('end', onEnd);
121
+ stream.on('error', onErr);
122
+
123
+ // Important: stop here; promise resolves on 'end' or rejects on 'error'
124
+ return;
125
+ }
126
+ else
127
+ {
128
+
129
+ ws.send(JSON.stringify(message), (err) => err ? reject(err) : resolve());
130
+ }
24
131
  }
25
132
  catch(e)
26
133
  {