@nmtjs/client 0.15.2 → 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
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import type { ProtocolBlob, ProtocolBlobMetadata } from '@nmtjs/protocol'
|
|
2
|
+
import type {
|
|
3
|
+
ProtocolClientBlobStream,
|
|
4
|
+
ProtocolServerBlobConsumer,
|
|
5
|
+
} from '@nmtjs/protocol/client'
|
|
6
|
+
import { MAX_UINT32, noopFn } from '@nmtjs/common'
|
|
7
|
+
import { ClientMessageType, kBlobKey, ServerMessageType } from '@nmtjs/protocol'
|
|
8
|
+
import { ProtocolServerBlobStream } from '@nmtjs/protocol/client'
|
|
9
|
+
|
|
10
|
+
import type { ClientCore } from '../core.ts'
|
|
11
|
+
import { ClientStreams, ServerStreams } from '../streams.ts'
|
|
12
|
+
|
|
13
|
+
const DEFAULT_PULL_SIZE = 65535
|
|
14
|
+
|
|
15
|
+
const toReasonString = (reason: unknown) => {
|
|
16
|
+
if (typeof reason === 'string') return reason
|
|
17
|
+
if (reason === undefined || reason === null) return undefined
|
|
18
|
+
return String(reason)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface StreamLayerApi {
|
|
22
|
+
readonly clientStreams: ClientStreams
|
|
23
|
+
readonly serverStreams: ServerStreams
|
|
24
|
+
getStreamId: () => number
|
|
25
|
+
addClientStream: (blob: ProtocolBlob) => ProtocolClientBlobStream
|
|
26
|
+
createServerBlobStream: (
|
|
27
|
+
streamId: number,
|
|
28
|
+
metadata: ProtocolBlobMetadata,
|
|
29
|
+
) => ProtocolServerBlobConsumer
|
|
30
|
+
addServerBlobStream: (metadata: ProtocolBlobMetadata) => {
|
|
31
|
+
streamId: number
|
|
32
|
+
stream: ProtocolServerBlobStream
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const createServerBlobConsumer = (
|
|
37
|
+
metadata: ProtocolBlobMetadata,
|
|
38
|
+
subscribe: (options?: { signal?: AbortSignal }) => ProtocolServerBlobStream,
|
|
39
|
+
): ProtocolServerBlobConsumer => {
|
|
40
|
+
const consumer = ((options?: { signal?: AbortSignal }) =>
|
|
41
|
+
subscribe(options)) as ProtocolServerBlobConsumer
|
|
42
|
+
|
|
43
|
+
Object.defineProperties(consumer, {
|
|
44
|
+
metadata: {
|
|
45
|
+
configurable: false,
|
|
46
|
+
enumerable: true,
|
|
47
|
+
writable: false,
|
|
48
|
+
value: metadata,
|
|
49
|
+
},
|
|
50
|
+
[kBlobKey]: {
|
|
51
|
+
configurable: false,
|
|
52
|
+
enumerable: false,
|
|
53
|
+
writable: false,
|
|
54
|
+
value: true,
|
|
55
|
+
},
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
return consumer
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const createStreamLayer = (core: ClientCore): StreamLayerApi => {
|
|
62
|
+
const clientStreams = new ClientStreams()
|
|
63
|
+
const serverStreams = new ServerStreams()
|
|
64
|
+
|
|
65
|
+
let streamId = 0
|
|
66
|
+
|
|
67
|
+
const getStreamId = () => {
|
|
68
|
+
if (streamId >= MAX_UINT32) {
|
|
69
|
+
streamId = 0
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return streamId++
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const addClientStream = (blob: ProtocolBlob) => {
|
|
76
|
+
const id = getStreamId()
|
|
77
|
+
return clientStreams.add(blob.source, id, blob.metadata)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const createServerBlobStream = (
|
|
81
|
+
id: number,
|
|
82
|
+
metadata: ProtocolBlobMetadata,
|
|
83
|
+
) => {
|
|
84
|
+
const stream = new ProtocolServerBlobStream(metadata, {
|
|
85
|
+
pull: () => {
|
|
86
|
+
if (!core.messageContext) return
|
|
87
|
+
|
|
88
|
+
core.emitStreamEvent({
|
|
89
|
+
direction: 'outgoing',
|
|
90
|
+
streamType: 'server_blob',
|
|
91
|
+
action: 'pull',
|
|
92
|
+
streamId: id,
|
|
93
|
+
byteLength: DEFAULT_PULL_SIZE,
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
const buffer = core.protocol.encodeMessage(
|
|
97
|
+
core.messageContext,
|
|
98
|
+
ClientMessageType.ServerStreamPull,
|
|
99
|
+
{ streamId: id, size: DEFAULT_PULL_SIZE },
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
core.send(buffer).catch(noopFn)
|
|
103
|
+
},
|
|
104
|
+
close: () => {
|
|
105
|
+
serverStreams.remove(id)
|
|
106
|
+
},
|
|
107
|
+
readableStrategy: { highWaterMark: 0 },
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
serverStreams.add(id, stream)
|
|
111
|
+
|
|
112
|
+
return createServerBlobConsumer(metadata, ({ signal } = {}) => {
|
|
113
|
+
if (signal) {
|
|
114
|
+
signal.addEventListener(
|
|
115
|
+
'abort',
|
|
116
|
+
() => {
|
|
117
|
+
if (!core.messageContext) return
|
|
118
|
+
|
|
119
|
+
core.emitStreamEvent({
|
|
120
|
+
direction: 'outgoing',
|
|
121
|
+
streamType: 'server_blob',
|
|
122
|
+
action: 'abort',
|
|
123
|
+
streamId: id,
|
|
124
|
+
reason: toReasonString(signal.reason),
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
const buffer = core.protocol.encodeMessage(
|
|
128
|
+
core.messageContext,
|
|
129
|
+
ClientMessageType.ServerStreamAbort,
|
|
130
|
+
{ streamId: id, reason: toReasonString(signal.reason) },
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
core.send(buffer).catch(noopFn)
|
|
134
|
+
void serverStreams.abort(id).catch(noopFn)
|
|
135
|
+
},
|
|
136
|
+
{ once: true },
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return stream
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const addServerBlobStream = (metadata: ProtocolBlobMetadata) => {
|
|
145
|
+
const id = getStreamId()
|
|
146
|
+
const stream = new ProtocolServerBlobStream(metadata)
|
|
147
|
+
serverStreams.add(id, stream)
|
|
148
|
+
return { streamId: id, stream }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
core.on('message', (message: any) => {
|
|
152
|
+
switch (message.type) {
|
|
153
|
+
case ServerMessageType.ServerStreamPush:
|
|
154
|
+
core.emitStreamEvent({
|
|
155
|
+
direction: 'incoming',
|
|
156
|
+
streamType: 'server_blob',
|
|
157
|
+
action: 'push',
|
|
158
|
+
streamId: message.streamId,
|
|
159
|
+
byteLength: message.chunk.byteLength,
|
|
160
|
+
})
|
|
161
|
+
void serverStreams.push(message.streamId, message.chunk)
|
|
162
|
+
break
|
|
163
|
+
case ServerMessageType.ServerStreamEnd:
|
|
164
|
+
core.emitStreamEvent({
|
|
165
|
+
direction: 'incoming',
|
|
166
|
+
streamType: 'server_blob',
|
|
167
|
+
action: 'end',
|
|
168
|
+
streamId: message.streamId,
|
|
169
|
+
})
|
|
170
|
+
void serverStreams.end(message.streamId)
|
|
171
|
+
break
|
|
172
|
+
case ServerMessageType.ServerStreamAbort:
|
|
173
|
+
core.emitStreamEvent({
|
|
174
|
+
direction: 'incoming',
|
|
175
|
+
streamType: 'server_blob',
|
|
176
|
+
action: 'abort',
|
|
177
|
+
streamId: message.streamId,
|
|
178
|
+
reason: message.reason,
|
|
179
|
+
})
|
|
180
|
+
void serverStreams.abort(message.streamId)
|
|
181
|
+
break
|
|
182
|
+
case ServerMessageType.ClientStreamPull:
|
|
183
|
+
core.emitStreamEvent({
|
|
184
|
+
direction: 'incoming',
|
|
185
|
+
streamType: 'client_blob',
|
|
186
|
+
action: 'pull',
|
|
187
|
+
streamId: message.streamId,
|
|
188
|
+
byteLength: message.size,
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
void clientStreams.pull(message.streamId, message.size).then(
|
|
192
|
+
(chunk) => {
|
|
193
|
+
if (!core.messageContext) return
|
|
194
|
+
|
|
195
|
+
if (chunk) {
|
|
196
|
+
core.emitStreamEvent({
|
|
197
|
+
direction: 'outgoing',
|
|
198
|
+
streamType: 'client_blob',
|
|
199
|
+
action: 'push',
|
|
200
|
+
streamId: message.streamId,
|
|
201
|
+
byteLength: chunk.byteLength,
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
const buffer = core.protocol.encodeMessage(
|
|
205
|
+
core.messageContext,
|
|
206
|
+
ClientMessageType.ClientStreamPush,
|
|
207
|
+
{ streamId: message.streamId, chunk },
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
core.send(buffer).catch(noopFn)
|
|
211
|
+
return
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
core.emitStreamEvent({
|
|
215
|
+
direction: 'outgoing',
|
|
216
|
+
streamType: 'client_blob',
|
|
217
|
+
action: 'end',
|
|
218
|
+
streamId: message.streamId,
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
const buffer = core.protocol.encodeMessage(
|
|
222
|
+
core.messageContext,
|
|
223
|
+
ClientMessageType.ClientStreamEnd,
|
|
224
|
+
{ streamId: message.streamId },
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
core.send(buffer).catch(noopFn)
|
|
228
|
+
void clientStreams.end(message.streamId).catch(noopFn)
|
|
229
|
+
},
|
|
230
|
+
() => {
|
|
231
|
+
if (!core.messageContext) return
|
|
232
|
+
|
|
233
|
+
core.emitStreamEvent({
|
|
234
|
+
direction: 'outgoing',
|
|
235
|
+
streamType: 'client_blob',
|
|
236
|
+
action: 'abort',
|
|
237
|
+
streamId: message.streamId,
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
const buffer = core.protocol.encodeMessage(
|
|
241
|
+
core.messageContext,
|
|
242
|
+
ClientMessageType.ClientStreamAbort,
|
|
243
|
+
{ streamId: message.streamId },
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
core.send(buffer).catch(noopFn)
|
|
247
|
+
clientStreams.remove(message.streamId)
|
|
248
|
+
},
|
|
249
|
+
)
|
|
250
|
+
break
|
|
251
|
+
case ServerMessageType.ClientStreamAbort:
|
|
252
|
+
core.emitStreamEvent({
|
|
253
|
+
direction: 'incoming',
|
|
254
|
+
streamType: 'client_blob',
|
|
255
|
+
action: 'abort',
|
|
256
|
+
streamId: message.streamId,
|
|
257
|
+
reason: message.reason,
|
|
258
|
+
})
|
|
259
|
+
void clientStreams.abort(message.streamId, message.reason).catch(noopFn)
|
|
260
|
+
break
|
|
261
|
+
}
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
core.on('disconnected', (reason) => {
|
|
265
|
+
void clientStreams.clear(reason).catch(noopFn)
|
|
266
|
+
void serverStreams.clear(reason).catch(noopFn)
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
clientStreams,
|
|
271
|
+
serverStreams,
|
|
272
|
+
getStreamId,
|
|
273
|
+
addClientStream,
|
|
274
|
+
createServerBlobStream,
|
|
275
|
+
addServerBlobStream,
|
|
276
|
+
}
|
|
277
|
+
}
|
package/src/plugins/browser.ts
CHANGED
|
@@ -1,32 +1,60 @@
|
|
|
1
1
|
import type { ClientPlugin } from './types.ts'
|
|
2
2
|
|
|
3
|
+
const syncPauseReasons = (
|
|
4
|
+
setPauseReason: (reason: string, active: boolean) => void,
|
|
5
|
+
) => {
|
|
6
|
+
if (globalThis.window && 'navigator' in globalThis.window) {
|
|
7
|
+
setPauseReason('offline', globalThis.window.navigator?.onLine === false)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (globalThis.document) {
|
|
11
|
+
setPauseReason(
|
|
12
|
+
'tab_hidden',
|
|
13
|
+
globalThis.document.visibilityState === 'hidden',
|
|
14
|
+
)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
3
18
|
export const browserConnectivityPlugin = (): ClientPlugin => {
|
|
4
|
-
return (
|
|
19
|
+
return ({ core }) => {
|
|
5
20
|
const cleanup: Array<() => void> = []
|
|
6
21
|
|
|
7
|
-
const
|
|
8
|
-
if (
|
|
9
|
-
|
|
22
|
+
const triggerReconnect = () => {
|
|
23
|
+
if (!core.isDisposed()) {
|
|
24
|
+
core.triggerReconnect()
|
|
10
25
|
}
|
|
11
26
|
}
|
|
12
27
|
|
|
13
28
|
return {
|
|
14
29
|
name: 'browser-connectivity',
|
|
15
30
|
onInit: () => {
|
|
31
|
+
syncPauseReasons(core.setReconnectPauseReason.bind(core))
|
|
32
|
+
|
|
16
33
|
if (globalThis.window) {
|
|
17
|
-
const onPageShow = () =>
|
|
34
|
+
const onPageShow = () => triggerReconnect()
|
|
18
35
|
globalThis.window.addEventListener('pageshow', onPageShow)
|
|
19
36
|
cleanup.push(() =>
|
|
20
37
|
globalThis.window?.removeEventListener('pageshow', onPageShow),
|
|
21
38
|
)
|
|
22
39
|
|
|
23
|
-
const onOnline = () =>
|
|
40
|
+
const onOnline = () => {
|
|
41
|
+
core.setReconnectPauseReason('offline', false)
|
|
42
|
+
triggerReconnect()
|
|
43
|
+
}
|
|
24
44
|
globalThis.window.addEventListener('online', onOnline)
|
|
25
45
|
cleanup.push(() =>
|
|
26
46
|
globalThis.window?.removeEventListener('online', onOnline),
|
|
27
47
|
)
|
|
28
48
|
|
|
29
|
-
const
|
|
49
|
+
const onOffline = () => {
|
|
50
|
+
core.setReconnectPauseReason('offline', true)
|
|
51
|
+
}
|
|
52
|
+
globalThis.window.addEventListener('offline', onOffline)
|
|
53
|
+
cleanup.push(() =>
|
|
54
|
+
globalThis.window?.removeEventListener('offline', onOffline),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
const onFocus = () => triggerReconnect()
|
|
30
58
|
globalThis.window.addEventListener('focus', onFocus)
|
|
31
59
|
cleanup.push(() =>
|
|
32
60
|
globalThis.window?.removeEventListener('focus', onFocus),
|
|
@@ -35,8 +63,10 @@ export const browserConnectivityPlugin = (): ClientPlugin => {
|
|
|
35
63
|
|
|
36
64
|
if (globalThis.document) {
|
|
37
65
|
const onVisibilityChange = () => {
|
|
38
|
-
|
|
39
|
-
|
|
66
|
+
const hidden = globalThis.document?.visibilityState === 'hidden'
|
|
67
|
+
core.setReconnectPauseReason('tab_hidden', hidden)
|
|
68
|
+
if (!hidden) {
|
|
69
|
+
triggerReconnect()
|
|
40
70
|
}
|
|
41
71
|
}
|
|
42
72
|
|
package/src/plugins/heartbeat.ts
CHANGED
|
@@ -40,7 +40,7 @@ export interface HeartbeatPluginOptions {
|
|
|
40
40
|
export const heartbeatPlugin = (
|
|
41
41
|
options: HeartbeatPluginOptions = {},
|
|
42
42
|
): ClientPlugin => {
|
|
43
|
-
return (
|
|
43
|
+
return ({ core, ping }) => {
|
|
44
44
|
const interval = options.interval ?? DEFAULT_HEARTBEAT_INTERVAL
|
|
45
45
|
const timeout = options.timeout ?? DEFAULT_HEARTBEAT_TIMEOUT
|
|
46
46
|
|
|
@@ -55,7 +55,7 @@ export const heartbeatPlugin = (
|
|
|
55
55
|
|
|
56
56
|
const startHeartbeat = () => {
|
|
57
57
|
if (heartbeatTask) return
|
|
58
|
-
if (
|
|
58
|
+
if (core.transportType !== ConnectionType.Bidirectional) return
|
|
59
59
|
|
|
60
60
|
heartbeatAbortController = new AbortController()
|
|
61
61
|
const signal = heartbeatAbortController.signal
|
|
@@ -63,8 +63,8 @@ export const heartbeatPlugin = (
|
|
|
63
63
|
heartbeatTask = (async () => {
|
|
64
64
|
while (
|
|
65
65
|
!signal.aborted &&
|
|
66
|
-
!
|
|
67
|
-
|
|
66
|
+
!core.isDisposed() &&
|
|
67
|
+
core.state === 'connected'
|
|
68
68
|
) {
|
|
69
69
|
if (isPaused()) {
|
|
70
70
|
await sleep(1000, signal)
|
|
@@ -75,21 +75,21 @@ export const heartbeatPlugin = (
|
|
|
75
75
|
|
|
76
76
|
if (
|
|
77
77
|
signal.aborted ||
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
core.isDisposed() ||
|
|
79
|
+
core.state !== 'connected'
|
|
80
80
|
) {
|
|
81
81
|
continue
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
try {
|
|
85
|
-
await
|
|
85
|
+
await ping.ping(timeout, signal)
|
|
86
86
|
} catch {
|
|
87
87
|
if (
|
|
88
88
|
!signal.aborted &&
|
|
89
|
-
!
|
|
90
|
-
|
|
89
|
+
!core.isDisposed() &&
|
|
90
|
+
core.state === 'connected'
|
|
91
91
|
) {
|
|
92
|
-
await
|
|
92
|
+
await core
|
|
93
93
|
.requestReconnect('heartbeat_timeout')
|
|
94
94
|
.catch(() => void 0)
|
|
95
95
|
}
|
package/src/plugins/index.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
ClientPlugin,
|
|
3
|
+
ClientPluginContext,
|
|
4
|
+
ClientPluginEvent,
|
|
5
|
+
ClientPluginInstance,
|
|
6
|
+
ReconnectConfig,
|
|
7
|
+
StreamEvent,
|
|
8
|
+
} from './types.ts'
|
|
1
9
|
export * from './browser.ts'
|
|
2
10
|
export * from './heartbeat.ts'
|
|
3
11
|
export * from './logging.ts'
|
|
4
12
|
export * from './reconnect.ts'
|
|
5
|
-
export * from './types.ts'
|
package/src/plugins/reconnect.ts
CHANGED
|
@@ -1,44 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
import type { ClientDisconnectReason, ClientPlugin } from './types.ts'
|
|
4
|
-
|
|
5
|
-
const DEFAULT_RECONNECT_TIMEOUT = 1000
|
|
6
|
-
const DEFAULT_MAX_RECONNECT_TIMEOUT = 60000
|
|
7
|
-
|
|
8
|
-
const sleep = (ms: number, signal?: AbortSignal) => {
|
|
9
|
-
return new Promise<void>((resolve) => {
|
|
10
|
-
if (signal?.aborted) return resolve()
|
|
11
|
-
const timer = setTimeout(resolve, ms)
|
|
12
|
-
if (signal) {
|
|
13
|
-
signal.addEventListener(
|
|
14
|
-
'abort',
|
|
15
|
-
() => {
|
|
16
|
-
clearTimeout(timer)
|
|
17
|
-
resolve()
|
|
18
|
-
},
|
|
19
|
-
{ once: true },
|
|
20
|
-
)
|
|
21
|
-
}
|
|
22
|
-
})
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const computeReconnectDelay = (ms: number) => {
|
|
26
|
-
if (globalThis.window) {
|
|
27
|
-
const jitter = Math.floor(ms * 0.2 * Math.random())
|
|
28
|
-
return ms + jitter
|
|
29
|
-
}
|
|
30
|
-
return ms
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const isReconnectPaused = () => {
|
|
34
|
-
if (globalThis.window && 'navigator' in globalThis.window) {
|
|
35
|
-
if (globalThis.window.navigator?.onLine === false) return true
|
|
36
|
-
}
|
|
37
|
-
if (globalThis.document) {
|
|
38
|
-
if (globalThis.document.visibilityState === 'hidden') return true
|
|
39
|
-
}
|
|
40
|
-
return false
|
|
41
|
-
}
|
|
1
|
+
import type { ClientPlugin } from './types.ts'
|
|
42
2
|
|
|
43
3
|
export interface ReconnectPluginOptions {
|
|
44
4
|
initialTimeout?: number
|
|
@@ -48,83 +8,16 @@ export interface ReconnectPluginOptions {
|
|
|
48
8
|
export const reconnectPlugin = (
|
|
49
9
|
options: ReconnectPluginOptions = {},
|
|
50
10
|
): ClientPlugin => {
|
|
51
|
-
return (
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
reconnectAbortController?.abort()
|
|
58
|
-
reconnectAbortController = null
|
|
59
|
-
reconnecting = null
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const ensureReconnectLoop = () => {
|
|
63
|
-
if (reconnecting) return
|
|
64
|
-
|
|
65
|
-
reconnectAbortController = new AbortController()
|
|
66
|
-
const signal = reconnectAbortController.signal
|
|
67
|
-
|
|
68
|
-
reconnecting = (async () => {
|
|
69
|
-
while (
|
|
70
|
-
!signal.aborted &&
|
|
71
|
-
!client.isDisposed() &&
|
|
72
|
-
client.state === 'disconnected' &&
|
|
73
|
-
client.lastDisconnectReason !== 'client'
|
|
74
|
-
) {
|
|
75
|
-
if (isReconnectPaused()) {
|
|
76
|
-
await sleep(1000, signal)
|
|
77
|
-
continue
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const delay = computeReconnectDelay(reconnectTimeout)
|
|
81
|
-
await sleep(delay, signal)
|
|
82
|
-
|
|
83
|
-
if (
|
|
84
|
-
signal.aborted ||
|
|
85
|
-
client.isDisposed() ||
|
|
86
|
-
client.state !== 'disconnected' ||
|
|
87
|
-
client.lastDisconnectReason === 'client'
|
|
88
|
-
) {
|
|
89
|
-
break
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const previousTimeout = reconnectTimeout
|
|
93
|
-
await client.connect().catch(() => void 0)
|
|
94
|
-
|
|
95
|
-
if (client.state === 'disconnected') {
|
|
96
|
-
reconnectTimeout = Math.min(
|
|
97
|
-
previousTimeout * 2,
|
|
98
|
-
options.maxTimeout ?? DEFAULT_MAX_RECONNECT_TIMEOUT,
|
|
99
|
-
)
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
})().finally(() => {
|
|
103
|
-
reconnecting = null
|
|
104
|
-
reconnectAbortController = null
|
|
11
|
+
return ({ core }) => ({
|
|
12
|
+
name: 'reconnect',
|
|
13
|
+
onInit: () => {
|
|
14
|
+
core.configureReconnect({
|
|
15
|
+
initialTimeout: options.initialTimeout,
|
|
16
|
+
maxTimeout: options.maxTimeout,
|
|
105
17
|
})
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
reason === 'client' ||
|
|
112
|
-
client.isDisposed()
|
|
113
|
-
) {
|
|
114
|
-
cancelReconnect()
|
|
115
|
-
return
|
|
116
|
-
}
|
|
117
|
-
ensureReconnectLoop()
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
return {
|
|
121
|
-
name: 'reconnect',
|
|
122
|
-
onConnect: () => {
|
|
123
|
-
reconnectTimeout = options.initialTimeout ?? DEFAULT_RECONNECT_TIMEOUT
|
|
124
|
-
cancelReconnect()
|
|
125
|
-
},
|
|
126
|
-
onDisconnect,
|
|
127
|
-
dispose: cancelReconnect,
|
|
128
|
-
}
|
|
129
|
-
}
|
|
18
|
+
},
|
|
19
|
+
dispose: () => {
|
|
20
|
+
core.configureReconnect(null)
|
|
21
|
+
},
|
|
22
|
+
})
|
|
130
23
|
}
|
package/src/plugins/types.ts
CHANGED
|
@@ -1,8 +1,30 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ClientCore, ConnectionState } from '../core.ts'
|
|
2
|
+
import type { PingLayerApi } from '../layers/ping.ts'
|
|
2
3
|
|
|
3
4
|
export type ClientDisconnectReason = 'client' | 'server' | (string & {})
|
|
4
5
|
|
|
6
|
+
export interface ReconnectConfig {
|
|
7
|
+
initialTimeout?: number
|
|
8
|
+
maxTimeout?: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type StreamEvent = {
|
|
12
|
+
direction: 'incoming' | 'outgoing'
|
|
13
|
+
streamType: 'rpc' | 'client_blob' | 'server_blob'
|
|
14
|
+
action: 'response' | 'pull' | 'push' | 'end' | 'abort'
|
|
15
|
+
callId?: number
|
|
16
|
+
streamId?: number
|
|
17
|
+
byteLength?: number
|
|
18
|
+
reason?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
5
21
|
export type ClientPluginEvent =
|
|
22
|
+
| {
|
|
23
|
+
kind: 'state_changed'
|
|
24
|
+
timestamp: number
|
|
25
|
+
state: ConnectionState
|
|
26
|
+
previous: ConnectionState
|
|
27
|
+
}
|
|
6
28
|
| {
|
|
7
29
|
kind: 'connected'
|
|
8
30
|
timestamp: number
|
|
@@ -38,17 +60,7 @@ export type ClientPluginEvent =
|
|
|
38
60
|
procedure: string
|
|
39
61
|
error: unknown
|
|
40
62
|
}
|
|
41
|
-
| {
|
|
42
|
-
kind: 'stream_event'
|
|
43
|
-
timestamp: number
|
|
44
|
-
direction: 'incoming' | 'outgoing'
|
|
45
|
-
streamType: 'rpc' | 'client_blob' | 'server_blob'
|
|
46
|
-
action: 'response' | 'pull' | 'push' | 'end' | 'abort'
|
|
47
|
-
callId?: number
|
|
48
|
-
streamId?: number
|
|
49
|
-
byteLength?: number
|
|
50
|
-
reason?: string
|
|
51
|
-
}
|
|
63
|
+
| ({ kind: 'stream_event'; timestamp: number } & StreamEvent)
|
|
52
64
|
|
|
53
65
|
/**
|
|
54
66
|
* Client plugin lifecycle contract.
|
|
@@ -67,6 +79,11 @@ export interface ClientPluginInstance {
|
|
|
67
79
|
dispose?(): void
|
|
68
80
|
}
|
|
69
81
|
|
|
82
|
+
export interface ClientPluginContext {
|
|
83
|
+
core: ClientCore
|
|
84
|
+
ping: PingLayerApi
|
|
85
|
+
}
|
|
86
|
+
|
|
70
87
|
export type ClientPlugin = (
|
|
71
|
-
|
|
88
|
+
context: ClientPluginContext,
|
|
72
89
|
) => ClientPluginInstance
|