@nmtjs/client 0.15.3 → 0.16.0-beta.1
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 +64 -0
- package/dist/client.js +97 -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 +33 -83
- package/dist/core.js +305 -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 +521 -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 +216 -0
- package/src/clients/runtime.ts +93 -79
- package/src/clients/static.ts +46 -38
- package/src/core.ts +394 -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 +725 -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,337 @@ 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
|
+
plugins?: ClientPlugin[]
|
|
47
|
+
}
|
|
55
48
|
|
|
56
49
|
export class ClientError extends ProtocolError {}
|
|
57
50
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
51
|
+
const DEFAULT_RECONNECT_TIMEOUT = 1000
|
|
52
|
+
const DEFAULT_MAX_RECONNECT_TIMEOUT = 60000
|
|
53
|
+
const DEFAULT_CONNECT_ERROR_REASON = 'connect_error'
|
|
54
|
+
|
|
55
|
+
const sleep = (ms: number, signal?: AbortSignal) => {
|
|
56
|
+
return new Promise<void>((resolve) => {
|
|
57
|
+
if (signal?.aborted) return resolve()
|
|
58
|
+
|
|
59
|
+
const timer = setTimeout(resolve, ms)
|
|
60
|
+
signal?.addEventListener(
|
|
61
|
+
'abort',
|
|
62
|
+
() => {
|
|
63
|
+
clearTimeout(timer)
|
|
64
|
+
resolve()
|
|
65
|
+
},
|
|
66
|
+
{ once: true },
|
|
67
|
+
)
|
|
68
|
+
})
|
|
61
69
|
}
|
|
62
70
|
|
|
63
|
-
const
|
|
71
|
+
const computeReconnectDelay = (ms: number) => {
|
|
72
|
+
if (globalThis.window) {
|
|
73
|
+
const jitter = Math.floor(ms * 0.2 * Math.random())
|
|
74
|
+
return ms + jitter
|
|
75
|
+
}
|
|
64
76
|
|
|
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
|
|
77
|
+
return ms
|
|
76
78
|
}
|
|
77
79
|
|
|
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<{
|
|
80
|
+
export class ClientCore extends EventEmitter<{
|
|
81
|
+
message: [message: unknown, raw: ArrayBufferView]
|
|
92
82
|
connected: []
|
|
93
83
|
disconnected: [reason: ClientDisconnectReason]
|
|
84
|
+
state_changed: [state: ConnectionState, previous: ConnectionState]
|
|
94
85
|
pong: [nonce: number]
|
|
95
86
|
}> {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
>
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
protected protocol: ProtocolVersionInterface
|
|
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
|
|
87
|
+
readonly protocol: ProtocolVersionInterface
|
|
88
|
+
readonly format: BaseClientFormat
|
|
89
|
+
readonly application?: string
|
|
90
|
+
|
|
91
|
+
auth: any
|
|
92
|
+
messageContext: MessageContext | null = null
|
|
93
|
+
|
|
94
|
+
#state: ConnectionState = 'idle'
|
|
95
|
+
#messageContextFactory: (() => MessageContext) | null = null
|
|
96
|
+
#cab: AbortController | null = null
|
|
97
|
+
#connecting: Promise<void> | null = null
|
|
98
|
+
#disposed = false
|
|
99
|
+
#plugins: ClientPluginInstance[] = []
|
|
100
|
+
#lastDisconnectReason: ClientDisconnectReason = 'server'
|
|
101
|
+
#clientDisconnectAsReconnect = false
|
|
102
|
+
#clientDisconnectOverrideReason: ClientDisconnectReason | null = null
|
|
103
|
+
#reconnectConfig: ReconnectConfig | null = null
|
|
104
|
+
#reconnectPauseReasons = new Set<string>()
|
|
105
|
+
#reconnectController: AbortController | null = null
|
|
106
|
+
#reconnectPromise: Promise<void> | null = null
|
|
107
|
+
#reconnectTimeout = DEFAULT_RECONNECT_TIMEOUT
|
|
108
|
+
#reconnectImmediate = false
|
|
140
109
|
|
|
141
110
|
constructor(
|
|
142
|
-
|
|
143
|
-
readonly
|
|
144
|
-
readonly transportOptions: TransportFactory extends ClientTransportFactory<
|
|
145
|
-
any,
|
|
146
|
-
infer U
|
|
147
|
-
>
|
|
148
|
-
? U
|
|
149
|
-
: never,
|
|
111
|
+
options: ClientCoreOptions,
|
|
112
|
+
readonly transport: ClientTransport,
|
|
150
113
|
) {
|
|
151
114
|
super()
|
|
152
115
|
|
|
153
116
|
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
|
-
}
|
|
117
|
+
this.format = options.format
|
|
118
|
+
this.application = options.application
|
|
174
119
|
}
|
|
175
120
|
|
|
176
121
|
get state() {
|
|
177
|
-
return this
|
|
122
|
+
return this.#state
|
|
178
123
|
}
|
|
179
124
|
|
|
180
125
|
get lastDisconnectReason() {
|
|
181
|
-
return this
|
|
126
|
+
return this.#lastDisconnectReason
|
|
182
127
|
}
|
|
183
128
|
|
|
184
129
|
get transportType() {
|
|
185
130
|
return this.transport.type
|
|
186
131
|
}
|
|
187
132
|
|
|
133
|
+
get connectionSignal() {
|
|
134
|
+
return this.#cab?.signal
|
|
135
|
+
}
|
|
136
|
+
|
|
188
137
|
isDisposed() {
|
|
189
|
-
return this
|
|
138
|
+
return this.#disposed
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
initPlugins(plugins: ClientPlugin[] = [], context: ClientPluginContext) {
|
|
142
|
+
if (this.#plugins.length > 0) return
|
|
143
|
+
|
|
144
|
+
this.#plugins = plugins.map((plugin) => plugin(context))
|
|
145
|
+
for (const plugin of this.#plugins) {
|
|
146
|
+
plugin.onInit?.()
|
|
147
|
+
}
|
|
190
148
|
}
|
|
191
149
|
|
|
192
|
-
|
|
193
|
-
|
|
150
|
+
setMessageContextFactory(factory: () => MessageContext) {
|
|
151
|
+
this.#messageContextFactory = factory
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
configureReconnect(config: ReconnectConfig | null) {
|
|
155
|
+
this.#reconnectConfig = config
|
|
156
|
+
this.#reconnectTimeout = config?.initialTimeout ?? DEFAULT_RECONNECT_TIMEOUT
|
|
157
|
+
this.#reconnectImmediate = false
|
|
158
|
+
|
|
159
|
+
if (!config) {
|
|
160
|
+
this.#cancelReconnectLoop()
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (
|
|
165
|
+
this.transport.type === ConnectionType.Bidirectional &&
|
|
166
|
+
this.#state === 'disconnected' &&
|
|
167
|
+
this.#lastDisconnectReason !== 'client'
|
|
168
|
+
) {
|
|
169
|
+
this.#ensureReconnectLoop()
|
|
170
|
+
}
|
|
194
171
|
}
|
|
195
172
|
|
|
196
|
-
|
|
197
|
-
|
|
173
|
+
setReconnectPauseReason(reason: string, active: boolean) {
|
|
174
|
+
if (active) {
|
|
175
|
+
this.#reconnectPauseReasons.add(reason)
|
|
176
|
+
} else {
|
|
177
|
+
this.#reconnectPauseReasons.delete(reason)
|
|
178
|
+
}
|
|
198
179
|
}
|
|
199
180
|
|
|
200
|
-
|
|
201
|
-
|
|
181
|
+
triggerReconnect() {
|
|
182
|
+
if (
|
|
183
|
+
this.#disposed ||
|
|
184
|
+
!this.#reconnectConfig ||
|
|
185
|
+
this.transport.type !== ConnectionType.Bidirectional
|
|
186
|
+
) {
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
this.#reconnectImmediate = true
|
|
191
|
+
|
|
192
|
+
if (this.#state === 'disconnected' || this.#state === 'idle') {
|
|
193
|
+
this.#ensureReconnectLoop()
|
|
194
|
+
}
|
|
202
195
|
}
|
|
203
196
|
|
|
204
197
|
connect() {
|
|
205
|
-
if (this
|
|
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
|
-
}
|
|
198
|
+
if (this.#disposed) {
|
|
199
|
+
return Promise.reject(new Error('Client is disposed'))
|
|
288
200
|
}
|
|
289
201
|
|
|
290
|
-
|
|
291
|
-
|
|
202
|
+
if (this.#state === 'connected') return Promise.resolve()
|
|
203
|
+
if (this.#connecting) return this.#connecting
|
|
204
|
+
|
|
205
|
+
if (this.transport.type === ConnectionType.Unidirectional) {
|
|
206
|
+
return this.#handleConnected()
|
|
207
|
+
}
|
|
292
208
|
|
|
293
|
-
|
|
294
|
-
.
|
|
295
|
-
|
|
209
|
+
if (!this.#messageContextFactory) {
|
|
210
|
+
return Promise.reject(
|
|
211
|
+
new Error('Message context factory is not configured'),
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
this.#setState('connecting')
|
|
216
|
+
this.#cab = new AbortController()
|
|
217
|
+
this.messageContext = this.#messageContextFactory()
|
|
218
|
+
|
|
219
|
+
this.#connecting = this.transport
|
|
220
|
+
.connect({
|
|
221
|
+
auth: this.auth,
|
|
222
|
+
application: this.application,
|
|
223
|
+
onMessage: (message) => {
|
|
224
|
+
void this.#onMessage(message)
|
|
225
|
+
},
|
|
226
|
+
onConnect: () => {
|
|
227
|
+
void this.#handleConnected()
|
|
228
|
+
},
|
|
229
|
+
onDisconnect: (reason) => {
|
|
230
|
+
void this.#handleDisconnected(reason)
|
|
231
|
+
},
|
|
296
232
|
})
|
|
297
|
-
.catch((error) => {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
233
|
+
.catch(async (error) => {
|
|
234
|
+
this.messageContext = null
|
|
235
|
+
this.#cab = null
|
|
236
|
+
await this.#handleDisconnected(DEFAULT_CONNECT_ERROR_REASON)
|
|
301
237
|
throw error
|
|
302
238
|
})
|
|
303
239
|
.finally(() => {
|
|
304
|
-
this
|
|
305
|
-
|
|
306
|
-
if (emitDisconnectOnFailure && !this._disposed) {
|
|
307
|
-
this._state = 'disconnected'
|
|
308
|
-
this._lastDisconnectReason = emitDisconnectOnFailure
|
|
309
|
-
void this.onDisconnect(emitDisconnectOnFailure)
|
|
310
|
-
}
|
|
240
|
+
this.#connecting = null
|
|
311
241
|
})
|
|
312
242
|
|
|
313
|
-
return this
|
|
243
|
+
return this.#connecting
|
|
314
244
|
}
|
|
315
245
|
|
|
316
|
-
async disconnect(
|
|
317
|
-
|
|
318
|
-
// Ensure connect() won't short-circuit while the transport is closing.
|
|
319
|
-
this._state = 'disconnected'
|
|
320
|
-
this._lastDisconnectReason = 'client'
|
|
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
|
-
}
|
|
246
|
+
async disconnect(reason: ClientDisconnectReason = 'client') {
|
|
247
|
+
this.#cancelReconnectLoop()
|
|
329
248
|
|
|
330
|
-
|
|
331
|
-
await this
|
|
332
|
-
|
|
333
|
-
this.cab = null
|
|
249
|
+
if (this.transport.type === ConnectionType.Unidirectional) {
|
|
250
|
+
await this.#handleDisconnected(reason)
|
|
251
|
+
return
|
|
334
252
|
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
blob(
|
|
338
|
-
source: Blob | ReadableStream | string | AsyncIterable<Uint8Array>,
|
|
339
|
-
metadata?: ProtocolBlobMetadata,
|
|
340
|
-
) {
|
|
341
|
-
return ProtocolBlob.from(source, metadata)
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
protected async _call(
|
|
345
|
-
procedure: string,
|
|
346
|
-
payload: any,
|
|
347
|
-
options: ClientCallOptions = {},
|
|
348
|
-
) {
|
|
349
|
-
const timeout = options.timeout ?? this.options.timeout
|
|
350
|
-
const controller = new AbortController()
|
|
351
|
-
|
|
352
|
-
// attach all abort signals
|
|
353
|
-
const signals: AbortSignal[] = [controller.signal]
|
|
354
253
|
|
|
355
|
-
if (
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
const callId = this.getCallId()
|
|
362
|
-
const call = createFuture() as ProtocolClientCall
|
|
363
|
-
call.procedure = procedure
|
|
364
|
-
call.signal = signal
|
|
254
|
+
if (this.#state === 'idle' || this.#state === 'disconnected') {
|
|
255
|
+
this.#lastDisconnectReason = reason
|
|
256
|
+
this.#setState('disconnected')
|
|
257
|
+
return
|
|
258
|
+
}
|
|
365
259
|
|
|
366
|
-
this
|
|
367
|
-
this.emitClientEvent({
|
|
368
|
-
kind: 'rpc_request',
|
|
369
|
-
timestamp: Date.now(),
|
|
370
|
-
callId,
|
|
371
|
-
procedure,
|
|
372
|
-
body: payload,
|
|
373
|
-
})
|
|
374
|
-
|
|
375
|
-
// Check if signal is already aborted before proceeding
|
|
376
|
-
if (signal?.aborted) {
|
|
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
|
+
this.#setState('disconnecting')
|
|
406
261
|
|
|
262
|
+
if (this.#cab && !this.#cab.signal.aborted) {
|
|
407
263
|
try {
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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)
|
|
264
|
+
this.#cab.abort(reason)
|
|
265
|
+
} catch {
|
|
266
|
+
this.#cab.abort()
|
|
437
267
|
}
|
|
438
268
|
}
|
|
439
269
|
|
|
440
|
-
|
|
441
|
-
(
|
|
442
|
-
if (value instanceof ProtocolServerRPCStream) {
|
|
443
|
-
return value.createAsyncIterable(() => {
|
|
444
|
-
controller.abort()
|
|
445
|
-
})
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
if (options._stream_response && typeof value === 'function') {
|
|
449
|
-
return value
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
controller.abort()
|
|
453
|
-
return value
|
|
454
|
-
},
|
|
455
|
-
(err) => {
|
|
456
|
-
controller.abort()
|
|
457
|
-
throw err
|
|
458
|
-
},
|
|
459
|
-
)
|
|
270
|
+
try {
|
|
271
|
+
await this.transport.disconnect()
|
|
460
272
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
})
|
|
468
|
-
} else {
|
|
469
|
-
return await result.finally(() => {
|
|
470
|
-
this.calls.delete(callId)
|
|
471
|
-
})
|
|
273
|
+
if (this.#state === 'disconnecting') {
|
|
274
|
+
await this.#handleDisconnected(reason)
|
|
275
|
+
}
|
|
276
|
+
} catch (error) {
|
|
277
|
+
await this.#handleDisconnected(reason)
|
|
278
|
+
throw error
|
|
472
279
|
}
|
|
473
280
|
}
|
|
474
281
|
|
|
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?.()
|
|
282
|
+
requestReconnect(reason: ClientDisconnectReason = 'server') {
|
|
283
|
+
if (this.transport.type !== ConnectionType.Bidirectional) {
|
|
284
|
+
return Promise.resolve()
|
|
488
285
|
}
|
|
489
|
-
this.emit('connected')
|
|
490
|
-
}
|
|
491
286
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
reason === 'client' && this.clientDisconnectAsReconnect
|
|
495
|
-
? (this.clientDisconnectOverrideReason ?? 'server')
|
|
496
|
-
: reason
|
|
287
|
+
this.#clientDisconnectAsReconnect = true
|
|
288
|
+
this.#clientDisconnectOverrideReason = reason
|
|
497
289
|
|
|
498
|
-
this.
|
|
499
|
-
|
|
290
|
+
return this.disconnect('client')
|
|
291
|
+
}
|
|
500
292
|
|
|
501
|
-
|
|
502
|
-
this
|
|
503
|
-
this.emitClientEvent({
|
|
504
|
-
kind: 'disconnected',
|
|
505
|
-
timestamp: Date.now(),
|
|
506
|
-
reason: effectiveReason,
|
|
507
|
-
})
|
|
293
|
+
dispose() {
|
|
294
|
+
if (this.#disposed) return
|
|
508
295
|
|
|
509
|
-
|
|
296
|
+
this.#disposed = true
|
|
297
|
+
this.#cancelReconnectLoop()
|
|
510
298
|
this.messageContext = null
|
|
511
299
|
|
|
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) {
|
|
300
|
+
if (this.#cab && !this.#cab.signal.aborted) {
|
|
528
301
|
try {
|
|
529
|
-
this
|
|
302
|
+
this.#cab.abort('dispose')
|
|
530
303
|
} catch {
|
|
531
|
-
this
|
|
304
|
+
this.#cab.abort()
|
|
532
305
|
}
|
|
533
|
-
this.cab = null
|
|
534
306
|
}
|
|
535
307
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
308
|
+
if (
|
|
309
|
+
this.transport.type === ConnectionType.Bidirectional &&
|
|
310
|
+
(this.#state === 'connecting' || this.#state === 'connected')
|
|
311
|
+
) {
|
|
312
|
+
void this.transport.disconnect().catch(noopFn)
|
|
540
313
|
}
|
|
541
314
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
315
|
+
for (let i = this.#plugins.length - 1; i >= 0; i--) {
|
|
316
|
+
this.#plugins[i].dispose?.()
|
|
317
|
+
}
|
|
545
318
|
}
|
|
546
319
|
|
|
547
|
-
|
|
548
|
-
if (this.
|
|
549
|
-
|
|
320
|
+
send(buffer: ArrayBufferView, signal?: AbortSignal) {
|
|
321
|
+
if (this.transport.type !== ConnectionType.Bidirectional) {
|
|
322
|
+
throw new Error('Invalid transport type for send')
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return this.transport.send(buffer, { signal })
|
|
550
326
|
}
|
|
551
327
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
328
|
+
transportCall(
|
|
329
|
+
context: TransportCallContext,
|
|
330
|
+
rpc: TransportRpcParams,
|
|
331
|
+
options: TransportCallOptions,
|
|
332
|
+
): Promise<TransportCallResponse> {
|
|
333
|
+
if (this.transport.type !== ConnectionType.Unidirectional) {
|
|
334
|
+
throw new Error('Invalid transport type for call')
|
|
558
335
|
}
|
|
559
336
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
this.pendingPings.set(nonce, future)
|
|
563
|
-
|
|
564
|
-
const buffer = this.protocol.encodeMessage(
|
|
565
|
-
this.messageContext,
|
|
566
|
-
ClientMessageType.Ping,
|
|
567
|
-
{ nonce },
|
|
568
|
-
)
|
|
337
|
+
return this.transport.call(context, rpc, options)
|
|
338
|
+
}
|
|
569
339
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
340
|
+
emitClientEvent(event: ClientPluginEvent) {
|
|
341
|
+
for (const plugin of this.#plugins) {
|
|
342
|
+
try {
|
|
343
|
+
const result = plugin.onClientEvent?.(event)
|
|
344
|
+
Promise.resolve(result).catch(noopFn)
|
|
345
|
+
} catch {}
|
|
346
|
+
}
|
|
577
347
|
}
|
|
578
348
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
349
|
+
emitStreamEvent(event: StreamEvent) {
|
|
350
|
+
this.emitClientEvent({
|
|
351
|
+
kind: 'stream_event',
|
|
352
|
+
timestamp: Date.now(),
|
|
353
|
+
...event,
|
|
354
|
+
})
|
|
584
355
|
}
|
|
585
356
|
|
|
586
|
-
|
|
357
|
+
async #onMessage(buffer: ArrayBufferView) {
|
|
587
358
|
if (!this.messageContext) return
|
|
588
359
|
|
|
589
360
|
const message = this.protocol.decodeMessage(this.messageContext, buffer)
|
|
590
|
-
|
|
361
|
+
|
|
362
|
+
for (const plugin of this.#plugins) {
|
|
591
363
|
plugin.onServerMessage?.(message, buffer)
|
|
592
364
|
}
|
|
365
|
+
|
|
593
366
|
this.emitClientEvent({
|
|
594
367
|
kind: 'server_message',
|
|
595
368
|
timestamp: Date.now(),
|
|
@@ -598,450 +371,170 @@ export abstract class BaseClient<
|
|
|
598
371
|
body: message,
|
|
599
372
|
})
|
|
600
373
|
|
|
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
|
-
}
|
|
374
|
+
this.emit('message', message, buffer)
|
|
756
375
|
}
|
|
757
376
|
|
|
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
|
-
}
|
|
377
|
+
async #handleConnected() {
|
|
378
|
+
this.#reconnectTimeout =
|
|
379
|
+
this.#reconnectConfig?.initialTimeout ?? DEFAULT_RECONNECT_TIMEOUT
|
|
380
|
+
this.#reconnectImmediate = false
|
|
381
|
+
|
|
382
|
+
this.#setState('connected')
|
|
383
|
+
this.#lastDisconnectReason = 'server'
|
|
384
|
+
|
|
385
|
+
this.emitClientEvent({
|
|
386
|
+
kind: 'connected',
|
|
387
|
+
timestamp: Date.now(),
|
|
388
|
+
transportType:
|
|
389
|
+
this.transport.type === ConnectionType.Bidirectional
|
|
390
|
+
? 'bidirectional'
|
|
391
|
+
: 'unidirectional',
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
for (const plugin of this.#plugins) {
|
|
395
|
+
await plugin.onConnect?.()
|
|
800
396
|
}
|
|
397
|
+
|
|
398
|
+
this.emit('connected')
|
|
801
399
|
}
|
|
802
400
|
|
|
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)
|
|
401
|
+
async #handleDisconnected(reason: ClientDisconnectReason) {
|
|
402
|
+
const effectiveReason =
|
|
403
|
+
reason === 'client' && this.#clientDisconnectAsReconnect
|
|
404
|
+
? (this.#clientDisconnectOverrideReason ?? 'server')
|
|
405
|
+
: reason
|
|
406
|
+
|
|
407
|
+
this.#clientDisconnectAsReconnect = false
|
|
408
|
+
this.#clientDisconnectOverrideReason = null
|
|
409
|
+
|
|
410
|
+
const shouldSkip =
|
|
411
|
+
this.#state === 'disconnected' &&
|
|
412
|
+
this.messageContext === null &&
|
|
413
|
+
this.#lastDisconnectReason === effectiveReason
|
|
414
|
+
|
|
415
|
+
this.messageContext = null
|
|
416
|
+
|
|
417
|
+
if (this.#cab) {
|
|
418
|
+
if (!this.#cab.signal.aborted) {
|
|
419
|
+
try {
|
|
420
|
+
this.#cab.abort(reason)
|
|
421
|
+
} catch {
|
|
422
|
+
this.#cab.abort()
|
|
880
423
|
}
|
|
881
424
|
}
|
|
425
|
+
this.#cab = null
|
|
882
426
|
}
|
|
883
|
-
}
|
|
884
427
|
|
|
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
|
-
}
|
|
428
|
+
if (shouldSkip) return
|
|
429
|
+
|
|
430
|
+
this.#lastDisconnectReason = effectiveReason
|
|
431
|
+
this.#setState('disconnected')
|
|
432
|
+
|
|
433
|
+
this.emitClientEvent({
|
|
434
|
+
kind: 'disconnected',
|
|
435
|
+
timestamp: Date.now(),
|
|
436
|
+
reason: effectiveReason,
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
this.emit('disconnected', effectiveReason)
|
|
440
|
+
|
|
441
|
+
for (let i = this.#plugins.length - 1; i >= 0; i--) {
|
|
442
|
+
await this.#plugins[i].onDisconnect?.(effectiveReason)
|
|
1003
443
|
}
|
|
1004
|
-
}
|
|
1005
444
|
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
return this.transport.send(buffer, { signal })
|
|
445
|
+
if (this.#shouldReconnect(effectiveReason)) {
|
|
446
|
+
this.#ensureReconnectLoop()
|
|
447
|
+
}
|
|
1010
448
|
}
|
|
1011
449
|
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
450
|
+
#setState(next: ConnectionState) {
|
|
451
|
+
if (next === this.#state) return
|
|
452
|
+
|
|
453
|
+
const previous = this.#state
|
|
454
|
+
this.#state = next
|
|
455
|
+
|
|
1018
456
|
this.emitClientEvent({
|
|
1019
|
-
kind: '
|
|
457
|
+
kind: 'state_changed',
|
|
1020
458
|
timestamp: Date.now(),
|
|
1021
|
-
|
|
459
|
+
state: next,
|
|
460
|
+
previous,
|
|
1022
461
|
})
|
|
462
|
+
|
|
463
|
+
this.emit('state_changed', next, previous)
|
|
1023
464
|
}
|
|
1024
465
|
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
this
|
|
1028
|
-
|
|
1029
|
-
|
|
466
|
+
#shouldReconnect(reason: ClientDisconnectReason) {
|
|
467
|
+
return (
|
|
468
|
+
!this.#disposed &&
|
|
469
|
+
!!this.#reconnectConfig &&
|
|
470
|
+
this.transport.type === ConnectionType.Bidirectional &&
|
|
471
|
+
reason !== 'client'
|
|
472
|
+
)
|
|
1030
473
|
}
|
|
1031
474
|
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
475
|
+
#cancelReconnectLoop() {
|
|
476
|
+
this.#reconnectImmediate = false
|
|
477
|
+
this.#reconnectController?.abort()
|
|
478
|
+
this.#reconnectController = null
|
|
479
|
+
this.#reconnectPromise = null
|
|
1037
480
|
}
|
|
1038
481
|
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
482
|
+
#ensureReconnectLoop() {
|
|
483
|
+
if (this.#reconnectPromise || !this.#reconnectConfig) return
|
|
484
|
+
|
|
485
|
+
const signal = new AbortController()
|
|
486
|
+
this.#reconnectController = signal
|
|
487
|
+
|
|
488
|
+
this.#reconnectPromise = (async () => {
|
|
489
|
+
while (
|
|
490
|
+
!signal.signal.aborted &&
|
|
491
|
+
!this.#disposed &&
|
|
492
|
+
this.#reconnectConfig &&
|
|
493
|
+
(this.#state === 'disconnected' || this.#state === 'idle') &&
|
|
494
|
+
this.#lastDisconnectReason !== 'client'
|
|
495
|
+
) {
|
|
496
|
+
if (this.#reconnectPauseReasons.size) {
|
|
497
|
+
await sleep(1000, signal.signal)
|
|
498
|
+
continue
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const delay = this.#reconnectImmediate
|
|
502
|
+
? 0
|
|
503
|
+
: computeReconnectDelay(this.#reconnectTimeout)
|
|
504
|
+
this.#reconnectImmediate = false
|
|
505
|
+
|
|
506
|
+
if (delay > 0) {
|
|
507
|
+
await sleep(delay, signal.signal)
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const currentState = this.state
|
|
511
|
+
|
|
512
|
+
if (
|
|
513
|
+
signal.signal.aborted ||
|
|
514
|
+
this.#disposed ||
|
|
515
|
+
!this.#reconnectConfig ||
|
|
516
|
+
currentState === 'connected' ||
|
|
517
|
+
currentState === 'connecting'
|
|
518
|
+
) {
|
|
519
|
+
break
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const previousTimeout = this.#reconnectTimeout
|
|
523
|
+
|
|
524
|
+
await this.connect().catch(noopFn)
|
|
525
|
+
|
|
526
|
+
if (this.state !== 'connected' && this.#reconnectConfig) {
|
|
527
|
+
this.#reconnectTimeout = Math.min(
|
|
528
|
+
previousTimeout * 2,
|
|
529
|
+
this.#reconnectConfig.maxTimeout ?? DEFAULT_MAX_RECONNECT_TIMEOUT,
|
|
530
|
+
)
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
})().finally(() => {
|
|
534
|
+
if (this.#reconnectController === signal) {
|
|
535
|
+
this.#reconnectController = null
|
|
536
|
+
}
|
|
537
|
+
this.#reconnectPromise = null
|
|
538
|
+
})
|
|
1046
539
|
}
|
|
1047
540
|
}
|