configurapi-handler-ws 1.0.0 → 1.1.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 +39 -0
- package/package.json +7 -4
- package/src/index.d.ts +4 -0
- package/src/index.js +4 -2
- package/src/postbackFunction.d.ts +3 -0
- package/src/postbackFunction.js +208 -0
- package/src/postbackResponse.d.ts +9 -0
- package/src/postbackResponse.js +15 -0
- package/src/webSocketClient.d.ts +49 -0
- package/src/webSocketClient.js +267 -0
- /package/{types.d.ts → src/streamResponse.d.ts} +0 -0
package/README.md
CHANGED
|
@@ -1,3 +1,42 @@
|
|
|
1
1
|
# configurapi-handler-ws
|
|
2
2
|
|
|
3
3
|
Configurapi request handlers for websocket
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
### Server side:
|
|
8
|
+
|
|
9
|
+
* If you want to stream data back to the client, return a `StreamResponse` with a readable stream.
|
|
10
|
+
* Use pushbackFunction(postbackFunction: Function, response: PostbackResponse) to send a response to the postback endpoint. The response will be wrapped in the specified postbackFunction.
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
const { pushbackFunction, PostbackResponse } = require("configurapi-handler-ws");
|
|
14
|
+
|
|
15
|
+
let callback = async (body:any, headers:Record<string,string>) =>
|
|
16
|
+
{
|
|
17
|
+
const isStream = body && typeof body === 'object' && typeof body.pipe === 'function';
|
|
18
|
+
const isBinary = typeof body === 'string' || Buffer.isBuffer(body) || body instanceof Uint8Array;
|
|
19
|
+
|
|
20
|
+
let resp = await fetch(`http://localhost:9100/@connections/${connectionId}`, {
|
|
21
|
+
method: 'POST',
|
|
22
|
+
headers,
|
|
23
|
+
body: isStream || isBinary ? body : JSON.stringify(body),
|
|
24
|
+
...(isStream ? { duplex: 'half' } : {})
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
console.log(resp.status + ' ' + resp.statusText)
|
|
28
|
+
console.log(await resp.json())
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
await pushbackFunction(callback, new PostbackResponse('body', {'content-type': 'application/json'}))
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Client side:
|
|
35
|
+
|
|
36
|
+
* Use WebSocketClient to connect to a WebSocket runner, send data over the connection, and disconnect from the runner when finished.
|
|
37
|
+
* To pass data (such as a protocol or authentication token) when establishing the WebSocket connection, provide it as the second parameter to the constructor.
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
const { WebSocketClient } = require("configurapi-handler-ws");
|
|
41
|
+
let client = new WebSocketClient(WEBSOCKET_BASE_URL, {onPush: (data:IResponse)=>console.log(data)});
|
|
42
|
+
```
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "configurapi-handler-ws",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"main": "src/index",
|
|
5
5
|
"files": [
|
|
6
6
|
"src/*",
|
|
7
|
-
"
|
|
7
|
+
"src/*.d.ts"
|
|
8
8
|
],
|
|
9
|
-
"types": "
|
|
9
|
+
"types": "src/index.d.ts",
|
|
10
10
|
"repository": {
|
|
11
11
|
"type": "git",
|
|
12
12
|
"url": "git+ssh://git@gitlab.com/mappies/configurapi-handler-ws.git"
|
|
@@ -20,5 +20,8 @@
|
|
|
20
20
|
"url": "https://gitlab.com/mappies/configurapi-handler-ws/issues"
|
|
21
21
|
},
|
|
22
22
|
"homepage": "https://gitlab.com/mappies/configurapi-handler-ws#readme",
|
|
23
|
-
"description": ""
|
|
23
|
+
"description": "",
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/node": "^25.2.3"
|
|
26
|
+
}
|
|
24
27
|
}
|
package/src/index.d.ts
ADDED
package/src/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
2
1
|
module.exports = {
|
|
3
|
-
StreamResponse: require('./
|
|
2
|
+
StreamResponse: require('./streamResponse'),
|
|
3
|
+
pushbackFunction: require('./postbackFunction'),
|
|
4
|
+
PostbackResponse: require('./postbackResponse'),
|
|
5
|
+
WebSocketClient: require('./webSocketClient')
|
|
4
6
|
};
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
const { randomUUID } = require("node:crypto");
|
|
2
|
+
const StreamResponse = require("./streamResponse");
|
|
3
|
+
|
|
4
|
+
// tiny sleep for retry/backoff
|
|
5
|
+
function sleep(ms) {
|
|
6
|
+
return new Promise(r => setTimeout(r, ms));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
module.exports = async function pushFunction(postBackFunction, response)
|
|
10
|
+
{
|
|
11
|
+
// Helper: postToConnection with light retry on throttling
|
|
12
|
+
const postJSON = async (body, headers) =>
|
|
13
|
+
{
|
|
14
|
+
try
|
|
15
|
+
{
|
|
16
|
+
if(typeof body === 'object')
|
|
17
|
+
{
|
|
18
|
+
headers['Content-Type'] = 'application/json';
|
|
19
|
+
}
|
|
20
|
+
await postBackFunction.apply(undefined, [body, headers])
|
|
21
|
+
}
|
|
22
|
+
catch(err)
|
|
23
|
+
{
|
|
24
|
+
for(let i=0; i<10; i++)
|
|
25
|
+
{
|
|
26
|
+
if (err && err.name === 'GoneException') throw err; // client disconnected
|
|
27
|
+
if (err && err.name === 'LimitExceededException')
|
|
28
|
+
{
|
|
29
|
+
await sleep(i*250 + Math.floor(Math.random() * 250));
|
|
30
|
+
try
|
|
31
|
+
{
|
|
32
|
+
await postBackFunction.apply(undefined, [body, headers])
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
catch(e){ err = e};
|
|
36
|
+
}
|
|
37
|
+
else throw err;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
await new Promise((resolve, reject)=>
|
|
43
|
+
{
|
|
44
|
+
try
|
|
45
|
+
{
|
|
46
|
+
if(response instanceof StreamResponse)
|
|
47
|
+
{
|
|
48
|
+
const baseHeaders = {
|
|
49
|
+
...response.headers
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const stream = response.body;
|
|
53
|
+
const streamId = baseHeaders?.['message-id'] || randomUUID();
|
|
54
|
+
|
|
55
|
+
// Guard: need a readable stream
|
|
56
|
+
if(!stream || typeof stream.on !== 'function')
|
|
57
|
+
{
|
|
58
|
+
return reject(new Error('StreamResponse.stream is not a readable stream.'));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// API Gateway WS frame hard limit is 32 KB; keep batches small.
|
|
62
|
+
const BATCH_BYTES = 1024;
|
|
63
|
+
let bucket = '';
|
|
64
|
+
let bucketBytes = 0;
|
|
65
|
+
|
|
66
|
+
let flushTimer = undefined;
|
|
67
|
+
|
|
68
|
+
const frames = []; //{statusCode:number, body:any, headers: Record<string, string>}[]
|
|
69
|
+
let sendingPromise = undefined; //Promise<void>|undefined
|
|
70
|
+
|
|
71
|
+
async function drain()// Promise<void>
|
|
72
|
+
{
|
|
73
|
+
if (sendingPromise) return sendingPromise; // wait for whichever send is active
|
|
74
|
+
|
|
75
|
+
sendingPromise = (async () =>
|
|
76
|
+
{
|
|
77
|
+
try
|
|
78
|
+
{
|
|
79
|
+
while (frames.length)
|
|
80
|
+
{
|
|
81
|
+
const resp = frames.shift();
|
|
82
|
+
if(resp)
|
|
83
|
+
{
|
|
84
|
+
await postJSON(resp.body, resp.headers);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
finally
|
|
89
|
+
{
|
|
90
|
+
sendingPromise = undefined;
|
|
91
|
+
}
|
|
92
|
+
})();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function enqueue(resp)
|
|
96
|
+
{
|
|
97
|
+
frames.push(resp);
|
|
98
|
+
// fire-and-forget; finish() will await completion
|
|
99
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
100
|
+
await drain();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// snapshot & reset BEFORE any await to avoid duplicate sends
|
|
104
|
+
async function flushChunkIfAny()
|
|
105
|
+
{
|
|
106
|
+
if (bucketBytes === 0) return;
|
|
107
|
+
|
|
108
|
+
const payload = bucket; // snapshot current buffer
|
|
109
|
+
bucket = '';
|
|
110
|
+
bucketBytes = 0;
|
|
111
|
+
const headers = { 'message-id': streamId, 'x-stream': 'chunk', 'x-stream-id': streamId, 'Content-Type': 'text/plain' };
|
|
112
|
+
const resp = {body: payload, statusCode: 200, headers};
|
|
113
|
+
await enqueue(resp);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function scheduleFlush(delayMs)
|
|
117
|
+
{
|
|
118
|
+
if (flushTimer) return;
|
|
119
|
+
|
|
120
|
+
flushTimer = setTimeout(() => {
|
|
121
|
+
flushTimer = undefined;
|
|
122
|
+
flushChunkIfAny()
|
|
123
|
+
.then(() =>
|
|
124
|
+
{
|
|
125
|
+
// re-arm only if there is still pending data *and* we didn't reject
|
|
126
|
+
if (bucketBytes > 0) scheduleFlush(50);
|
|
127
|
+
})
|
|
128
|
+
.catch(err =>
|
|
129
|
+
{
|
|
130
|
+
try { cleanup(); } catch {}
|
|
131
|
+
|
|
132
|
+
// Propagate to the outer promise so Lambda sees the failure
|
|
133
|
+
reject(err);
|
|
134
|
+
});
|
|
135
|
+
}, delayMs);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const onData = (chunk) =>
|
|
139
|
+
{
|
|
140
|
+
const text = Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk);
|
|
141
|
+
bucket += text;
|
|
142
|
+
bucketBytes += Buffer.byteLength(text);
|
|
143
|
+
|
|
144
|
+
if (bucketBytes >= BATCH_BYTES || text.includes('\n'))
|
|
145
|
+
{
|
|
146
|
+
scheduleFlush(0); // flush ASAP; no overlap due to queue
|
|
147
|
+
}
|
|
148
|
+
else
|
|
149
|
+
{
|
|
150
|
+
scheduleFlush(50);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// finish by flushing tail, enqueuing 'done', and awaiting queue
|
|
155
|
+
async function finish()
|
|
156
|
+
{
|
|
157
|
+
if (flushTimer) { clearTimeout(flushTimer); flushTimer = undefined; }
|
|
158
|
+
|
|
159
|
+
await flushChunkIfAny();
|
|
160
|
+
|
|
161
|
+
const headers = { 'message-id': streamId, 'x-stream': 'done', 'x-stream-id': streamId };
|
|
162
|
+
|
|
163
|
+
await enqueue({body: '', statusCode: 204, headers});
|
|
164
|
+
|
|
165
|
+
await drain(); // wait until all frames are sent
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const cleanup = () =>
|
|
169
|
+
{
|
|
170
|
+
try
|
|
171
|
+
{
|
|
172
|
+
if (flushTimer) { clearTimeout(flushTimer); flushTimer = undefined; }
|
|
173
|
+
stream.removeListener('data', onData);
|
|
174
|
+
stream.removeListener('end', onEnd);
|
|
175
|
+
stream.removeListener('error', onErr);
|
|
176
|
+
} catch(_) {}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// use finish() so we never resolve before queue is empty
|
|
180
|
+
const onEnd = () => { finish().then(() => { cleanup(); resolve(undefined); }).catch(err => { cleanup(); reject(err); }); };
|
|
181
|
+
|
|
182
|
+
const onErr = (e) =>
|
|
183
|
+
{
|
|
184
|
+
cleanup();
|
|
185
|
+
return reject(e);
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
stream.on('data', onData);
|
|
189
|
+
stream.once('end', onEnd);
|
|
190
|
+
stream.once('error', onErr);
|
|
191
|
+
|
|
192
|
+
// Important: stop here; promise resolves on 'end' or rejects on 'error'
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
else
|
|
196
|
+
{
|
|
197
|
+
// Non-stream response: send as-is
|
|
198
|
+
postJSON(response.body, response.headers)
|
|
199
|
+
.then(() => resolve(undefined))
|
|
200
|
+
.catch((err) => reject(err));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
catch(e)
|
|
204
|
+
{
|
|
205
|
+
reject(e)
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const Response = require('configurapi').Response;
|
|
2
|
+
|
|
3
|
+
module.exports = class PostbackResponse extends Response
|
|
4
|
+
{
|
|
5
|
+
body;
|
|
6
|
+
headers;
|
|
7
|
+
|
|
8
|
+
constructor(body, headers = {})
|
|
9
|
+
{
|
|
10
|
+
this.body = body;
|
|
11
|
+
this.headers = headers;
|
|
12
|
+
|
|
13
|
+
super(body, 200, headers);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export interface ClientOptions
|
|
2
|
+
{
|
|
3
|
+
/** Reject if no reply arrives within this many ms (default 15000) */
|
|
4
|
+
replyTimeoutMs?: number;
|
|
5
|
+
|
|
6
|
+
/** Header name to carry the correlation id (default "message-id") */
|
|
7
|
+
idHeaderName?: string;
|
|
8
|
+
|
|
9
|
+
/** If a response arrives without an id, deliver it here (or ignore if omitted) */
|
|
10
|
+
onPush?: (data: {
|
|
11
|
+
body: any;
|
|
12
|
+
statusCode: number;
|
|
13
|
+
headers: Record<string, string>;
|
|
14
|
+
}) => Promise<void>;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A single string or an array of strings representing the sub-protocol(s)
|
|
18
|
+
* that the client would like to use, in order of preference.
|
|
19
|
+
* If it is omitted, an empty array is used by default, i.e., [].
|
|
20
|
+
*/
|
|
21
|
+
protocols?: () => string | string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
declare class WebSocketClient
|
|
25
|
+
{
|
|
26
|
+
constructor(endpoint: string, opts?: ClientOptions);
|
|
27
|
+
|
|
28
|
+
readonly isOpen: boolean;
|
|
29
|
+
|
|
30
|
+
connecting: boolean;
|
|
31
|
+
|
|
32
|
+
connect(): Promise<void>;
|
|
33
|
+
|
|
34
|
+
close(code?: number, reason?: string): Promise<void>;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Send IRequest and resolve when an IResponse arrives
|
|
38
|
+
* with the same messageId
|
|
39
|
+
*/
|
|
40
|
+
send<T>(
|
|
41
|
+
req: any,
|
|
42
|
+
opts?: {
|
|
43
|
+
timeoutMs?: number;
|
|
44
|
+
onChunk?: (chunk: string) => Promise<void>;
|
|
45
|
+
onDone?: () => Promise<void>;
|
|
46
|
+
onError?: (e: Error) => Promise<void>;
|
|
47
|
+
}
|
|
48
|
+
): Promise<T>;
|
|
49
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
const MessageIdHeaderName = 'message-id';
|
|
2
|
+
|
|
3
|
+
module.exports = class WebSocketClient
|
|
4
|
+
{
|
|
5
|
+
url;
|
|
6
|
+
ws;
|
|
7
|
+
connecting = false;
|
|
8
|
+
replyTimeoutMs;
|
|
9
|
+
protocols;
|
|
10
|
+
onPush;
|
|
11
|
+
|
|
12
|
+
// Correlate by messageId: id -> waiter
|
|
13
|
+
inflight = new Map();
|
|
14
|
+
|
|
15
|
+
constructor(endpoint, opts = {})
|
|
16
|
+
{
|
|
17
|
+
this.url = endpoint;
|
|
18
|
+
this.replyTimeoutMs = opts.replyTimeoutMs ?? 27000;
|
|
19
|
+
this.onPush = opts.onPush;
|
|
20
|
+
this.protocols = opts.protocols;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
get isOpen()
|
|
24
|
+
{
|
|
25
|
+
return !!this.ws && this.ws.readyState === this.ws.OPEN;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async connect()
|
|
29
|
+
{
|
|
30
|
+
if (this.isOpen || this.connecting) return;
|
|
31
|
+
this.connecting = true;
|
|
32
|
+
|
|
33
|
+
await new Promise((resolve, reject) =>
|
|
34
|
+
{
|
|
35
|
+
const protocols = this.protocols?.();
|
|
36
|
+
this.ws = new WebSocket(this.url, protocols);
|
|
37
|
+
|
|
38
|
+
this.ws.addEventListener("open", () =>
|
|
39
|
+
{
|
|
40
|
+
this.connecting = false;
|
|
41
|
+
resolve();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
this.ws.addEventListener("message", async (ev) =>
|
|
45
|
+
{
|
|
46
|
+
try
|
|
47
|
+
{
|
|
48
|
+
await this.handleResponse(ev.data);
|
|
49
|
+
}
|
|
50
|
+
catch(error)
|
|
51
|
+
{
|
|
52
|
+
reject(error)
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
this.ws.addEventListener("close", () =>
|
|
57
|
+
{
|
|
58
|
+
this.connecting = false;
|
|
59
|
+
// Fail all pending calls
|
|
60
|
+
for (const [id, waiter] of this.inflight) {
|
|
61
|
+
clearTimeout(waiter.timer);
|
|
62
|
+
waiter.reject(new Error(`WebSocket closed before response (id=${id})`));
|
|
63
|
+
}
|
|
64
|
+
this.inflight.clear();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
this.ws.addEventListener("error", (err) =>
|
|
68
|
+
{
|
|
69
|
+
this.connecting = false;
|
|
70
|
+
if (!this.isOpen) reject(err);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async close(code, reason)
|
|
76
|
+
{
|
|
77
|
+
if (!this.ws) return;
|
|
78
|
+
await new Promise((resolve) =>
|
|
79
|
+
{
|
|
80
|
+
this.ws?.addEventListener("close", () => resolve(), { once: true });
|
|
81
|
+
try { this.ws.close(code, reason); } catch { resolve(); }
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Send IRequest and resolve when an IResponse arrives with the same messageId */
|
|
86
|
+
async send(req, opts)
|
|
87
|
+
{
|
|
88
|
+
if (!this.isOpen) await this.connect();
|
|
89
|
+
|
|
90
|
+
const id = this.ensureMessageId(req);
|
|
91
|
+
|
|
92
|
+
// Set up the waiter BEFORE sending (avoid fast-response race)
|
|
93
|
+
const timeoutMs = opts?.timeoutMs ?? this.replyTimeoutMs;
|
|
94
|
+
const promise = new Promise((resolve, reject) =>
|
|
95
|
+
{
|
|
96
|
+
let timer = null;
|
|
97
|
+
const waiter = { resolve, reject, timer: null, onChunk: opts?.onChunk, onDone: opts?.onDone, onError: opts?.onError, timedOut: false, muted: false };
|
|
98
|
+
if (timeoutMs > 0)
|
|
99
|
+
{
|
|
100
|
+
timer = setTimeout(() => {
|
|
101
|
+
waiter.timedOut = true;
|
|
102
|
+
|
|
103
|
+
waiter.onChunk?.("\n\nI've reached my response time limit and couldn't complete my full answer. Could you let me know which part you'd like me to focus on?");
|
|
104
|
+
// Reject the promise so callers can show a warning, but keep waiter so 'done' can clean up
|
|
105
|
+
reject(new Error(`Timed out waiting for response (messageId=${id})`));
|
|
106
|
+
}, timeoutMs);
|
|
107
|
+
}
|
|
108
|
+
waiter.timer = timer;
|
|
109
|
+
|
|
110
|
+
this.inflight.set(id, waiter);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
try
|
|
114
|
+
{
|
|
115
|
+
this.ws.send(JSON.stringify(req));
|
|
116
|
+
}
|
|
117
|
+
catch (e)
|
|
118
|
+
{
|
|
119
|
+
const waiter = this.inflight.get(id);
|
|
120
|
+
if (waiter) { clearTimeout(waiter.timer); this.inflight.delete(id); }
|
|
121
|
+
throw e;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return promise;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async handleResponse(text)
|
|
128
|
+
{
|
|
129
|
+
let msg;
|
|
130
|
+
try { msg = JSON.parse(text); } catch { msg = { headers: {}, payload: text }; }
|
|
131
|
+
|
|
132
|
+
const rawFlag = this.getHeaderCaseInsensitive(msg.headers, 'x-stream');
|
|
133
|
+
const streamFlag = rawFlag ? String(rawFlag).toLowerCase() : '';
|
|
134
|
+
|
|
135
|
+
if (streamFlag)
|
|
136
|
+
{
|
|
137
|
+
// correlate by message-id; fallback is applied only for CHUNK below
|
|
138
|
+
const id = this.getHeaderCaseInsensitive(msg.headers, MessageIdHeaderName);
|
|
139
|
+
const waiter = id ? this.inflight.get(id) : undefined;
|
|
140
|
+
|
|
141
|
+
if(msg?.statusCode > 299)
|
|
142
|
+
{
|
|
143
|
+
const error = new Error(`${msg.message} - (${msg.statusCode})`)
|
|
144
|
+
|
|
145
|
+
if(waiter)
|
|
146
|
+
{
|
|
147
|
+
await waiter.onError?.(error);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (streamFlag === 'chunk')
|
|
152
|
+
{
|
|
153
|
+
if (waiter?.timedOut) return;
|
|
154
|
+
|
|
155
|
+
if (waiter)
|
|
156
|
+
{
|
|
157
|
+
await waiter.onChunk?.(msg.body ?? msg.payload ?? msg ?? '');
|
|
158
|
+
}
|
|
159
|
+
else if (this.onPush)
|
|
160
|
+
{
|
|
161
|
+
this.onPush(msg);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
else if (streamFlag === 'done' || streamFlag === 'error')
|
|
165
|
+
{
|
|
166
|
+
if (!waiter)
|
|
167
|
+
{
|
|
168
|
+
if (this.onPush) this.onPush(msg);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
await waiter.onChunk?.(msg.body ?? msg.payload ?? '');
|
|
173
|
+
await waiter.onDone?.();
|
|
174
|
+
|
|
175
|
+
clearTimeout(waiter.timer);
|
|
176
|
+
|
|
177
|
+
if(id)
|
|
178
|
+
{
|
|
179
|
+
this.inflight.delete(id);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// We alaready rejected if timed out.
|
|
183
|
+
if(waiter.timedOut) return
|
|
184
|
+
|
|
185
|
+
if (streamFlag === 'error')
|
|
186
|
+
{
|
|
187
|
+
waiter.reject(msg);
|
|
188
|
+
}
|
|
189
|
+
else
|
|
190
|
+
{
|
|
191
|
+
waiter.resolve(msg);
|
|
192
|
+
}
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Unknown x-stream value: treat as push to avoid nuking inflight state
|
|
197
|
+
if (this.onPush) this.onPush(msg);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const id = this.getHeaderCaseInsensitive(msg.headers, MessageIdHeaderName);
|
|
202
|
+
|
|
203
|
+
if (msg?.statusCode && msg.statusCode !== 202 && id && this.inflight.has(id))
|
|
204
|
+
{
|
|
205
|
+
const waiter = this.inflight.get(id);
|
|
206
|
+
|
|
207
|
+
if(msg.statusCode > 299)
|
|
208
|
+
{
|
|
209
|
+
const error = new Error(`${msg.message} - (${msg.statusCode})`)
|
|
210
|
+
if(waiter)
|
|
211
|
+
{
|
|
212
|
+
await waiter.onError?.(error);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
clearTimeout(waiter.timer);
|
|
217
|
+
|
|
218
|
+
this.inflight.delete(id);
|
|
219
|
+
|
|
220
|
+
if(waiter.timedOut) return // Already rejected
|
|
221
|
+
waiter.resolve(msg);
|
|
222
|
+
}
|
|
223
|
+
else
|
|
224
|
+
{
|
|
225
|
+
if(msg?.statusCode > 299)
|
|
226
|
+
{
|
|
227
|
+
const error = new Error(`${msg.message} - (${msg.statusCode})`)
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// No id or unknown id. Treat as push/unsolicited.
|
|
232
|
+
if (this.onPush) this.onPush(msg);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
ensureMessageId(request)
|
|
237
|
+
{
|
|
238
|
+
if(request === undefined) return '';
|
|
239
|
+
|
|
240
|
+
if(request.headers === undefined)
|
|
241
|
+
{
|
|
242
|
+
request.headers = {}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
let id = this.getHeaderCaseInsensitive(request.headers, MessageIdHeaderName);
|
|
246
|
+
if(!id)
|
|
247
|
+
{
|
|
248
|
+
id = crypto.randomUUID()
|
|
249
|
+
|
|
250
|
+
request.headers[MessageIdHeaderName] = id;
|
|
251
|
+
}
|
|
252
|
+
return id;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
getHeaderCaseInsensitive(headers, name)
|
|
256
|
+
{
|
|
257
|
+
if (!headers || typeof headers !== "object") return undefined;
|
|
258
|
+
const target = name.toLowerCase();
|
|
259
|
+
|
|
260
|
+
for (const k of Object.keys(headers))
|
|
261
|
+
{
|
|
262
|
+
if (k.toLowerCase() === target) return String(headers[k]);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return undefined;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
File without changes
|