@nmtjs/client 0.15.3 → 0.16.0-beta.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/dist/client.d.ts +65 -0
- package/dist/client.js +98 -0
- package/dist/client.js.map +1 -0
- package/dist/clients/runtime.d.ts +6 -12
- package/dist/clients/runtime.js +58 -57
- package/dist/clients/runtime.js.map +1 -1
- package/dist/clients/static.d.ts +4 -9
- package/dist/clients/static.js +20 -20
- package/dist/clients/static.js.map +1 -1
- package/dist/core.d.ts +36 -83
- package/dist/core.js +315 -690
- package/dist/core.js.map +1 -1
- package/dist/events.d.ts +0 -1
- package/dist/events.js +74 -11
- package/dist/events.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/layers/ping.d.ts +6 -0
- package/dist/layers/ping.js +65 -0
- package/dist/layers/ping.js.map +1 -0
- package/dist/layers/rpc.d.ts +19 -0
- package/dist/layers/rpc.js +564 -0
- package/dist/layers/rpc.js.map +1 -0
- package/dist/layers/streams.d.ts +20 -0
- package/dist/layers/streams.js +194 -0
- package/dist/layers/streams.js.map +1 -0
- package/dist/plugins/browser.js +28 -9
- package/dist/plugins/browser.js.map +1 -1
- package/dist/plugins/heartbeat.js +10 -10
- package/dist/plugins/heartbeat.js.map +1 -1
- package/dist/plugins/index.d.ts +1 -1
- package/dist/plugins/index.js +0 -1
- package/dist/plugins/index.js.map +1 -1
- package/dist/plugins/reconnect.js +11 -94
- package/dist/plugins/reconnect.js.map +1 -1
- package/dist/plugins/types.d.ts +27 -11
- package/dist/transport.d.ts +49 -31
- package/dist/types.d.ts +21 -5
- package/package.json +10 -10
- package/src/client.ts +218 -0
- package/src/clients/runtime.ts +93 -79
- package/src/clients/static.ts +46 -38
- package/src/core.ts +408 -901
- package/src/events.ts +113 -14
- package/src/index.ts +4 -0
- package/src/layers/ping.ts +99 -0
- package/src/layers/rpc.ts +778 -0
- package/src/layers/streams.ts +277 -0
- package/src/plugins/browser.ts +39 -9
- package/src/plugins/heartbeat.ts +10 -10
- package/src/plugins/index.ts +8 -1
- package/src/plugins/reconnect.ts +12 -119
- package/src/plugins/types.ts +30 -13
- package/src/transport.ts +75 -46
- package/src/types.ts +33 -8
package/src/core.ts
CHANGED
|
@@ -1,49 +1,30 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type { TAnyRouterContract } from '@nmtjs/contract'
|
|
3
|
-
import type { ProtocolBlobMetadata, ProtocolVersion } from '@nmtjs/protocol'
|
|
1
|
+
import type { ProtocolVersion } from '@nmtjs/protocol'
|
|
4
2
|
import type {
|
|
5
3
|
BaseClientFormat,
|
|
6
4
|
MessageContext,
|
|
7
5
|
ProtocolVersionInterface,
|
|
8
|
-
ServerMessageTypePayload,
|
|
9
|
-
} from '@nmtjs/protocol/client'
|
|
10
|
-
import {
|
|
11
|
-
anyAbortSignal,
|
|
12
|
-
createFuture,
|
|
13
|
-
MAX_UINT32,
|
|
14
|
-
noopFn,
|
|
15
|
-
withTimeout,
|
|
16
|
-
} from '@nmtjs/common'
|
|
17
|
-
import {
|
|
18
|
-
ClientMessageType,
|
|
19
|
-
ConnectionType,
|
|
20
|
-
ErrorCode,
|
|
21
|
-
ProtocolBlob,
|
|
22
|
-
ServerMessageType,
|
|
23
|
-
} from '@nmtjs/protocol'
|
|
24
|
-
import {
|
|
25
|
-
ProtocolError,
|
|
26
|
-
ProtocolServerBlobStream,
|
|
27
|
-
ProtocolServerRPCStream,
|
|
28
|
-
ProtocolServerStream,
|
|
29
|
-
versions,
|
|
30
6
|
} from '@nmtjs/protocol/client'
|
|
7
|
+
import { noopFn } from '@nmtjs/common'
|
|
8
|
+
import { ConnectionType } from '@nmtjs/protocol'
|
|
9
|
+
import { ProtocolError, versions } from '@nmtjs/protocol/client'
|
|
31
10
|
|
|
32
11
|
import type {
|
|
33
|
-
ClientDisconnectReason,
|
|
34
12
|
ClientPlugin,
|
|
13
|
+
ClientPluginContext,
|
|
35
14
|
ClientPluginEvent,
|
|
36
15
|
ClientPluginInstance,
|
|
16
|
+
ReconnectConfig,
|
|
17
|
+
StreamEvent,
|
|
37
18
|
} from './plugins/types.ts'
|
|
38
|
-
import type { BaseClientTransformer } from './transformers.ts'
|
|
39
|
-
import type { ClientCallResponse, ClientTransportFactory } from './transport.ts'
|
|
40
19
|
import type {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
20
|
+
ClientDisconnectReason,
|
|
21
|
+
ClientTransport,
|
|
22
|
+
TransportCallContext,
|
|
23
|
+
TransportCallOptions,
|
|
24
|
+
TransportCallResponse,
|
|
25
|
+
TransportRpcParams,
|
|
26
|
+
} from './transport.ts'
|
|
45
27
|
import { EventEmitter } from './events.ts'
|
|
46
|
-
import { ClientStreams, ServerStreams } from './streams.ts'
|
|
47
28
|
|
|
48
29
|
export {
|
|
49
30
|
ErrorCode,
|
|
@@ -51,545 +32,351 @@ export {
|
|
|
51
32
|
type ProtocolBlobMetadata,
|
|
52
33
|
} from '@nmtjs/protocol'
|
|
53
34
|
|
|
54
|
-
export
|
|
35
|
+
export type ConnectionState =
|
|
36
|
+
| 'idle'
|
|
37
|
+
| 'connecting'
|
|
38
|
+
| 'connected'
|
|
39
|
+
| 'disconnecting'
|
|
40
|
+
| 'disconnected'
|
|
41
|
+
|
|
42
|
+
export interface ClientCoreOptions {
|
|
43
|
+
protocol: ProtocolVersion
|
|
44
|
+
format: BaseClientFormat
|
|
45
|
+
application?: string
|
|
46
|
+
autoConnect?: boolean
|
|
47
|
+
plugins?: ClientPlugin[]
|
|
48
|
+
}
|
|
55
49
|
|
|
56
50
|
export class ClientError extends ProtocolError {}
|
|
57
51
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
52
|
+
const DEFAULT_RECONNECT_TIMEOUT = 1000
|
|
53
|
+
const DEFAULT_MAX_RECONNECT_TIMEOUT = 60000
|
|
54
|
+
const DEFAULT_CONNECT_ERROR_REASON = 'connect_error'
|
|
55
|
+
|
|
56
|
+
const sleep = (ms: number, signal?: AbortSignal) => {
|
|
57
|
+
return new Promise<void>((resolve) => {
|
|
58
|
+
if (signal?.aborted) return resolve()
|
|
59
|
+
|
|
60
|
+
const timer = setTimeout(resolve, ms)
|
|
61
|
+
signal?.addEventListener(
|
|
62
|
+
'abort',
|
|
63
|
+
() => {
|
|
64
|
+
clearTimeout(timer)
|
|
65
|
+
resolve()
|
|
66
|
+
},
|
|
67
|
+
{ once: true },
|
|
68
|
+
)
|
|
69
|
+
})
|
|
61
70
|
}
|
|
62
71
|
|
|
63
|
-
const
|
|
72
|
+
const computeReconnectDelay = (ms: number) => {
|
|
73
|
+
if (globalThis.window) {
|
|
74
|
+
const jitter = Math.floor(ms * 0.2 * Math.random())
|
|
75
|
+
return ms + jitter
|
|
76
|
+
}
|
|
64
77
|
|
|
65
|
-
|
|
66
|
-
RouterContract extends TAnyRouterContract = TAnyRouterContract,
|
|
67
|
-
SafeCall extends boolean = false,
|
|
68
|
-
> {
|
|
69
|
-
contract: RouterContract
|
|
70
|
-
protocol: ProtocolVersion
|
|
71
|
-
format: BaseClientFormat
|
|
72
|
-
application?: string
|
|
73
|
-
timeout?: number
|
|
74
|
-
plugins?: ClientPlugin[]
|
|
75
|
-
safe?: SafeCall
|
|
78
|
+
return ms
|
|
76
79
|
}
|
|
77
80
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
* @todo Consider edge case where callId/streamId overflow at MAX_UINT32 with existing entries
|
|
81
|
-
*/
|
|
82
|
-
export abstract class BaseClient<
|
|
83
|
-
TransportFactory extends ClientTransportFactory<
|
|
84
|
-
any,
|
|
85
|
-
any
|
|
86
|
-
> = ClientTransportFactory<any, any>,
|
|
87
|
-
RouterContract extends TAnyRouterContract = TAnyRouterContract,
|
|
88
|
-
SafeCall extends boolean = false,
|
|
89
|
-
InputTypeProvider extends TypeProvider = TypeProvider,
|
|
90
|
-
OutputTypeProvider extends TypeProvider = TypeProvider,
|
|
91
|
-
> extends EventEmitter<{
|
|
81
|
+
export class ClientCore extends EventEmitter<{
|
|
82
|
+
message: [message: unknown, raw: ArrayBufferView]
|
|
92
83
|
connected: []
|
|
93
84
|
disconnected: [reason: ClientDisconnectReason]
|
|
85
|
+
state_changed: [state: ConnectionState, previous: ConnectionState]
|
|
94
86
|
pong: [nonce: number]
|
|
95
87
|
}> {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
protected messageContext!: MessageContext | null
|
|
120
|
-
protected clientStreams = new ClientStreams()
|
|
121
|
-
protected serverStreams = new ServerStreams()
|
|
122
|
-
protected rpcStreams = new ServerStreams()
|
|
123
|
-
protected callId = 0
|
|
124
|
-
protected streamId = 0
|
|
125
|
-
protected cab: AbortController | null = null
|
|
126
|
-
protected connecting: Promise<void> | null = null
|
|
127
|
-
|
|
128
|
-
protected _state: 'connected' | 'disconnected' = 'disconnected'
|
|
129
|
-
protected _lastDisconnectReason: ClientDisconnectReason = 'server'
|
|
130
|
-
protected _disposed = false
|
|
131
|
-
|
|
132
|
-
protected pingNonce = 0
|
|
133
|
-
protected pendingPings = new Map<number, Future<void>>()
|
|
134
|
-
protected plugins: ClientPluginInstance[] = []
|
|
135
|
-
|
|
136
|
-
private clientDisconnectAsReconnect = false
|
|
137
|
-
private clientDisconnectOverrideReason: string | null = null
|
|
138
|
-
|
|
139
|
-
private authValue: any
|
|
88
|
+
readonly protocol: ProtocolVersionInterface
|
|
89
|
+
readonly format: BaseClientFormat
|
|
90
|
+
readonly application?: string
|
|
91
|
+
readonly autoConnect: boolean
|
|
92
|
+
|
|
93
|
+
auth: any
|
|
94
|
+
messageContext: MessageContext | null = null
|
|
95
|
+
|
|
96
|
+
#state: ConnectionState = 'idle'
|
|
97
|
+
#messageContextFactory: (() => MessageContext) | null = null
|
|
98
|
+
#cab: AbortController | null = null
|
|
99
|
+
#connecting: Promise<void> | null = null
|
|
100
|
+
#disposed = false
|
|
101
|
+
#plugins: ClientPluginInstance[] = []
|
|
102
|
+
#lastDisconnectReason: ClientDisconnectReason = 'server'
|
|
103
|
+
#clientDisconnectAsReconnect = false
|
|
104
|
+
#clientDisconnectOverrideReason: ClientDisconnectReason | null = null
|
|
105
|
+
#reconnectConfig: ReconnectConfig | null = null
|
|
106
|
+
#reconnectPauseReasons = new Set<string>()
|
|
107
|
+
#reconnectController: AbortController | null = null
|
|
108
|
+
#reconnectPromise: Promise<void> | null = null
|
|
109
|
+
#reconnectTimeout = DEFAULT_RECONNECT_TIMEOUT
|
|
110
|
+
#reconnectImmediate = false
|
|
140
111
|
|
|
141
112
|
constructor(
|
|
142
|
-
|
|
143
|
-
readonly
|
|
144
|
-
readonly transportOptions: TransportFactory extends ClientTransportFactory<
|
|
145
|
-
any,
|
|
146
|
-
infer U
|
|
147
|
-
>
|
|
148
|
-
? U
|
|
149
|
-
: never,
|
|
113
|
+
options: ClientCoreOptions,
|
|
114
|
+
readonly transport: ClientTransport,
|
|
150
115
|
) {
|
|
151
116
|
super()
|
|
152
117
|
|
|
153
118
|
this.protocol = versions[options.protocol]
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
this.transport = this.transportFactory(
|
|
158
|
-
{ protocol, format },
|
|
159
|
-
this.transportOptions,
|
|
160
|
-
) as any
|
|
161
|
-
|
|
162
|
-
this.plugins = this.options.plugins?.map((plugin) => plugin(this)) ?? []
|
|
163
|
-
for (const plugin of this.plugins) {
|
|
164
|
-
plugin.onInit?.()
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
dispose() {
|
|
169
|
-
this._disposed = true
|
|
170
|
-
this.stopAllPendingPings('dispose')
|
|
171
|
-
for (let i = this.plugins.length - 1; i >= 0; i--) {
|
|
172
|
-
this.plugins[i].dispose?.()
|
|
173
|
-
}
|
|
119
|
+
this.format = options.format
|
|
120
|
+
this.application = options.application
|
|
121
|
+
this.autoConnect = options.autoConnect ?? false
|
|
174
122
|
}
|
|
175
123
|
|
|
176
124
|
get state() {
|
|
177
|
-
return this
|
|
125
|
+
return this.#state
|
|
178
126
|
}
|
|
179
127
|
|
|
180
128
|
get lastDisconnectReason() {
|
|
181
|
-
return this
|
|
129
|
+
return this.#lastDisconnectReason
|
|
182
130
|
}
|
|
183
131
|
|
|
184
132
|
get transportType() {
|
|
185
133
|
return this.transport.type
|
|
186
134
|
}
|
|
187
135
|
|
|
188
|
-
|
|
189
|
-
return this
|
|
136
|
+
get connectionSignal() {
|
|
137
|
+
return this.#cab?.signal
|
|
190
138
|
}
|
|
191
139
|
|
|
192
|
-
|
|
193
|
-
return this
|
|
140
|
+
isDisposed() {
|
|
141
|
+
return this.#disposed
|
|
194
142
|
}
|
|
195
143
|
|
|
196
|
-
|
|
197
|
-
return
|
|
144
|
+
shouldConnectOnCall() {
|
|
145
|
+
return (
|
|
146
|
+
this.autoConnect &&
|
|
147
|
+
!this.#disposed &&
|
|
148
|
+
this.#lastDisconnectReason !== 'client' &&
|
|
149
|
+
(this.#state === 'idle' ||
|
|
150
|
+
this.#state === 'connecting' ||
|
|
151
|
+
this.#state === 'disconnected')
|
|
152
|
+
)
|
|
198
153
|
}
|
|
199
154
|
|
|
200
|
-
|
|
201
|
-
this.
|
|
202
|
-
}
|
|
155
|
+
initPlugins(plugins: ClientPlugin[] = [], context: ClientPluginContext) {
|
|
156
|
+
if (this.#plugins.length > 0) return
|
|
203
157
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
if (this._disposed) return Promise.reject(new Error('Client is disposed'))
|
|
209
|
-
|
|
210
|
-
const _connect = async () => {
|
|
211
|
-
if (this.transport.type === ConnectionType.Bidirectional) {
|
|
212
|
-
const client = this
|
|
213
|
-
this.cab = new AbortController()
|
|
214
|
-
const protocol = this.protocol
|
|
215
|
-
const serverStreams = this.serverStreams
|
|
216
|
-
const transport = {
|
|
217
|
-
send: (buffer) => {
|
|
218
|
-
this.send(buffer).catch(noopFn)
|
|
219
|
-
},
|
|
220
|
-
}
|
|
221
|
-
this.messageContext = {
|
|
222
|
-
transport,
|
|
223
|
-
encoder: this.options.format,
|
|
224
|
-
decoder: this.options.format,
|
|
225
|
-
addClientStream: (blob) => {
|
|
226
|
-
const streamId = this.getStreamId()
|
|
227
|
-
return this.clientStreams.add(blob.source, streamId, blob.metadata)
|
|
228
|
-
},
|
|
229
|
-
addServerStream(streamId, metadata) {
|
|
230
|
-
const stream = new ProtocolServerBlobStream(metadata, {
|
|
231
|
-
pull: (controller) => {
|
|
232
|
-
client.emitStreamEvent({
|
|
233
|
-
direction: 'outgoing',
|
|
234
|
-
streamType: 'server_blob',
|
|
235
|
-
action: 'pull',
|
|
236
|
-
streamId,
|
|
237
|
-
byteLength: 65535,
|
|
238
|
-
})
|
|
239
|
-
transport.send(
|
|
240
|
-
protocol.encodeMessage(
|
|
241
|
-
this,
|
|
242
|
-
ClientMessageType.ServerStreamPull,
|
|
243
|
-
{ streamId, size: 65535 /* 64kb by default */ },
|
|
244
|
-
),
|
|
245
|
-
)
|
|
246
|
-
},
|
|
247
|
-
close: () => {
|
|
248
|
-
serverStreams.remove(streamId)
|
|
249
|
-
},
|
|
250
|
-
readableStrategy: { highWaterMark: 0 },
|
|
251
|
-
})
|
|
252
|
-
serverStreams.add(streamId, stream)
|
|
253
|
-
return ({ signal }: { signal?: AbortSignal } = {}) => {
|
|
254
|
-
if (signal)
|
|
255
|
-
signal.addEventListener(
|
|
256
|
-
'abort',
|
|
257
|
-
() => {
|
|
258
|
-
client.emitStreamEvent({
|
|
259
|
-
direction: 'outgoing',
|
|
260
|
-
streamType: 'server_blob',
|
|
261
|
-
action: 'abort',
|
|
262
|
-
streamId,
|
|
263
|
-
})
|
|
264
|
-
transport.send(
|
|
265
|
-
protocol.encodeMessage(
|
|
266
|
-
this,
|
|
267
|
-
ClientMessageType.ServerStreamAbort,
|
|
268
|
-
{ streamId },
|
|
269
|
-
),
|
|
270
|
-
)
|
|
271
|
-
serverStreams.abort(streamId)
|
|
272
|
-
},
|
|
273
|
-
{ once: true },
|
|
274
|
-
)
|
|
275
|
-
return stream
|
|
276
|
-
}
|
|
277
|
-
},
|
|
278
|
-
streamId: this.getStreamId.bind(this),
|
|
279
|
-
}
|
|
280
|
-
return this.transport.connect({
|
|
281
|
-
auth: this.auth,
|
|
282
|
-
application: this.options.application,
|
|
283
|
-
onMessage: this.onMessage.bind(this),
|
|
284
|
-
onConnect: this.onConnect.bind(this),
|
|
285
|
-
onDisconnect: this.onDisconnect.bind(this),
|
|
286
|
-
})
|
|
287
|
-
}
|
|
158
|
+
this.#plugins = plugins.map((plugin) => plugin(context))
|
|
159
|
+
for (const plugin of this.#plugins) {
|
|
160
|
+
plugin.onInit?.()
|
|
288
161
|
}
|
|
162
|
+
}
|
|
289
163
|
|
|
290
|
-
|
|
291
|
-
|
|
164
|
+
setMessageContextFactory(factory: () => MessageContext) {
|
|
165
|
+
this.#messageContextFactory = factory
|
|
166
|
+
}
|
|
292
167
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
.catch((error) => {
|
|
298
|
-
if (this.transport.type === ConnectionType.Bidirectional) {
|
|
299
|
-
emitDisconnectOnFailure = DEFAULT_RECONNECT_REASON
|
|
300
|
-
}
|
|
301
|
-
throw error
|
|
302
|
-
})
|
|
303
|
-
.finally(() => {
|
|
304
|
-
this.connecting = null
|
|
168
|
+
configureReconnect(config: ReconnectConfig | null) {
|
|
169
|
+
this.#reconnectConfig = config
|
|
170
|
+
this.#reconnectTimeout = config?.initialTimeout ?? DEFAULT_RECONNECT_TIMEOUT
|
|
171
|
+
this.#reconnectImmediate = false
|
|
305
172
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
}
|
|
311
|
-
})
|
|
173
|
+
if (!config) {
|
|
174
|
+
this.#cancelReconnectLoop()
|
|
175
|
+
return
|
|
176
|
+
}
|
|
312
177
|
|
|
313
|
-
|
|
178
|
+
if (
|
|
179
|
+
this.transport.type === ConnectionType.Bidirectional &&
|
|
180
|
+
this.#state === 'disconnected' &&
|
|
181
|
+
this.#lastDisconnectReason !== 'client'
|
|
182
|
+
) {
|
|
183
|
+
this.#ensureReconnectLoop()
|
|
184
|
+
}
|
|
314
185
|
}
|
|
315
186
|
|
|
316
|
-
|
|
317
|
-
if (
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
this.
|
|
321
|
-
|
|
322
|
-
if (options.reconnect) {
|
|
323
|
-
this.clientDisconnectAsReconnect = true
|
|
324
|
-
this.clientDisconnectOverrideReason = options.reason ?? 'server'
|
|
325
|
-
} else {
|
|
326
|
-
this.clientDisconnectAsReconnect = false
|
|
327
|
-
this.clientDisconnectOverrideReason = null
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
this.cab!.abort()
|
|
331
|
-
await this.transport.disconnect()
|
|
332
|
-
this.messageContext = null
|
|
333
|
-
this.cab = null
|
|
187
|
+
setReconnectPauseReason(reason: string, active: boolean) {
|
|
188
|
+
if (active) {
|
|
189
|
+
this.#reconnectPauseReasons.add(reason)
|
|
190
|
+
} else {
|
|
191
|
+
this.#reconnectPauseReasons.delete(reason)
|
|
334
192
|
}
|
|
335
193
|
}
|
|
336
194
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
195
|
+
triggerReconnect() {
|
|
196
|
+
if (
|
|
197
|
+
this.#disposed ||
|
|
198
|
+
!this.#reconnectConfig ||
|
|
199
|
+
this.transport.type !== ConnectionType.Bidirectional
|
|
200
|
+
) {
|
|
201
|
+
return
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
this.#reconnectImmediate = true
|
|
205
|
+
|
|
206
|
+
if (this.#state === 'disconnected' || this.#state === 'idle') {
|
|
207
|
+
this.#ensureReconnectLoop()
|
|
208
|
+
}
|
|
342
209
|
}
|
|
343
210
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
) {
|
|
349
|
-
const timeout = options.timeout ?? this.options.timeout
|
|
350
|
-
const controller = new AbortController()
|
|
211
|
+
connect() {
|
|
212
|
+
if (this.#disposed) {
|
|
213
|
+
return Promise.reject(new Error('Client is disposed'))
|
|
214
|
+
}
|
|
351
215
|
|
|
352
|
-
|
|
353
|
-
|
|
216
|
+
if (this.#state === 'connected') return Promise.resolve()
|
|
217
|
+
if (this.#connecting) return this.#connecting
|
|
354
218
|
|
|
355
|
-
if (
|
|
356
|
-
|
|
357
|
-
|
|
219
|
+
if (this.transport.type === ConnectionType.Unidirectional) {
|
|
220
|
+
return this.#handleConnected()
|
|
221
|
+
}
|
|
358
222
|
|
|
359
|
-
|
|
223
|
+
if (!this.#messageContextFactory) {
|
|
224
|
+
return Promise.reject(
|
|
225
|
+
new Error('Message context factory is not configured'),
|
|
226
|
+
)
|
|
227
|
+
}
|
|
360
228
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
229
|
+
this.#setState('connecting')
|
|
230
|
+
this.#cab = new AbortController()
|
|
231
|
+
this.messageContext = this.#messageContextFactory()
|
|
232
|
+
|
|
233
|
+
this.#connecting = this.transport
|
|
234
|
+
.connect({
|
|
235
|
+
auth: this.auth,
|
|
236
|
+
application: this.application,
|
|
237
|
+
onMessage: (message) => {
|
|
238
|
+
void this.#onMessage(message)
|
|
239
|
+
},
|
|
240
|
+
onConnect: () => {
|
|
241
|
+
void this.#handleConnected()
|
|
242
|
+
},
|
|
243
|
+
onDisconnect: (reason) => {
|
|
244
|
+
void this.#handleDisconnected(reason)
|
|
245
|
+
},
|
|
246
|
+
})
|
|
247
|
+
.catch(async (error) => {
|
|
248
|
+
this.messageContext = null
|
|
249
|
+
this.#cab = null
|
|
250
|
+
await this.#handleDisconnected(DEFAULT_CONNECT_ERROR_REASON)
|
|
251
|
+
throw error
|
|
252
|
+
})
|
|
253
|
+
.finally(() => {
|
|
254
|
+
this.#connecting = null
|
|
255
|
+
})
|
|
365
256
|
|
|
366
|
-
this
|
|
367
|
-
|
|
368
|
-
kind: 'rpc_request',
|
|
369
|
-
timestamp: Date.now(),
|
|
370
|
-
callId,
|
|
371
|
-
procedure,
|
|
372
|
-
body: payload,
|
|
373
|
-
})
|
|
257
|
+
return this.#connecting
|
|
258
|
+
}
|
|
374
259
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
this.calls.delete(callId)
|
|
378
|
-
const error = new ProtocolError(
|
|
379
|
-
ErrorCode.ClientRequestError,
|
|
380
|
-
signal.reason,
|
|
381
|
-
)
|
|
382
|
-
call.reject(error)
|
|
383
|
-
} else {
|
|
384
|
-
if (signal) {
|
|
385
|
-
signal.addEventListener(
|
|
386
|
-
'abort',
|
|
387
|
-
() => {
|
|
388
|
-
call.reject(
|
|
389
|
-
new ProtocolError(ErrorCode.ClientRequestError, signal!.reason),
|
|
390
|
-
)
|
|
391
|
-
if (
|
|
392
|
-
this.transport.type === ConnectionType.Bidirectional &&
|
|
393
|
-
this.messageContext
|
|
394
|
-
) {
|
|
395
|
-
const buffer = this.protocol.encodeMessage(
|
|
396
|
-
this.messageContext,
|
|
397
|
-
ClientMessageType.RpcAbort,
|
|
398
|
-
{ callId },
|
|
399
|
-
)
|
|
400
|
-
this.send(buffer).catch(noopFn)
|
|
401
|
-
}
|
|
402
|
-
},
|
|
403
|
-
{ once: true },
|
|
404
|
-
)
|
|
405
|
-
}
|
|
260
|
+
async disconnect(reason: ClientDisconnectReason = 'client') {
|
|
261
|
+
this.#cancelReconnectLoop()
|
|
406
262
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
const buffer = this.protocol.encodeMessage(
|
|
411
|
-
this.messageContext!,
|
|
412
|
-
ClientMessageType.Rpc,
|
|
413
|
-
{ callId, procedure, payload: transformedPayload },
|
|
414
|
-
)
|
|
415
|
-
await this.send(buffer, signal)
|
|
416
|
-
} else {
|
|
417
|
-
const response = await this.transport.call(
|
|
418
|
-
{
|
|
419
|
-
application: this.options.application,
|
|
420
|
-
format: this.options.format,
|
|
421
|
-
auth: this.auth,
|
|
422
|
-
},
|
|
423
|
-
{ callId, procedure, payload: transformedPayload },
|
|
424
|
-
{ signal, _stream_response: options._stream_response },
|
|
425
|
-
)
|
|
426
|
-
this.handleCallResponse(callId, response)
|
|
427
|
-
}
|
|
428
|
-
} catch (error) {
|
|
429
|
-
this.emitClientEvent({
|
|
430
|
-
kind: 'rpc_error',
|
|
431
|
-
timestamp: Date.now(),
|
|
432
|
-
callId,
|
|
433
|
-
procedure,
|
|
434
|
-
error,
|
|
435
|
-
})
|
|
436
|
-
call.reject(error)
|
|
437
|
-
}
|
|
263
|
+
if (this.transport.type === ConnectionType.Unidirectional) {
|
|
264
|
+
await this.#handleDisconnected(reason)
|
|
265
|
+
return
|
|
438
266
|
}
|
|
439
267
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
})
|
|
446
|
-
}
|
|
268
|
+
if (this.#state === 'idle' || this.#state === 'disconnected') {
|
|
269
|
+
this.#lastDisconnectReason = reason
|
|
270
|
+
this.#setState('disconnected')
|
|
271
|
+
return
|
|
272
|
+
}
|
|
447
273
|
|
|
448
|
-
|
|
449
|
-
return value
|
|
450
|
-
}
|
|
274
|
+
this.#setState('disconnecting')
|
|
451
275
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
)
|
|
276
|
+
if (this.#cab && !this.#cab.signal.aborted) {
|
|
277
|
+
try {
|
|
278
|
+
this.#cab.abort(reason)
|
|
279
|
+
} catch {
|
|
280
|
+
this.#cab.abort()
|
|
281
|
+
}
|
|
282
|
+
}
|
|
460
283
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
this.calls.delete(callId)
|
|
471
|
-
})
|
|
284
|
+
try {
|
|
285
|
+
await this.transport.disconnect()
|
|
286
|
+
|
|
287
|
+
if (this.#state === 'disconnecting') {
|
|
288
|
+
await this.#handleDisconnected(reason)
|
|
289
|
+
}
|
|
290
|
+
} catch (error) {
|
|
291
|
+
await this.#handleDisconnected(reason)
|
|
292
|
+
throw error
|
|
472
293
|
}
|
|
473
294
|
}
|
|
474
295
|
|
|
475
|
-
|
|
476
|
-
this.
|
|
477
|
-
|
|
478
|
-
this.emitClientEvent({
|
|
479
|
-
kind: 'connected',
|
|
480
|
-
timestamp: Date.now(),
|
|
481
|
-
transportType:
|
|
482
|
-
this.transport.type === ConnectionType.Bidirectional
|
|
483
|
-
? 'bidirectional'
|
|
484
|
-
: 'unidirectional',
|
|
485
|
-
})
|
|
486
|
-
for (const plugin of this.plugins) {
|
|
487
|
-
await plugin.onConnect?.()
|
|
296
|
+
requestReconnect(reason: ClientDisconnectReason = 'server') {
|
|
297
|
+
if (this.transport.type !== ConnectionType.Bidirectional) {
|
|
298
|
+
return Promise.resolve()
|
|
488
299
|
}
|
|
489
|
-
this.emit('connected')
|
|
490
|
-
}
|
|
491
300
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
reason === 'client' && this.clientDisconnectAsReconnect
|
|
495
|
-
? (this.clientDisconnectOverrideReason ?? 'server')
|
|
496
|
-
: reason
|
|
301
|
+
this.#clientDisconnectAsReconnect = true
|
|
302
|
+
this.#clientDisconnectOverrideReason = reason
|
|
497
303
|
|
|
498
|
-
this.
|
|
499
|
-
|
|
304
|
+
return this.disconnect('client')
|
|
305
|
+
}
|
|
500
306
|
|
|
501
|
-
|
|
502
|
-
this
|
|
503
|
-
this.emitClientEvent({
|
|
504
|
-
kind: 'disconnected',
|
|
505
|
-
timestamp: Date.now(),
|
|
506
|
-
reason: effectiveReason,
|
|
507
|
-
})
|
|
307
|
+
dispose() {
|
|
308
|
+
if (this.#disposed) return
|
|
508
309
|
|
|
509
|
-
|
|
310
|
+
this.#disposed = true
|
|
311
|
+
this.#cancelReconnectLoop()
|
|
510
312
|
this.messageContext = null
|
|
511
313
|
|
|
512
|
-
this.
|
|
513
|
-
|
|
514
|
-
// Fail-fast: do not keep pending calls around across disconnects.
|
|
515
|
-
if (this.calls.size) {
|
|
516
|
-
const error = new ProtocolError(
|
|
517
|
-
ErrorCode.ConnectionError,
|
|
518
|
-
'Disconnected',
|
|
519
|
-
{ reason: effectiveReason },
|
|
520
|
-
)
|
|
521
|
-
for (const call of this.calls.values()) {
|
|
522
|
-
call.reject(error)
|
|
523
|
-
}
|
|
524
|
-
this.calls.clear()
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
if (this.cab) {
|
|
314
|
+
if (this.#cab && !this.#cab.signal.aborted) {
|
|
528
315
|
try {
|
|
529
|
-
this
|
|
316
|
+
this.#cab.abort('dispose')
|
|
530
317
|
} catch {
|
|
531
|
-
this
|
|
318
|
+
this.#cab.abort()
|
|
532
319
|
}
|
|
533
|
-
this.cab = null
|
|
534
320
|
}
|
|
535
321
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
322
|
+
if (
|
|
323
|
+
this.transport.type === ConnectionType.Bidirectional &&
|
|
324
|
+
(this.#state === 'connecting' || this.#state === 'connected')
|
|
325
|
+
) {
|
|
326
|
+
void this.transport.disconnect().catch(noopFn)
|
|
540
327
|
}
|
|
541
328
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
329
|
+
for (let i = this.#plugins.length - 1; i >= 0; i--) {
|
|
330
|
+
this.#plugins[i].dispose?.()
|
|
331
|
+
}
|
|
545
332
|
}
|
|
546
333
|
|
|
547
|
-
|
|
548
|
-
if (this.
|
|
549
|
-
|
|
334
|
+
send(buffer: ArrayBufferView, signal?: AbortSignal) {
|
|
335
|
+
if (this.transport.type !== ConnectionType.Bidirectional) {
|
|
336
|
+
throw new Error('Invalid transport type for send')
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return this.transport.send(buffer, { signal })
|
|
550
340
|
}
|
|
551
341
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
342
|
+
transportCall(
|
|
343
|
+
context: TransportCallContext,
|
|
344
|
+
rpc: TransportRpcParams,
|
|
345
|
+
options: TransportCallOptions,
|
|
346
|
+
): Promise<TransportCallResponse> {
|
|
347
|
+
if (this.transport.type !== ConnectionType.Unidirectional) {
|
|
348
|
+
throw new Error('Invalid transport type for call')
|
|
558
349
|
}
|
|
559
350
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
this.pendingPings.set(nonce, future)
|
|
563
|
-
|
|
564
|
-
const buffer = this.protocol.encodeMessage(
|
|
565
|
-
this.messageContext,
|
|
566
|
-
ClientMessageType.Ping,
|
|
567
|
-
{ nonce },
|
|
568
|
-
)
|
|
351
|
+
return this.transport.call(context, rpc, options)
|
|
352
|
+
}
|
|
569
353
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
354
|
+
emitClientEvent(event: ClientPluginEvent) {
|
|
355
|
+
for (const plugin of this.#plugins) {
|
|
356
|
+
try {
|
|
357
|
+
const result = plugin.onClientEvent?.(event)
|
|
358
|
+
Promise.resolve(result).catch(noopFn)
|
|
359
|
+
} catch {}
|
|
360
|
+
}
|
|
577
361
|
}
|
|
578
362
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
363
|
+
emitStreamEvent(event: StreamEvent) {
|
|
364
|
+
this.emitClientEvent({
|
|
365
|
+
kind: 'stream_event',
|
|
366
|
+
timestamp: Date.now(),
|
|
367
|
+
...event,
|
|
368
|
+
})
|
|
584
369
|
}
|
|
585
370
|
|
|
586
|
-
|
|
371
|
+
async #onMessage(buffer: ArrayBufferView) {
|
|
587
372
|
if (!this.messageContext) return
|
|
588
373
|
|
|
589
374
|
const message = this.protocol.decodeMessage(this.messageContext, buffer)
|
|
590
|
-
|
|
375
|
+
|
|
376
|
+
for (const plugin of this.#plugins) {
|
|
591
377
|
plugin.onServerMessage?.(message, buffer)
|
|
592
378
|
}
|
|
379
|
+
|
|
593
380
|
this.emitClientEvent({
|
|
594
381
|
kind: 'server_message',
|
|
595
382
|
timestamp: Date.now(),
|
|
@@ -598,450 +385,170 @@ export abstract class BaseClient<
|
|
|
598
385
|
body: message,
|
|
599
386
|
})
|
|
600
387
|
|
|
601
|
-
|
|
602
|
-
case ServerMessageType.RpcResponse:
|
|
603
|
-
this.handleRPCResponseMessage(message)
|
|
604
|
-
break
|
|
605
|
-
case ServerMessageType.RpcStreamResponse:
|
|
606
|
-
this.handleRPCStreamResponseMessage(message)
|
|
607
|
-
break
|
|
608
|
-
case ServerMessageType.Pong: {
|
|
609
|
-
const pending = this.pendingPings.get(message.nonce)
|
|
610
|
-
if (pending) {
|
|
611
|
-
this.pendingPings.delete(message.nonce)
|
|
612
|
-
pending.resolve()
|
|
613
|
-
}
|
|
614
|
-
this.emit('pong', message.nonce)
|
|
615
|
-
break
|
|
616
|
-
}
|
|
617
|
-
case ServerMessageType.Ping: {
|
|
618
|
-
if (this.messageContext) {
|
|
619
|
-
const buffer = this.protocol.encodeMessage(
|
|
620
|
-
this.messageContext,
|
|
621
|
-
ClientMessageType.Pong,
|
|
622
|
-
{ nonce: message.nonce },
|
|
623
|
-
)
|
|
624
|
-
this.send(buffer).catch(noopFn)
|
|
625
|
-
}
|
|
626
|
-
break
|
|
627
|
-
}
|
|
628
|
-
case ServerMessageType.RpcStreamChunk:
|
|
629
|
-
this.emitStreamEvent({
|
|
630
|
-
direction: 'incoming',
|
|
631
|
-
streamType: 'rpc',
|
|
632
|
-
action: 'push',
|
|
633
|
-
callId: message.callId,
|
|
634
|
-
byteLength: message.chunk.byteLength,
|
|
635
|
-
})
|
|
636
|
-
this.rpcStreams.push(message.callId, message.chunk)
|
|
637
|
-
break
|
|
638
|
-
case ServerMessageType.RpcStreamEnd:
|
|
639
|
-
this.emitStreamEvent({
|
|
640
|
-
direction: 'incoming',
|
|
641
|
-
streamType: 'rpc',
|
|
642
|
-
action: 'end',
|
|
643
|
-
callId: message.callId,
|
|
644
|
-
})
|
|
645
|
-
this.rpcStreams.end(message.callId)
|
|
646
|
-
this.calls.delete(message.callId)
|
|
647
|
-
break
|
|
648
|
-
case ServerMessageType.RpcStreamAbort:
|
|
649
|
-
this.emitStreamEvent({
|
|
650
|
-
direction: 'incoming',
|
|
651
|
-
streamType: 'rpc',
|
|
652
|
-
action: 'abort',
|
|
653
|
-
callId: message.callId,
|
|
654
|
-
reason: message.reason,
|
|
655
|
-
})
|
|
656
|
-
this.rpcStreams.abort(message.callId)
|
|
657
|
-
this.calls.delete(message.callId)
|
|
658
|
-
break
|
|
659
|
-
case ServerMessageType.ServerStreamPush:
|
|
660
|
-
this.emitStreamEvent({
|
|
661
|
-
direction: 'incoming',
|
|
662
|
-
streamType: 'server_blob',
|
|
663
|
-
action: 'push',
|
|
664
|
-
streamId: message.streamId,
|
|
665
|
-
byteLength: message.chunk.byteLength,
|
|
666
|
-
})
|
|
667
|
-
this.serverStreams.push(message.streamId, message.chunk)
|
|
668
|
-
break
|
|
669
|
-
case ServerMessageType.ServerStreamEnd:
|
|
670
|
-
this.emitStreamEvent({
|
|
671
|
-
direction: 'incoming',
|
|
672
|
-
streamType: 'server_blob',
|
|
673
|
-
action: 'end',
|
|
674
|
-
streamId: message.streamId,
|
|
675
|
-
})
|
|
676
|
-
this.serverStreams.end(message.streamId)
|
|
677
|
-
break
|
|
678
|
-
case ServerMessageType.ServerStreamAbort:
|
|
679
|
-
this.emitStreamEvent({
|
|
680
|
-
direction: 'incoming',
|
|
681
|
-
streamType: 'server_blob',
|
|
682
|
-
action: 'abort',
|
|
683
|
-
streamId: message.streamId,
|
|
684
|
-
reason: message.reason,
|
|
685
|
-
})
|
|
686
|
-
this.serverStreams.abort(message.streamId)
|
|
687
|
-
break
|
|
688
|
-
case ServerMessageType.ClientStreamPull:
|
|
689
|
-
this.emitStreamEvent({
|
|
690
|
-
direction: 'incoming',
|
|
691
|
-
streamType: 'client_blob',
|
|
692
|
-
action: 'pull',
|
|
693
|
-
streamId: message.streamId,
|
|
694
|
-
byteLength: message.size,
|
|
695
|
-
})
|
|
696
|
-
this.clientStreams.pull(message.streamId, message.size).then(
|
|
697
|
-
(chunk) => {
|
|
698
|
-
if (chunk) {
|
|
699
|
-
this.emitStreamEvent({
|
|
700
|
-
direction: 'outgoing',
|
|
701
|
-
streamType: 'client_blob',
|
|
702
|
-
action: 'push',
|
|
703
|
-
streamId: message.streamId,
|
|
704
|
-
byteLength: chunk.byteLength,
|
|
705
|
-
})
|
|
706
|
-
const buffer = this.protocol.encodeMessage(
|
|
707
|
-
this.messageContext!,
|
|
708
|
-
ClientMessageType.ClientStreamPush,
|
|
709
|
-
{ streamId: message.streamId, chunk },
|
|
710
|
-
)
|
|
711
|
-
this.send(buffer).catch(noopFn)
|
|
712
|
-
} else {
|
|
713
|
-
this.emitStreamEvent({
|
|
714
|
-
direction: 'outgoing',
|
|
715
|
-
streamType: 'client_blob',
|
|
716
|
-
action: 'end',
|
|
717
|
-
streamId: message.streamId,
|
|
718
|
-
})
|
|
719
|
-
const buffer = this.protocol.encodeMessage(
|
|
720
|
-
this.messageContext!,
|
|
721
|
-
ClientMessageType.ClientStreamEnd,
|
|
722
|
-
{ streamId: message.streamId },
|
|
723
|
-
)
|
|
724
|
-
this.send(buffer).catch(noopFn)
|
|
725
|
-
this.clientStreams.end(message.streamId)
|
|
726
|
-
}
|
|
727
|
-
},
|
|
728
|
-
() => {
|
|
729
|
-
this.emitStreamEvent({
|
|
730
|
-
direction: 'outgoing',
|
|
731
|
-
streamType: 'client_blob',
|
|
732
|
-
action: 'abort',
|
|
733
|
-
streamId: message.streamId,
|
|
734
|
-
})
|
|
735
|
-
const buffer = this.protocol.encodeMessage(
|
|
736
|
-
this.messageContext!,
|
|
737
|
-
ClientMessageType.ClientStreamAbort,
|
|
738
|
-
{ streamId: message.streamId },
|
|
739
|
-
)
|
|
740
|
-
this.send(buffer).catch(noopFn)
|
|
741
|
-
this.clientStreams.remove(message.streamId)
|
|
742
|
-
},
|
|
743
|
-
)
|
|
744
|
-
break
|
|
745
|
-
case ServerMessageType.ClientStreamAbort:
|
|
746
|
-
this.emitStreamEvent({
|
|
747
|
-
direction: 'incoming',
|
|
748
|
-
streamType: 'client_blob',
|
|
749
|
-
action: 'abort',
|
|
750
|
-
streamId: message.streamId,
|
|
751
|
-
reason: message.reason,
|
|
752
|
-
})
|
|
753
|
-
this.clientStreams.abort(message.streamId)
|
|
754
|
-
break
|
|
755
|
-
}
|
|
388
|
+
this.emit('message', message, buffer)
|
|
756
389
|
}
|
|
757
390
|
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
kind: 'rpc_response',
|
|
778
|
-
timestamp: Date.now(),
|
|
779
|
-
callId,
|
|
780
|
-
procedure: call.procedure,
|
|
781
|
-
body: transformed,
|
|
782
|
-
})
|
|
783
|
-
call.resolve(transformed)
|
|
784
|
-
} catch (error) {
|
|
785
|
-
this.emitClientEvent({
|
|
786
|
-
kind: 'rpc_error',
|
|
787
|
-
timestamp: Date.now(),
|
|
788
|
-
callId,
|
|
789
|
-
procedure: call.procedure,
|
|
790
|
-
error,
|
|
791
|
-
})
|
|
792
|
-
call.reject(
|
|
793
|
-
new ProtocolError(
|
|
794
|
-
ErrorCode.ClientRequestError,
|
|
795
|
-
'Unable to decode response',
|
|
796
|
-
error,
|
|
797
|
-
),
|
|
798
|
-
)
|
|
799
|
-
}
|
|
391
|
+
async #handleConnected() {
|
|
392
|
+
this.#reconnectTimeout =
|
|
393
|
+
this.#reconnectConfig?.initialTimeout ?? DEFAULT_RECONNECT_TIMEOUT
|
|
394
|
+
this.#reconnectImmediate = false
|
|
395
|
+
|
|
396
|
+
this.#setState('connected')
|
|
397
|
+
this.#lastDisconnectReason = 'server'
|
|
398
|
+
|
|
399
|
+
this.emitClientEvent({
|
|
400
|
+
kind: 'connected',
|
|
401
|
+
timestamp: Date.now(),
|
|
402
|
+
transportType:
|
|
403
|
+
this.transport.type === ConnectionType.Bidirectional
|
|
404
|
+
? 'bidirectional'
|
|
405
|
+
: 'unidirectional',
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
for (const plugin of this.#plugins) {
|
|
409
|
+
await plugin.onConnect?.()
|
|
800
410
|
}
|
|
411
|
+
|
|
412
|
+
this.emit('connected')
|
|
801
413
|
}
|
|
802
414
|
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
const { procedure, signal } = call
|
|
826
|
-
this.emitClientEvent({
|
|
827
|
-
kind: 'rpc_response',
|
|
828
|
-
timestamp: Date.now(),
|
|
829
|
-
callId: message.callId,
|
|
830
|
-
procedure,
|
|
831
|
-
stream: true,
|
|
832
|
-
})
|
|
833
|
-
const stream = new ProtocolServerRPCStream({
|
|
834
|
-
start: (controller) => {
|
|
835
|
-
if (signal) {
|
|
836
|
-
if (signal.aborted) controller.error(signal.reason)
|
|
837
|
-
else
|
|
838
|
-
signal.addEventListener(
|
|
839
|
-
'abort',
|
|
840
|
-
() => {
|
|
841
|
-
controller.error(signal.reason)
|
|
842
|
-
if (this.rpcStreams.has(message.callId)) {
|
|
843
|
-
this.rpcStreams.remove(message.callId)
|
|
844
|
-
this.calls.delete(message.callId)
|
|
845
|
-
if (this.messageContext) {
|
|
846
|
-
const buffer = this.protocol.encodeMessage(
|
|
847
|
-
this.messageContext,
|
|
848
|
-
ClientMessageType.RpcAbort,
|
|
849
|
-
{ callId: message.callId, reason: signal.reason },
|
|
850
|
-
)
|
|
851
|
-
this.send(buffer).catch(noopFn)
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
|
-
},
|
|
855
|
-
{ once: true },
|
|
856
|
-
)
|
|
857
|
-
}
|
|
858
|
-
},
|
|
859
|
-
transform: (chunk) => {
|
|
860
|
-
return this.transformer.decode(
|
|
861
|
-
procedure,
|
|
862
|
-
this.options.format.decode(chunk),
|
|
863
|
-
)
|
|
864
|
-
},
|
|
865
|
-
readableStrategy: { highWaterMark: 0 },
|
|
866
|
-
})
|
|
867
|
-
this.rpcStreams.add(message.callId, stream)
|
|
868
|
-
call.resolve(stream)
|
|
869
|
-
} else {
|
|
870
|
-
// Call not found, but stream response received
|
|
871
|
-
// This can happen if the call was aborted or timed out
|
|
872
|
-
// Need to send an abort for the stream to avoid resource leaks from server side
|
|
873
|
-
if (this.messageContext) {
|
|
874
|
-
const buffer = this.protocol.encodeMessage(
|
|
875
|
-
this.messageContext,
|
|
876
|
-
ClientMessageType.RpcAbort,
|
|
877
|
-
{ callId: message.callId },
|
|
878
|
-
)
|
|
879
|
-
this.send(buffer).catch(noopFn)
|
|
415
|
+
async #handleDisconnected(reason: ClientDisconnectReason) {
|
|
416
|
+
const effectiveReason =
|
|
417
|
+
reason === 'client' && this.#clientDisconnectAsReconnect
|
|
418
|
+
? (this.#clientDisconnectOverrideReason ?? 'server')
|
|
419
|
+
: reason
|
|
420
|
+
|
|
421
|
+
this.#clientDisconnectAsReconnect = false
|
|
422
|
+
this.#clientDisconnectOverrideReason = null
|
|
423
|
+
|
|
424
|
+
const shouldSkip =
|
|
425
|
+
this.#state === 'disconnected' &&
|
|
426
|
+
this.messageContext === null &&
|
|
427
|
+
this.#lastDisconnectReason === effectiveReason
|
|
428
|
+
|
|
429
|
+
this.messageContext = null
|
|
430
|
+
|
|
431
|
+
if (this.#cab) {
|
|
432
|
+
if (!this.#cab.signal.aborted) {
|
|
433
|
+
try {
|
|
434
|
+
this.#cab.abort(reason)
|
|
435
|
+
} catch {
|
|
436
|
+
this.#cab.abort()
|
|
880
437
|
}
|
|
881
438
|
}
|
|
439
|
+
this.#cab = null
|
|
882
440
|
}
|
|
883
|
-
}
|
|
884
441
|
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
call.procedure,
|
|
901
|
-
this.options.format.decode(chunk),
|
|
902
|
-
)
|
|
903
|
-
},
|
|
904
|
-
})
|
|
905
|
-
this.rpcStreams.add(callId, stream)
|
|
906
|
-
call.resolve(({ signal }: { signal?: AbortSignal }) => {
|
|
907
|
-
const reader = response.stream.getReader()
|
|
908
|
-
|
|
909
|
-
let onAbort: (() => void) | undefined
|
|
910
|
-
if (signal) {
|
|
911
|
-
onAbort = () => {
|
|
912
|
-
reader.cancel(signal.reason).catch(noopFn)
|
|
913
|
-
this.rpcStreams.abort(callId).catch(noopFn)
|
|
914
|
-
}
|
|
915
|
-
if (signal.aborted) onAbort()
|
|
916
|
-
else signal.addEventListener('abort', onAbort, { once: true })
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
void (async () => {
|
|
920
|
-
try {
|
|
921
|
-
while (true) {
|
|
922
|
-
const { done, value } = await reader.read()
|
|
923
|
-
if (done) break
|
|
924
|
-
await this.rpcStreams.push(callId, value)
|
|
925
|
-
}
|
|
926
|
-
await this.rpcStreams.end(callId)
|
|
927
|
-
} catch {
|
|
928
|
-
await this.rpcStreams.abort(callId).catch(noopFn)
|
|
929
|
-
} finally {
|
|
930
|
-
reader.releaseLock()
|
|
931
|
-
if (signal && onAbort) {
|
|
932
|
-
signal.removeEventListener('abort', onAbort)
|
|
933
|
-
}
|
|
934
|
-
}
|
|
935
|
-
})()
|
|
936
|
-
|
|
937
|
-
return stream
|
|
938
|
-
})
|
|
939
|
-
} else {
|
|
940
|
-
// Call not found, but stream response received
|
|
941
|
-
// This can happen if the call was aborted or timed out
|
|
942
|
-
// Need to cancel the stream to avoid resource leaks from server side
|
|
943
|
-
response.stream.cancel().catch(noopFn)
|
|
944
|
-
}
|
|
945
|
-
} else if (response.type === 'blob') {
|
|
946
|
-
if (call) {
|
|
947
|
-
this.emitClientEvent({
|
|
948
|
-
kind: 'rpc_response',
|
|
949
|
-
timestamp: Date.now(),
|
|
950
|
-
callId,
|
|
951
|
-
procedure: call.procedure,
|
|
952
|
-
stream: true,
|
|
953
|
-
})
|
|
954
|
-
const { metadata, source } = response
|
|
955
|
-
const stream = new ProtocolServerBlobStream(metadata)
|
|
956
|
-
this.serverStreams.add(this.getStreamId(), stream)
|
|
957
|
-
call.resolve(({ signal }: { signal?: AbortSignal }) => {
|
|
958
|
-
source.pipeTo(stream.writable, { signal }).catch(noopFn)
|
|
959
|
-
return stream
|
|
960
|
-
})
|
|
961
|
-
} else {
|
|
962
|
-
// Call not found, but blob response received
|
|
963
|
-
// This can happen if the call was aborted or timed out
|
|
964
|
-
// Need to cancel the stream to avoid resource leaks from server side
|
|
965
|
-
response.source.cancel().catch(noopFn)
|
|
966
|
-
}
|
|
967
|
-
} else if (response.type === 'rpc') {
|
|
968
|
-
if (!call) return
|
|
969
|
-
try {
|
|
970
|
-
const decodedPayload =
|
|
971
|
-
response.result.byteLength === 0
|
|
972
|
-
? undefined
|
|
973
|
-
: this.options.format.decode(response.result)
|
|
974
|
-
|
|
975
|
-
const transformed = this.transformer.decode(
|
|
976
|
-
call.procedure,
|
|
977
|
-
decodedPayload,
|
|
978
|
-
)
|
|
979
|
-
this.emitClientEvent({
|
|
980
|
-
kind: 'rpc_response',
|
|
981
|
-
timestamp: Date.now(),
|
|
982
|
-
callId,
|
|
983
|
-
procedure: call.procedure,
|
|
984
|
-
body: transformed,
|
|
985
|
-
})
|
|
986
|
-
call.resolve(transformed)
|
|
987
|
-
} catch (error) {
|
|
988
|
-
this.emitClientEvent({
|
|
989
|
-
kind: 'rpc_error',
|
|
990
|
-
timestamp: Date.now(),
|
|
991
|
-
callId,
|
|
992
|
-
procedure: call.procedure,
|
|
993
|
-
error,
|
|
994
|
-
})
|
|
995
|
-
call.reject(
|
|
996
|
-
new ProtocolError(
|
|
997
|
-
ErrorCode.ClientRequestError,
|
|
998
|
-
'Unable to decode response',
|
|
999
|
-
error,
|
|
1000
|
-
),
|
|
1001
|
-
)
|
|
1002
|
-
}
|
|
442
|
+
if (shouldSkip) return
|
|
443
|
+
|
|
444
|
+
this.#lastDisconnectReason = effectiveReason
|
|
445
|
+
this.#setState('disconnected')
|
|
446
|
+
|
|
447
|
+
this.emitClientEvent({
|
|
448
|
+
kind: 'disconnected',
|
|
449
|
+
timestamp: Date.now(),
|
|
450
|
+
reason: effectiveReason,
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
this.emit('disconnected', effectiveReason)
|
|
454
|
+
|
|
455
|
+
for (let i = this.#plugins.length - 1; i >= 0; i--) {
|
|
456
|
+
await this.#plugins[i].onDisconnect?.(effectiveReason)
|
|
1003
457
|
}
|
|
1004
|
-
}
|
|
1005
458
|
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
return this.transport.send(buffer, { signal })
|
|
459
|
+
if (this.#shouldReconnect(effectiveReason)) {
|
|
460
|
+
this.#ensureReconnectLoop()
|
|
461
|
+
}
|
|
1010
462
|
}
|
|
1011
463
|
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
464
|
+
#setState(next: ConnectionState) {
|
|
465
|
+
if (next === this.#state) return
|
|
466
|
+
|
|
467
|
+
const previous = this.#state
|
|
468
|
+
this.#state = next
|
|
469
|
+
|
|
1018
470
|
this.emitClientEvent({
|
|
1019
|
-
kind: '
|
|
471
|
+
kind: 'state_changed',
|
|
1020
472
|
timestamp: Date.now(),
|
|
1021
|
-
|
|
473
|
+
state: next,
|
|
474
|
+
previous,
|
|
1022
475
|
})
|
|
476
|
+
|
|
477
|
+
this.emit('state_changed', next, previous)
|
|
1023
478
|
}
|
|
1024
479
|
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
this
|
|
1028
|
-
|
|
1029
|
-
|
|
480
|
+
#shouldReconnect(reason: ClientDisconnectReason) {
|
|
481
|
+
return (
|
|
482
|
+
!this.#disposed &&
|
|
483
|
+
!!this.#reconnectConfig &&
|
|
484
|
+
this.transport.type === ConnectionType.Bidirectional &&
|
|
485
|
+
reason !== 'client'
|
|
486
|
+
)
|
|
1030
487
|
}
|
|
1031
488
|
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
489
|
+
#cancelReconnectLoop() {
|
|
490
|
+
this.#reconnectImmediate = false
|
|
491
|
+
this.#reconnectController?.abort()
|
|
492
|
+
this.#reconnectController = null
|
|
493
|
+
this.#reconnectPromise = null
|
|
1037
494
|
}
|
|
1038
495
|
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
496
|
+
#ensureReconnectLoop() {
|
|
497
|
+
if (this.#reconnectPromise || !this.#reconnectConfig) return
|
|
498
|
+
|
|
499
|
+
const signal = new AbortController()
|
|
500
|
+
this.#reconnectController = signal
|
|
501
|
+
|
|
502
|
+
this.#reconnectPromise = (async () => {
|
|
503
|
+
while (
|
|
504
|
+
!signal.signal.aborted &&
|
|
505
|
+
!this.#disposed &&
|
|
506
|
+
this.#reconnectConfig &&
|
|
507
|
+
(this.#state === 'disconnected' || this.#state === 'idle') &&
|
|
508
|
+
this.#lastDisconnectReason !== 'client'
|
|
509
|
+
) {
|
|
510
|
+
if (this.#reconnectPauseReasons.size) {
|
|
511
|
+
await sleep(1000, signal.signal)
|
|
512
|
+
continue
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const delay = this.#reconnectImmediate
|
|
516
|
+
? 0
|
|
517
|
+
: computeReconnectDelay(this.#reconnectTimeout)
|
|
518
|
+
this.#reconnectImmediate = false
|
|
519
|
+
|
|
520
|
+
if (delay > 0) {
|
|
521
|
+
await sleep(delay, signal.signal)
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const currentState = this.state
|
|
525
|
+
|
|
526
|
+
if (
|
|
527
|
+
signal.signal.aborted ||
|
|
528
|
+
this.#disposed ||
|
|
529
|
+
!this.#reconnectConfig ||
|
|
530
|
+
currentState === 'connected' ||
|
|
531
|
+
currentState === 'connecting'
|
|
532
|
+
) {
|
|
533
|
+
break
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const previousTimeout = this.#reconnectTimeout
|
|
537
|
+
|
|
538
|
+
await this.connect().catch(noopFn)
|
|
539
|
+
|
|
540
|
+
if (this.state !== 'connected' && this.#reconnectConfig) {
|
|
541
|
+
this.#reconnectTimeout = Math.min(
|
|
542
|
+
previousTimeout * 2,
|
|
543
|
+
this.#reconnectConfig.maxTimeout ?? DEFAULT_MAX_RECONNECT_TIMEOUT,
|
|
544
|
+
)
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
})().finally(() => {
|
|
548
|
+
if (this.#reconnectController === signal) {
|
|
549
|
+
this.#reconnectController = null
|
|
550
|
+
}
|
|
551
|
+
this.#reconnectPromise = null
|
|
552
|
+
})
|
|
1046
553
|
}
|
|
1047
554
|
}
|