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