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.
- package/package.json +2 -1
- 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.
|
|
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
|
-
|
|
15
|
-
|
|
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(
|
|
25
|
+
return reject(new Error('StreamResponse.stream is not a readable stream.'));
|
|
18
26
|
}
|
|
19
|
-
|
|
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
|
-
|
|
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
|
{
|