@milaboratories/pl-client 2.16.12 → 2.16.13
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/dist/core/driver.cjs +1 -1
- package/dist/core/driver.cjs.map +1 -1
- package/dist/core/driver.js +1 -1
- package/dist/core/driver.js.map +1 -1
- package/dist/core/ll_client.cjs +17 -7
- package/dist/core/ll_client.cjs.map +1 -1
- package/dist/core/ll_client.d.ts.map +1 -1
- package/dist/core/ll_client.js +17 -7
- package/dist/core/ll_client.js.map +1 -1
- package/dist/core/websocket_stream.cjs +126 -129
- package/dist/core/websocket_stream.cjs.map +1 -1
- package/dist/core/websocket_stream.d.ts +29 -22
- package/dist/core/websocket_stream.d.ts.map +1 -1
- package/dist/core/websocket_stream.js +127 -130
- package/dist/core/websocket_stream.js.map +1 -1
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.cjs +136 -0
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.cjs.map +1 -1
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.d.ts +75 -1
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.d.ts.map +1 -1
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.js +135 -1
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.js.map +1 -1
- package/dist/proto-rest/index.cjs +16 -2
- package/dist/proto-rest/index.cjs.map +1 -1
- package/dist/proto-rest/index.d.ts.map +1 -1
- package/dist/proto-rest/index.js +16 -2
- package/dist/proto-rest/index.js.map +1 -1
- package/package.json +6 -6
- package/src/core/driver.ts +1 -1
- package/src/core/ll_client.ts +25 -8
- package/src/core/websocket_stream.test.ts +19 -8
- package/src/core/websocket_stream.ts +154 -166
- package/src/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.ts +179 -1
- package/src/proto-rest/index.ts +17 -2
|
@@ -1,233 +1,210 @@
|
|
|
1
|
-
import { WebSocket } from 'undici';
|
|
2
|
-
import {
|
|
3
|
-
TxAPI_ClientMessage as ClientMessageType,
|
|
4
|
-
TxAPI_ServerMessage as ServerMessageType,
|
|
5
|
-
} from '../proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api';
|
|
1
|
+
import { WebSocket, type WebSocketInit, type Dispatcher, ErrorEvent } from 'undici';
|
|
6
2
|
import type { BiDiStream } from './abstract_stream';
|
|
7
3
|
import Denque from 'denque';
|
|
8
4
|
import type { RetryConfig } from '../helpers/retry_strategy';
|
|
9
5
|
import { RetryStrategy } from '../helpers/retry_strategy';
|
|
10
6
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
interface QueuedMessage {
|
|
14
|
-
message: ClientMessageType;
|
|
7
|
+
interface QueuedMessage<InType extends object> {
|
|
8
|
+
message: InType;
|
|
15
9
|
resolve: () => void;
|
|
16
10
|
reject: (error: Error) => void;
|
|
17
11
|
}
|
|
18
12
|
|
|
19
|
-
interface ResponseResolver {
|
|
20
|
-
resolve: (value: IteratorResult<
|
|
13
|
+
interface ResponseResolver<OutType extends object> {
|
|
14
|
+
resolve: (value: IteratorResult<OutType>) => void;
|
|
21
15
|
reject: (error: Error) => void;
|
|
22
16
|
}
|
|
23
17
|
|
|
18
|
+
enum ConnectionState {
|
|
19
|
+
NEW = 0,
|
|
20
|
+
CONNECTING = 1,
|
|
21
|
+
CONNECTED = 2,
|
|
22
|
+
CLOSING = 3,
|
|
23
|
+
CLOSED = 4,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type WSStreamOptions<ClientMsg extends object, ServerMsg extends object> = {
|
|
27
|
+
abortSignal?: AbortSignal;
|
|
28
|
+
|
|
29
|
+
dispatcher?: Dispatcher;
|
|
30
|
+
jwtToken?: string;
|
|
31
|
+
retryConfig?: Partial<RetryConfig>;
|
|
32
|
+
|
|
33
|
+
onComplete?: (stream: WebSocketBiDiStream<ClientMsg, ServerMsg>) => void | Promise<void>;
|
|
34
|
+
};
|
|
35
|
+
|
|
24
36
|
/**
|
|
25
37
|
* WebSocket-based bidirectional stream implementation for LLTransaction.
|
|
26
38
|
* Implements BiDiStream interface which is compatible with DuplexStreamingCall.
|
|
27
39
|
*/
|
|
28
|
-
export class WebSocketBiDiStream implements BiDiStream<
|
|
40
|
+
export class WebSocketBiDiStream<ClientMsg extends object, ServerMsg extends object> implements BiDiStream<ClientMsg, ServerMsg> {
|
|
29
41
|
// Connection
|
|
30
42
|
private ws: WebSocket | null = null;
|
|
31
|
-
private connectionState: ConnectionState =
|
|
32
|
-
private readonly url: string;
|
|
33
|
-
private readonly jwtToken?: string;
|
|
34
|
-
private readonly abortSignal: AbortSignal;
|
|
43
|
+
private connectionState: ConnectionState = ConnectionState.NEW;
|
|
35
44
|
private readonly reconnection: RetryStrategy;
|
|
36
45
|
|
|
37
46
|
// Send management
|
|
38
|
-
private readonly sendQueue = new Denque<QueuedMessage
|
|
47
|
+
private readonly sendQueue = new Denque<QueuedMessage<ClientMsg>>();
|
|
39
48
|
private sendCompleted = false;
|
|
49
|
+
private readonly onComplete: (stream: WebSocketBiDiStream<ClientMsg, ServerMsg>) => void | Promise<void>;
|
|
40
50
|
|
|
41
51
|
// Response management
|
|
42
|
-
private readonly responseQueue = new Denque<
|
|
43
|
-
private responseResolvers: ResponseResolver[] = [];
|
|
52
|
+
private readonly responseQueue = new Denque<ServerMsg>();
|
|
53
|
+
private responseResolvers: ResponseResolver<ServerMsg>[] = [];
|
|
44
54
|
|
|
45
55
|
// Error tracking
|
|
46
|
-
private
|
|
56
|
+
private lastError?: Error;
|
|
47
57
|
|
|
48
58
|
// === Public API ===
|
|
49
59
|
|
|
50
60
|
public readonly requests = {
|
|
51
|
-
send: async (message:
|
|
52
|
-
this.
|
|
53
|
-
return this.enqueueSend(message);
|
|
61
|
+
send: async (message: ClientMsg): Promise<void> => {
|
|
62
|
+
return await this.enqueueSend(message);
|
|
54
63
|
},
|
|
55
64
|
|
|
56
65
|
complete: async (): Promise<void> => {
|
|
57
66
|
if (this.sendCompleted) return;
|
|
58
67
|
|
|
68
|
+
await this.drainSendQueue(); // ensure we sent all already queued messages before closing the stream
|
|
69
|
+
try {
|
|
70
|
+
await this.onComplete(this); // custom onComplete may send additional messages
|
|
71
|
+
} catch (_: unknown) {
|
|
72
|
+
// When 'complete' gets called concurrently with connection break or over a broken
|
|
73
|
+
// transaction stream (server decided it should drop transaction), server would close
|
|
74
|
+
// connection anyway on its end. We can safely ignore error here and just continue working.
|
|
75
|
+
}
|
|
59
76
|
this.sendCompleted = true;
|
|
60
|
-
await this.drainSendQueue();
|
|
61
|
-
this.closeConnection();
|
|
62
77
|
},
|
|
63
78
|
};
|
|
64
79
|
|
|
65
|
-
public readonly responses: AsyncIterable<
|
|
80
|
+
public readonly responses: AsyncIterable<ServerMsg> = {
|
|
66
81
|
[Symbol.asyncIterator]: () => this.createResponseIterator(),
|
|
67
82
|
};
|
|
68
83
|
|
|
84
|
+
public close(): void {
|
|
85
|
+
this.reconnection.cancel();
|
|
86
|
+
|
|
87
|
+
if (this.connectionState < ConnectionState.CONNECTED) {
|
|
88
|
+
// Never reached CONNECTED state. ws.close() will never trigger 'close' event.
|
|
89
|
+
this.ws?.close();
|
|
90
|
+
this.onClose();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!this.progressConnectionState(ConnectionState.CLOSING)) return;
|
|
95
|
+
this.ws!.close();
|
|
96
|
+
}
|
|
97
|
+
|
|
69
98
|
constructor(
|
|
70
|
-
url: string,
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
99
|
+
private readonly url: string,
|
|
100
|
+
private readonly serializeClientMessage: (message: ClientMsg) => Uint8Array,
|
|
101
|
+
private readonly parseServerMessage: (data: Uint8Array) => ServerMsg,
|
|
102
|
+
private readonly options: WSStreamOptions<ClientMsg, ServerMsg> = {},
|
|
74
103
|
) {
|
|
75
|
-
this.
|
|
76
|
-
this.jwtToken = jwtToken;
|
|
77
|
-
this.abortSignal = abortSignal;
|
|
104
|
+
this.onComplete = this.options.onComplete ?? ((stream) => stream.close());
|
|
78
105
|
|
|
106
|
+
const retryConfig = this.options.retryConfig ?? {};
|
|
79
107
|
this.reconnection = new RetryStrategy(retryConfig, {
|
|
80
108
|
onRetry: () => { void this.connect(); },
|
|
81
109
|
onMaxAttemptsReached: (error) => this.handleError(error),
|
|
82
110
|
});
|
|
83
111
|
|
|
84
|
-
if (abortSignal
|
|
85
|
-
this.
|
|
112
|
+
if (this.options.abortSignal?.aborted) {
|
|
113
|
+
this.progressConnectionState(ConnectionState.CLOSED);
|
|
86
114
|
return;
|
|
87
115
|
}
|
|
88
116
|
|
|
89
|
-
this.
|
|
90
|
-
|
|
117
|
+
this.options.abortSignal?.addEventListener('abort', () => this.close());
|
|
118
|
+
this.connect();
|
|
91
119
|
}
|
|
92
120
|
|
|
93
121
|
// === Connection Lifecycle ===
|
|
94
122
|
|
|
95
123
|
private connect(): void {
|
|
96
|
-
if (this.
|
|
124
|
+
if (this.options.abortSignal?.aborted) return;
|
|
97
125
|
|
|
98
|
-
|
|
99
|
-
this.
|
|
126
|
+
// Prevent reconnecting after first successful connection.
|
|
127
|
+
if (!this.progressConnectionState(ConnectionState.CONNECTING)) return;
|
|
100
128
|
|
|
101
129
|
try {
|
|
102
130
|
this.ws = this.createWebSocket();
|
|
103
|
-
|
|
131
|
+
|
|
132
|
+
this.ws.addEventListener('open', () => this.onOpen());
|
|
133
|
+
this.ws.addEventListener('message', (event) => this.onMessage(event.data));
|
|
134
|
+
this.ws.addEventListener('error', (error) => this.onError(error));
|
|
135
|
+
this.ws.addEventListener('close', () => this.onClose());
|
|
104
136
|
} catch (error) {
|
|
105
|
-
this.
|
|
106
|
-
this.connectionState = 'disconnected';
|
|
137
|
+
this.lastError = this.toError(error);
|
|
107
138
|
this.reconnection.schedule();
|
|
108
139
|
}
|
|
109
140
|
}
|
|
110
141
|
|
|
111
142
|
private createWebSocket(): WebSocket {
|
|
112
|
-
const options =
|
|
113
|
-
? { headers: { authorization: `Bearer ${this.jwtToken}` } }
|
|
114
|
-
: undefined;
|
|
115
|
-
|
|
116
|
-
const ws = new (WebSocket as any)(this.url, options);
|
|
117
|
-
if (ws) {
|
|
118
|
-
ws.binaryType = 'arraybuffer';
|
|
119
|
-
}
|
|
120
|
-
return ws;
|
|
121
|
-
}
|
|
143
|
+
const options: WebSocketInit = {};
|
|
122
144
|
|
|
123
|
-
|
|
124
|
-
if (
|
|
145
|
+
if (this.options.jwtToken) options.headers = { authorization: `Bearer ${this.options.jwtToken}` };
|
|
146
|
+
if (this.options.dispatcher) options.dispatcher = this.options.dispatcher;
|
|
125
147
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
this.ws.addEventListener('close', () => this.onClose());
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
private attachAbortSignalHandler(): void {
|
|
133
|
-
this.abortSignal.addEventListener('abort', () => this.close());
|
|
148
|
+
const ws = new WebSocket(this.url, options);
|
|
149
|
+
ws.binaryType = 'arraybuffer';
|
|
150
|
+
return ws;
|
|
134
151
|
}
|
|
135
152
|
|
|
136
153
|
private onOpen(): void {
|
|
137
|
-
this.
|
|
138
|
-
this.
|
|
139
|
-
void this.processSendQueue();
|
|
154
|
+
this.progressConnectionState(ConnectionState.CONNECTED);
|
|
155
|
+
this.processSendQueue();
|
|
140
156
|
}
|
|
141
157
|
|
|
142
|
-
private
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
if (this.sendCompleted) {
|
|
148
|
-
this.finalizeStream();
|
|
149
|
-
} else {
|
|
150
|
-
this.connectionState = 'disconnected';
|
|
151
|
-
this.reconnection.schedule();
|
|
158
|
+
private onMessage(data: unknown): void {
|
|
159
|
+
if (!(data instanceof ArrayBuffer)) {
|
|
160
|
+
this.handleError(new Error(`Unexpected WS message format: ${typeof data}`));
|
|
161
|
+
return;
|
|
152
162
|
}
|
|
153
|
-
}
|
|
154
163
|
|
|
155
|
-
private onError(error: unknown): void {
|
|
156
|
-
this.handleError(this.toError(error));
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
private onMessage(data: unknown): void {
|
|
160
164
|
try {
|
|
161
|
-
const message = this.
|
|
165
|
+
const message = this.parseServerMessage(new Uint8Array(data));
|
|
162
166
|
this.deliverResponse(message);
|
|
163
167
|
} catch (error) {
|
|
164
168
|
this.handleError(this.toError(error));
|
|
165
169
|
}
|
|
166
170
|
}
|
|
167
171
|
|
|
168
|
-
private
|
|
169
|
-
if (this.
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
private close(): void {
|
|
175
|
-
if (this.isClosed()) return;
|
|
176
|
-
|
|
177
|
-
this.connectionState = 'closed';
|
|
178
|
-
this.reconnection.cancel();
|
|
179
|
-
this.closeWebSocket();
|
|
180
|
-
this.rejectAllPendingOperations();
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
private closeWebSocket(): void {
|
|
184
|
-
if (!this.ws) return;
|
|
185
|
-
|
|
186
|
-
try {
|
|
187
|
-
this.ws.close();
|
|
188
|
-
} catch {
|
|
189
|
-
// Suppress close errors
|
|
172
|
+
private onError(error: unknown): void {
|
|
173
|
+
if (this.connectionState < ConnectionState.CONNECTED) {
|
|
174
|
+
// Try to connect several times until we succeed or run out of attempts.
|
|
175
|
+
this.lastError = this.toError(error);
|
|
176
|
+
this.reconnection.schedule();
|
|
177
|
+
return;
|
|
190
178
|
}
|
|
191
179
|
|
|
192
|
-
this.
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
private finalizeStream(): void {
|
|
196
|
-
this.connectionState = 'closed';
|
|
197
|
-
this.resolveAllPendingResponses();
|
|
180
|
+
this.handleError(this.toError(error));
|
|
198
181
|
}
|
|
199
182
|
|
|
200
|
-
private
|
|
201
|
-
|
|
202
|
-
const resolver = this.responseResolvers.shift()!;
|
|
203
|
-
resolver.resolve({ value: undefined as any, done: true });
|
|
204
|
-
}
|
|
205
|
-
}
|
|
183
|
+
private onClose(): void {
|
|
184
|
+
this.progressConnectionState(ConnectionState.CLOSED);
|
|
206
185
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
186
|
+
if (!this.lastError) {
|
|
187
|
+
this.rejectAllSendOperations(this.createStreamClosedError());
|
|
188
|
+
this.resolveAllPendingResponses(); // unblock active async iterator
|
|
189
|
+
} else {
|
|
190
|
+
this.rejectAllPendingOperations(this.lastError);
|
|
210
191
|
}
|
|
211
|
-
|
|
212
|
-
throw new Error(`Unsupported message format: ${typeof data}`);
|
|
213
192
|
}
|
|
214
193
|
|
|
215
194
|
// === Send Queue Management ===
|
|
216
195
|
|
|
217
|
-
private
|
|
196
|
+
private enqueueSend(message: ClientMsg): Promise<void> {
|
|
218
197
|
if (this.sendCompleted) {
|
|
219
198
|
throw new Error('Cannot send: stream already completed');
|
|
220
199
|
}
|
|
221
200
|
|
|
222
|
-
if (this.abortSignal
|
|
201
|
+
if (this.options.abortSignal?.aborted) {
|
|
223
202
|
throw new Error('Cannot send: stream aborted');
|
|
224
203
|
}
|
|
225
|
-
}
|
|
226
204
|
|
|
227
|
-
private enqueueSend(message: ClientMessageType): Promise<void> {
|
|
228
205
|
return new Promise<void>((resolve, reject) => {
|
|
229
206
|
this.sendQueue.push({ message, resolve, reject });
|
|
230
|
-
|
|
207
|
+
this.processSendQueue();
|
|
231
208
|
});
|
|
232
209
|
}
|
|
233
210
|
|
|
@@ -241,10 +218,10 @@ export class WebSocketBiDiStream implements BiDiStream<ClientMessageType, Server
|
|
|
241
218
|
}
|
|
242
219
|
|
|
243
220
|
private canSendMessages(): boolean {
|
|
244
|
-
return this.connectionState ===
|
|
221
|
+
return this.connectionState === ConnectionState.CONNECTED;
|
|
245
222
|
}
|
|
246
223
|
|
|
247
|
-
private sendQueuedMessage(queued: QueuedMessage): void {
|
|
224
|
+
private sendQueuedMessage(queued: QueuedMessage<ClientMsg>): void {
|
|
248
225
|
try {
|
|
249
226
|
const ws = this.ws;
|
|
250
227
|
if (!ws) {
|
|
@@ -256,7 +233,7 @@ export class WebSocketBiDiStream implements BiDiStream<ClientMessageType, Server
|
|
|
256
233
|
throw new Error(`WebSocket is not open (readyState: ${ws.readyState})`);
|
|
257
234
|
}
|
|
258
235
|
|
|
259
|
-
const binary =
|
|
236
|
+
const binary = this.serializeClientMessage(queued.message);
|
|
260
237
|
ws.send(binary);
|
|
261
238
|
queued.resolve();
|
|
262
239
|
} catch (error) {
|
|
@@ -265,7 +242,7 @@ export class WebSocketBiDiStream implements BiDiStream<ClientMessageType, Server
|
|
|
265
242
|
}
|
|
266
243
|
|
|
267
244
|
private async drainSendQueue(): Promise<void> {
|
|
268
|
-
const POLL_INTERVAL_MS =
|
|
245
|
+
const POLL_INTERVAL_MS = 5;
|
|
269
246
|
|
|
270
247
|
while (this.sendQueue.length > 0) {
|
|
271
248
|
await this.waitForCondition(
|
|
@@ -280,21 +257,21 @@ export class WebSocketBiDiStream implements BiDiStream<ClientMessageType, Server
|
|
|
280
257
|
intervalMs: number,
|
|
281
258
|
): Promise<void> {
|
|
282
259
|
return new Promise<void>((resolve, reject) => {
|
|
283
|
-
if (this.abortSignal
|
|
284
|
-
return reject(this.toError(this.abortSignal.reason) ?? new Error('Stream aborted'));
|
|
260
|
+
if (this.options.abortSignal?.aborted) {
|
|
261
|
+
return reject(this.toError(this.options.abortSignal.reason) ?? new Error('Stream aborted'));
|
|
285
262
|
}
|
|
286
263
|
|
|
287
264
|
let timeoutId: ReturnType<typeof setTimeout>;
|
|
288
265
|
const onAbort = () => {
|
|
289
266
|
clearTimeout(timeoutId);
|
|
290
|
-
reject(this.toError(this.abortSignal
|
|
267
|
+
reject(this.toError(this.options.abortSignal?.reason) ?? new Error('Stream aborted'));
|
|
291
268
|
};
|
|
292
269
|
|
|
293
|
-
this.abortSignal
|
|
270
|
+
this.options.abortSignal?.addEventListener('abort', onAbort, { once: true });
|
|
294
271
|
|
|
295
272
|
const check = () => {
|
|
296
273
|
if (condition() || this.isStreamEnded()) {
|
|
297
|
-
this.abortSignal
|
|
274
|
+
this.options.abortSignal?.removeEventListener('abort', onAbort);
|
|
298
275
|
resolve();
|
|
299
276
|
} else {
|
|
300
277
|
timeoutId = setTimeout(check, intervalMs);
|
|
@@ -307,7 +284,7 @@ export class WebSocketBiDiStream implements BiDiStream<ClientMessageType, Server
|
|
|
307
284
|
|
|
308
285
|
// === Response Delivery ===
|
|
309
286
|
|
|
310
|
-
private deliverResponse(message:
|
|
287
|
+
private deliverResponse(message: ServerMsg): void {
|
|
311
288
|
if (this.responseResolvers.length > 0) {
|
|
312
289
|
const resolver = this.responseResolvers.shift()!;
|
|
313
290
|
resolver.resolve({ value: message, done: false });
|
|
@@ -316,7 +293,7 @@ export class WebSocketBiDiStream implements BiDiStream<ClientMessageType, Server
|
|
|
316
293
|
}
|
|
317
294
|
}
|
|
318
295
|
|
|
319
|
-
private async *createResponseIterator(): AsyncIterator<
|
|
296
|
+
private async *createResponseIterator(): AsyncIterator<ServerMsg> {
|
|
320
297
|
while (true) {
|
|
321
298
|
const result = await this.nextResponse();
|
|
322
299
|
|
|
@@ -326,8 +303,8 @@ export class WebSocketBiDiStream implements BiDiStream<ClientMessageType, Server
|
|
|
326
303
|
}
|
|
327
304
|
}
|
|
328
305
|
|
|
329
|
-
private nextResponse(): Promise<IteratorResult<
|
|
330
|
-
return new Promise<IteratorResult<
|
|
306
|
+
private nextResponse(): Promise<IteratorResult<ServerMsg>> {
|
|
307
|
+
return new Promise<IteratorResult<ServerMsg>>((resolve, reject) => {
|
|
331
308
|
// Fast path: message already available
|
|
332
309
|
if (this.responseQueue.length > 0) {
|
|
333
310
|
const message = this.responseQueue.shift()!;
|
|
@@ -337,8 +314,8 @@ export class WebSocketBiDiStream implements BiDiStream<ClientMessageType, Server
|
|
|
337
314
|
|
|
338
315
|
// Stream ended
|
|
339
316
|
if (this.isStreamEnded()) {
|
|
340
|
-
if (this.
|
|
341
|
-
reject(this.
|
|
317
|
+
if (this.lastError) {
|
|
318
|
+
reject(this.lastError);
|
|
342
319
|
} else {
|
|
343
320
|
resolve({ value: undefined as any, done: true });
|
|
344
321
|
}
|
|
@@ -350,21 +327,23 @@ export class WebSocketBiDiStream implements BiDiStream<ClientMessageType, Server
|
|
|
350
327
|
});
|
|
351
328
|
}
|
|
352
329
|
|
|
330
|
+
private resolveAllPendingResponses(): void {
|
|
331
|
+
while (this.responseResolvers.length > 0) {
|
|
332
|
+
const resolver = this.responseResolvers.shift()!;
|
|
333
|
+
resolver.resolve({ value: undefined as any, done: true });
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
353
337
|
// === Error Handling ===
|
|
354
|
-
private handleError(error: Error): void {
|
|
355
|
-
if (this.isClosed()) return;
|
|
356
338
|
|
|
357
|
-
|
|
358
|
-
this.
|
|
359
|
-
this.
|
|
360
|
-
this.closeWebSocket();
|
|
361
|
-
this.rejectAllPendingOperations(error);
|
|
339
|
+
private handleError(error: Error): void {
|
|
340
|
+
this.lastError = error;
|
|
341
|
+
this.close();
|
|
362
342
|
}
|
|
363
343
|
|
|
364
|
-
private rejectAllPendingOperations(error
|
|
365
|
-
|
|
366
|
-
this.
|
|
367
|
-
this.rejectAllResponseResolvers(err);
|
|
344
|
+
private rejectAllPendingOperations(error: Error): void {
|
|
345
|
+
this.rejectAllSendOperations(error);
|
|
346
|
+
this.rejectAllResponseResolvers(error);
|
|
368
347
|
}
|
|
369
348
|
|
|
370
349
|
private rejectAllSendOperations(error: Error): void {
|
|
@@ -382,31 +361,40 @@ export class WebSocketBiDiStream implements BiDiStream<ClientMessageType, Server
|
|
|
382
361
|
}
|
|
383
362
|
|
|
384
363
|
private createStreamClosedError(): Error {
|
|
385
|
-
if (this.abortSignal
|
|
386
|
-
const reason = this.abortSignal.reason;
|
|
364
|
+
if (this.options.abortSignal?.aborted) {
|
|
365
|
+
const reason = this.options.abortSignal.reason;
|
|
387
366
|
if (reason instanceof Error) {
|
|
388
367
|
return reason;
|
|
389
368
|
}
|
|
390
369
|
return new Error('Stream aborted', { cause: reason });
|
|
391
370
|
}
|
|
392
|
-
return new Error('Stream closed');
|
|
393
|
-
}
|
|
394
|
-
// === State Checks ===
|
|
395
371
|
|
|
396
|
-
|
|
397
|
-
return this.connectionState === 'connecting'
|
|
398
|
-
|| this.connectionState === 'connected';
|
|
372
|
+
return new Error('Stream closed');
|
|
399
373
|
}
|
|
400
374
|
|
|
401
|
-
|
|
402
|
-
return this.connectionState === 'closed';
|
|
403
|
-
}
|
|
375
|
+
// === Helpers ===
|
|
404
376
|
|
|
405
377
|
private isStreamEnded(): boolean {
|
|
406
|
-
return this.
|
|
378
|
+
return this.connectionState === ConnectionState.CLOSED || this.options.abortSignal?.aborted || false;
|
|
407
379
|
}
|
|
408
380
|
|
|
409
381
|
private toError(error: unknown): Error {
|
|
410
|
-
|
|
382
|
+
if (error instanceof Error) return error;
|
|
383
|
+
if (error instanceof ErrorEvent) return error.error;
|
|
384
|
+
return new Error(String(error));
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Connection state progresses linearly from NEW to CLOSED and never goes back.
|
|
389
|
+
* This internal contract dramatically simplifies the internal stream state management.
|
|
390
|
+
*
|
|
391
|
+
* If you ever feel the need to make this contract less strict, think twice.
|
|
392
|
+
*/
|
|
393
|
+
private progressConnectionState(newState: ConnectionState): boolean {
|
|
394
|
+
if (newState < this.connectionState) {
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
this.connectionState = newState;
|
|
398
|
+
return true;
|
|
411
399
|
}
|
|
412
400
|
}
|