@nmtjs/gateway 0.15.0-beta.2 → 0.15.0-beta.4
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/api.d.ts +15 -0
- package/dist/api.js +1 -0
- package/dist/api.js.map +1 -0
- package/dist/connections.d.ts +24 -0
- package/dist/connections.js +1 -0
- package/dist/connections.js.map +1 -0
- package/dist/enums.d.ts +13 -0
- package/dist/enums.js +1 -0
- package/dist/enums.js.map +1 -0
- package/dist/gateway.d.ts +66 -0
- package/dist/gateway.js +1 -0
- package/dist/gateway.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -0
- package/dist/injectables.d.ts +30 -0
- package/dist/injectables.js +1 -0
- package/dist/injectables.js.map +1 -0
- package/dist/rpcs.d.ts +13 -0
- package/dist/rpcs.js +1 -0
- package/dist/rpcs.js.map +1 -0
- package/dist/streams.d.ts +64 -0
- package/dist/streams.js +1 -0
- package/dist/streams.js.map +1 -0
- package/dist/transport.d.ts +50 -0
- package/dist/transport.js +1 -0
- package/dist/transport.js.map +1 -0
- package/dist/types.d.ts +17 -0
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -0
- package/package.json +12 -11
- package/src/api.ts +26 -0
- package/src/connections.ts +54 -0
- package/src/enums.ts +15 -0
- package/src/gateway.ts +698 -0
- package/src/index.ts +9 -0
- package/src/injectables.ts +88 -0
- package/src/rpcs.ts +90 -0
- package/src/streams.ts +408 -0
- package/src/transport.ts +95 -0
- package/src/types.ts +24 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { Readable } from 'node:stream'
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
ProtocolBlobInterface,
|
|
5
|
+
ProtocolBlobMetadata,
|
|
6
|
+
} from '@nmtjs/protocol'
|
|
7
|
+
import { anyAbortSignal } from '@nmtjs/common'
|
|
8
|
+
import {
|
|
9
|
+
createFactoryInjectable,
|
|
10
|
+
createLazyInjectable,
|
|
11
|
+
createOptionalInjectable,
|
|
12
|
+
Scope,
|
|
13
|
+
} from '@nmtjs/core'
|
|
14
|
+
|
|
15
|
+
import type { GatewayConnection } from './connections.ts'
|
|
16
|
+
|
|
17
|
+
export const connection = createLazyInjectable<
|
|
18
|
+
GatewayConnection,
|
|
19
|
+
Scope.Connection
|
|
20
|
+
>(Scope.Connection, 'Gateway connection')
|
|
21
|
+
|
|
22
|
+
export const connectionId = createLazyInjectable<
|
|
23
|
+
GatewayConnection['id'],
|
|
24
|
+
Scope.Connection
|
|
25
|
+
>(Scope.Connection, 'Gateway connection id')
|
|
26
|
+
|
|
27
|
+
export const connectionData = createLazyInjectable<unknown, Scope.Connection>(
|
|
28
|
+
Scope.Connection,
|
|
29
|
+
"Gateway connection's data",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
export const connectionAbortSignal = createLazyInjectable<
|
|
33
|
+
AbortSignal,
|
|
34
|
+
Scope.Connection
|
|
35
|
+
>(Scope.Connection, 'Connection abort signal')
|
|
36
|
+
|
|
37
|
+
export const rpcClientAbortSignal = createLazyInjectable<
|
|
38
|
+
AbortSignal,
|
|
39
|
+
Scope.Call
|
|
40
|
+
>(Scope.Call, 'RPC client abort signal')
|
|
41
|
+
|
|
42
|
+
export const rpcStreamAbortSignal = createLazyInjectable<
|
|
43
|
+
AbortSignal,
|
|
44
|
+
Scope.Call
|
|
45
|
+
>(Scope.Call, 'RPC stream abort signal')
|
|
46
|
+
|
|
47
|
+
export const rpcAbortSignal = createFactoryInjectable(
|
|
48
|
+
{
|
|
49
|
+
dependencies: {
|
|
50
|
+
rpcClientAbortSignal,
|
|
51
|
+
connectionAbortSignal,
|
|
52
|
+
rpcStreamAbortSignal: createOptionalInjectable(rpcStreamAbortSignal),
|
|
53
|
+
},
|
|
54
|
+
factory: (ctx) =>
|
|
55
|
+
anyAbortSignal(
|
|
56
|
+
ctx.rpcClientAbortSignal,
|
|
57
|
+
ctx.connectionAbortSignal,
|
|
58
|
+
ctx.rpcStreamAbortSignal,
|
|
59
|
+
),
|
|
60
|
+
},
|
|
61
|
+
'Any RPC abort signal',
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
export const createBlob = createLazyInjectable<
|
|
65
|
+
(
|
|
66
|
+
source:
|
|
67
|
+
| Readable
|
|
68
|
+
| globalThis.ReadableStream
|
|
69
|
+
| File
|
|
70
|
+
| Blob
|
|
71
|
+
| string
|
|
72
|
+
| ArrayBuffer
|
|
73
|
+
| Uint8Array,
|
|
74
|
+
metadata?: ProtocolBlobMetadata,
|
|
75
|
+
) => ProtocolBlobInterface,
|
|
76
|
+
Scope.Call
|
|
77
|
+
>(Scope.Call, 'Create RPC blob')
|
|
78
|
+
|
|
79
|
+
export const GatewayInjectables = {
|
|
80
|
+
connection,
|
|
81
|
+
connectionId,
|
|
82
|
+
connectionData,
|
|
83
|
+
connectionAbortSignal,
|
|
84
|
+
rpcClientAbortSignal,
|
|
85
|
+
rpcStreamAbortSignal,
|
|
86
|
+
rpcAbortSignal,
|
|
87
|
+
createBlob,
|
|
88
|
+
}
|
package/src/rpcs.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { Future } from '@nmtjs/common'
|
|
2
|
+
import { createFuture } from '@nmtjs/common'
|
|
3
|
+
|
|
4
|
+
export class RpcManager {
|
|
5
|
+
// connectionId:callId -> AbortController
|
|
6
|
+
readonly rpcs = new Map<string, AbortController>()
|
|
7
|
+
// connectionId:callId -> Future<void>
|
|
8
|
+
readonly streams = new Map<string, Future<void>>()
|
|
9
|
+
|
|
10
|
+
set(connectionId: string, callId: number, controller: AbortController) {
|
|
11
|
+
const key = this.getKey(connectionId, callId)
|
|
12
|
+
this.rpcs.set(key, controller)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
get(connectionId: string, callId: number) {
|
|
16
|
+
const key = this.getKey(connectionId, callId)
|
|
17
|
+
return this.rpcs.get(key)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
delete(connectionId: string, callId: number) {
|
|
21
|
+
const key = this.getKey(connectionId, callId)
|
|
22
|
+
this.rpcs.delete(key)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
abort(connectionId: string, callId: number) {
|
|
26
|
+
const key = this.getKey(connectionId, callId)
|
|
27
|
+
const controller = this.rpcs.get(key)
|
|
28
|
+
if (controller) {
|
|
29
|
+
controller.abort()
|
|
30
|
+
this.rpcs.delete(key)
|
|
31
|
+
this.releasePull(connectionId, callId)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
awaitPull(
|
|
36
|
+
connectionId: string,
|
|
37
|
+
callId: number,
|
|
38
|
+
signal?: AbortSignal,
|
|
39
|
+
): Promise<void> {
|
|
40
|
+
const key = this.getKey(connectionId, callId)
|
|
41
|
+
const rpc = this.rpcs.get(key)
|
|
42
|
+
if (!rpc) throw new Error(`RPC not found`)
|
|
43
|
+
const future = this.streams.get(key)
|
|
44
|
+
if (future) {
|
|
45
|
+
return future.promise
|
|
46
|
+
} else {
|
|
47
|
+
const newFuture = createFuture<void>()
|
|
48
|
+
if (signal)
|
|
49
|
+
signal.addEventListener('abort', () => newFuture.resolve(), {
|
|
50
|
+
once: true,
|
|
51
|
+
})
|
|
52
|
+
this.streams.set(key, newFuture)
|
|
53
|
+
return newFuture.promise
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
releasePull(connectionId: string, callId: number) {
|
|
58
|
+
const key = this.getKey(connectionId, callId)
|
|
59
|
+
const future = this.streams.get(key)
|
|
60
|
+
if (future) {
|
|
61
|
+
future.resolve()
|
|
62
|
+
this.streams.delete(key)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
close(connectionId: string) {
|
|
67
|
+
// Iterate all RPCs and abort those belonging to this connection
|
|
68
|
+
// Optimization: Maintain a Set<callId> per connectionId
|
|
69
|
+
for (const [key, controller] of this.rpcs) {
|
|
70
|
+
if (key.startsWith(`${connectionId}:`)) {
|
|
71
|
+
controller.abort()
|
|
72
|
+
this.rpcs.delete(key)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Also release any pending pulls for this connection
|
|
76
|
+
for (const key of this.streams.keys()) {
|
|
77
|
+
if (key.startsWith(`${connectionId}:`)) {
|
|
78
|
+
const future = this.streams.get(key)
|
|
79
|
+
if (future) {
|
|
80
|
+
future.resolve()
|
|
81
|
+
this.streams.delete(key)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private getKey(connectionId: string, callId: number) {
|
|
88
|
+
return `${connectionId}:${callId}`
|
|
89
|
+
}
|
|
90
|
+
}
|
package/src/streams.ts
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
import type Stream from 'node:stream'
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
EncodeRPCStreams,
|
|
5
|
+
ProtocolBlob,
|
|
6
|
+
ProtocolBlobMetadata,
|
|
7
|
+
} from '@nmtjs/protocol'
|
|
8
|
+
import type { ProtocolRPCEncode } from '@nmtjs/protocol/client'
|
|
9
|
+
import { noopFn } from '@nmtjs/common'
|
|
10
|
+
import {
|
|
11
|
+
ProtocolClientStream,
|
|
12
|
+
ProtocolServerStream,
|
|
13
|
+
} from '@nmtjs/protocol/server'
|
|
14
|
+
|
|
15
|
+
import { StreamTimeout } from './enums.ts'
|
|
16
|
+
|
|
17
|
+
export type StreamConfig = {
|
|
18
|
+
timeouts: {
|
|
19
|
+
[StreamTimeout.Pull]: number
|
|
20
|
+
[StreamTimeout.Consume]: number
|
|
21
|
+
[StreamTimeout.Finish]: number
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type StreamTimeouts = Record<StreamTimeout, any>
|
|
26
|
+
|
|
27
|
+
type ClientStreamState = {
|
|
28
|
+
connectionId: string
|
|
29
|
+
callId: number
|
|
30
|
+
stream: ProtocolClientStream
|
|
31
|
+
timeouts: StreamTimeouts
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type ServerStreamState = {
|
|
35
|
+
connectionId: string
|
|
36
|
+
callId: number
|
|
37
|
+
stream: ProtocolServerStream
|
|
38
|
+
timeouts: StreamTimeouts
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type StreamState = ClientStreamState | ServerStreamState
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @todo Clarify Pull/Consume timeout semantics - currently ambiguous whether
|
|
45
|
+
* Pull timeout means "client not pulling" or "server not producing" for server streams
|
|
46
|
+
*/
|
|
47
|
+
export class BlobStreamsManager {
|
|
48
|
+
readonly clientStreams = new Map<string, ClientStreamState>()
|
|
49
|
+
readonly serverStreams = new Map<string, ServerStreamState>()
|
|
50
|
+
|
|
51
|
+
// Index for quick lookup by callId (connectionId:callId -> Set<streamId>)
|
|
52
|
+
readonly connectionClientStreams = new Map<string, Set<number>>()
|
|
53
|
+
readonly connectionServerStreams = new Map<string, Set<number>>()
|
|
54
|
+
readonly clientCallStreams = new Map<string, Set<number>>()
|
|
55
|
+
readonly serverCallStreams = new Map<string, Set<number>>()
|
|
56
|
+
|
|
57
|
+
readonly timeoutDurations: Record<StreamTimeout, number>
|
|
58
|
+
|
|
59
|
+
constructor(config: StreamConfig) {
|
|
60
|
+
this.timeoutDurations = {
|
|
61
|
+
[StreamTimeout.Pull]: config.timeouts[StreamTimeout.Pull],
|
|
62
|
+
[StreamTimeout.Consume]: config.timeouts[StreamTimeout.Consume],
|
|
63
|
+
[StreamTimeout.Finish]: config.timeouts[StreamTimeout.Finish],
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// --- Client Streams (Upload) ---
|
|
68
|
+
|
|
69
|
+
createClientStream(
|
|
70
|
+
connectionId: string,
|
|
71
|
+
callId: number,
|
|
72
|
+
streamId: number,
|
|
73
|
+
metadata: ProtocolBlobMetadata,
|
|
74
|
+
options: Stream.ReadableOptions,
|
|
75
|
+
) {
|
|
76
|
+
const stream = new ProtocolClientStream(streamId, metadata, options)
|
|
77
|
+
stream.on('error', noopFn)
|
|
78
|
+
|
|
79
|
+
const key = this.getKey(connectionId, streamId)
|
|
80
|
+
const state: ClientStreamState = {
|
|
81
|
+
connectionId,
|
|
82
|
+
callId,
|
|
83
|
+
stream,
|
|
84
|
+
timeouts: {
|
|
85
|
+
[StreamTimeout.Pull]: undefined,
|
|
86
|
+
[StreamTimeout.Consume]: undefined,
|
|
87
|
+
[StreamTimeout.Finish]: undefined,
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
this.clientStreams.set(key, state)
|
|
91
|
+
this.trackClientCall(connectionId, callId, streamId)
|
|
92
|
+
this.trackConnectionClientStream(connectionId, streamId)
|
|
93
|
+
|
|
94
|
+
this.startTimeout(state, StreamTimeout.Consume)
|
|
95
|
+
|
|
96
|
+
return stream
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
pushToClientStream(
|
|
100
|
+
connectionId: string,
|
|
101
|
+
streamId: number,
|
|
102
|
+
chunk: ArrayBufferView,
|
|
103
|
+
) {
|
|
104
|
+
const key = this.getKey(connectionId, streamId)
|
|
105
|
+
const state = this.clientStreams.get(key)
|
|
106
|
+
if (state) {
|
|
107
|
+
state.stream.write(chunk)
|
|
108
|
+
this.clearTimeout(state, StreamTimeout.Consume)
|
|
109
|
+
this.startTimeout(state, StreamTimeout.Pull)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
endClientStream(connectionId: string, streamId: number) {
|
|
114
|
+
const key = this.getKey(connectionId, streamId)
|
|
115
|
+
const state = this.clientStreams.get(key)
|
|
116
|
+
if (state) {
|
|
117
|
+
state.stream.end(null)
|
|
118
|
+
this.removeClientStream(connectionId, streamId)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
abortClientStream(connectionId: string, streamId: number, error = 'Aborted') {
|
|
123
|
+
const key = this.getKey(connectionId, streamId)
|
|
124
|
+
const state = this.clientStreams.get(key)
|
|
125
|
+
if (state) {
|
|
126
|
+
state.stream.destroy(new Error(error))
|
|
127
|
+
this.removeClientStream(connectionId, streamId)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
consumeClientStream(connectionId: string, callId: number, streamId: number) {
|
|
132
|
+
this.untrackClientCall(connectionId, callId, streamId)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private removeClientStream(connectionId: string, streamId: number) {
|
|
136
|
+
const key = this.getKey(connectionId, streamId)
|
|
137
|
+
const state = this.clientStreams.get(key)
|
|
138
|
+
if (state) {
|
|
139
|
+
this.clientStreams.delete(key)
|
|
140
|
+
this.clearTimeout(state, StreamTimeout.Finish)
|
|
141
|
+
this.clearTimeout(state, StreamTimeout.Pull)
|
|
142
|
+
this.clearTimeout(state, StreamTimeout.Consume)
|
|
143
|
+
this.untrackClientCall(connectionId, state.callId, streamId)
|
|
144
|
+
this.untrackConnectionClientStream(connectionId, streamId)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// --- Server Streams (Download) ---
|
|
149
|
+
|
|
150
|
+
getServerStreamsMetadata(connectionId: string, callId: number) {
|
|
151
|
+
const key = this.getCallKey(connectionId, callId)
|
|
152
|
+
const streamIds = this.serverCallStreams.get(key)
|
|
153
|
+
const streams: EncodeRPCStreams = {}
|
|
154
|
+
|
|
155
|
+
if (streamIds) {
|
|
156
|
+
for (const streamId of streamIds) {
|
|
157
|
+
const streamKey = this.getKey(connectionId, streamId)
|
|
158
|
+
const state = this.serverStreams.get(streamKey)
|
|
159
|
+
if (state) {
|
|
160
|
+
streams[streamId] = state.stream.metadata
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return streams
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
createServerStream(
|
|
169
|
+
connectionId: string,
|
|
170
|
+
callId: number,
|
|
171
|
+
streamId: number,
|
|
172
|
+
blob: ProtocolBlob,
|
|
173
|
+
) {
|
|
174
|
+
const stream = new ProtocolServerStream(streamId, blob)
|
|
175
|
+
const key = this.getKey(connectionId, streamId)
|
|
176
|
+
|
|
177
|
+
const state: ServerStreamState = {
|
|
178
|
+
connectionId,
|
|
179
|
+
callId,
|
|
180
|
+
stream,
|
|
181
|
+
timeouts: {
|
|
182
|
+
[StreamTimeout.Pull]: undefined,
|
|
183
|
+
[StreamTimeout.Consume]: undefined,
|
|
184
|
+
[StreamTimeout.Finish]: undefined,
|
|
185
|
+
},
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Prevent unhandled 'error' events, in case the user did not subscribe to them
|
|
189
|
+
stream.on('error', noopFn)
|
|
190
|
+
|
|
191
|
+
this.serverStreams.set(key, state)
|
|
192
|
+
this.trackServerCall(connectionId, callId, streamId)
|
|
193
|
+
this.trackConnectionServerStream(connectionId, streamId)
|
|
194
|
+
|
|
195
|
+
this.startTimeout(state, StreamTimeout.Finish)
|
|
196
|
+
this.startTimeout(state, StreamTimeout.Consume)
|
|
197
|
+
|
|
198
|
+
return stream
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
pullServerStream(connectionId: string, streamId: number) {
|
|
202
|
+
const key = this.getKey(connectionId, streamId)
|
|
203
|
+
const state = this.serverStreams.get(key)
|
|
204
|
+
if (state) {
|
|
205
|
+
state.stream.resume()
|
|
206
|
+
this.clearTimeout(state, StreamTimeout.Consume)
|
|
207
|
+
this.startTimeout(state, StreamTimeout.Pull)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
abortServerStream(connectionId: string, streamId: number, error = 'Aborted') {
|
|
212
|
+
const key = this.getKey(connectionId, streamId)
|
|
213
|
+
const state = this.serverStreams.get(key)
|
|
214
|
+
if (state) {
|
|
215
|
+
state.stream.destroy(new Error(error))
|
|
216
|
+
this.removeServerStream(connectionId, streamId)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
removeServerStream(connectionId: string, streamId: number) {
|
|
221
|
+
const key = this.getKey(connectionId, streamId)
|
|
222
|
+
const state = this.serverStreams.get(key)
|
|
223
|
+
if (state) {
|
|
224
|
+
this.serverStreams.delete(key)
|
|
225
|
+
this.clearTimeout(state, StreamTimeout.Pull)
|
|
226
|
+
this.clearTimeout(state, StreamTimeout.Consume)
|
|
227
|
+
this.clearTimeout(state, StreamTimeout.Finish)
|
|
228
|
+
this.untrackServerCall(connectionId, state.callId, streamId)
|
|
229
|
+
this.untrackConnectionServerStream(connectionId, streamId)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// --- Timeouts ---
|
|
234
|
+
|
|
235
|
+
private startTimeout(state: StreamState, type: StreamTimeout) {
|
|
236
|
+
this.clearTimeout(state, type)
|
|
237
|
+
const duration = this.timeoutDurations[type]
|
|
238
|
+
const timeout = setTimeout(() => {
|
|
239
|
+
if (state.stream instanceof ProtocolClientStream) {
|
|
240
|
+
this.abortClientStream(
|
|
241
|
+
state.connectionId,
|
|
242
|
+
state.stream.id,
|
|
243
|
+
`${type} timeout`,
|
|
244
|
+
)
|
|
245
|
+
} else {
|
|
246
|
+
this.abortServerStream(
|
|
247
|
+
state.connectionId,
|
|
248
|
+
state.stream.id,
|
|
249
|
+
`${type} timeout`,
|
|
250
|
+
)
|
|
251
|
+
}
|
|
252
|
+
state.timeouts[type] = undefined
|
|
253
|
+
}, duration)
|
|
254
|
+
state.timeouts[type] = timeout
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
private clearTimeout(state: StreamState, type: StreamTimeout) {
|
|
258
|
+
const timeout = state.timeouts[type]
|
|
259
|
+
if (timeout) {
|
|
260
|
+
clearTimeout(timeout)
|
|
261
|
+
state.timeouts[type] = undefined
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// --- Helpers ---
|
|
266
|
+
|
|
267
|
+
private getKey(connectionId: string, streamId: number) {
|
|
268
|
+
return `${connectionId}:${streamId}`
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private getCallKey(connectionId: string, callId: number) {
|
|
272
|
+
return `${connectionId}:${callId}`
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private trackClientCall(
|
|
276
|
+
connectionId: string,
|
|
277
|
+
callId: number,
|
|
278
|
+
streamId: number,
|
|
279
|
+
) {
|
|
280
|
+
const key = this.getCallKey(connectionId, callId)
|
|
281
|
+
let set = this.clientCallStreams.get(key)
|
|
282
|
+
if (!set) {
|
|
283
|
+
set = new Set()
|
|
284
|
+
this.clientCallStreams.set(key, set)
|
|
285
|
+
}
|
|
286
|
+
set.add(streamId)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
private untrackClientCall(
|
|
290
|
+
connectionId: string,
|
|
291
|
+
callId: number,
|
|
292
|
+
streamId: number,
|
|
293
|
+
) {
|
|
294
|
+
const key = this.getCallKey(connectionId, callId)
|
|
295
|
+
const set = this.clientCallStreams.get(key)
|
|
296
|
+
if (set) {
|
|
297
|
+
set.delete(streamId)
|
|
298
|
+
if (set.size === 0) {
|
|
299
|
+
this.clientCallStreams.delete(key)
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
private trackServerCall(
|
|
305
|
+
connectionId: string,
|
|
306
|
+
callId: number,
|
|
307
|
+
streamId: number,
|
|
308
|
+
) {
|
|
309
|
+
const key = this.getCallKey(connectionId, callId)
|
|
310
|
+
let set = this.serverCallStreams.get(key)
|
|
311
|
+
if (!set) {
|
|
312
|
+
set = new Set()
|
|
313
|
+
this.serverCallStreams.set(key, set)
|
|
314
|
+
}
|
|
315
|
+
set.add(streamId)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
private untrackServerCall(
|
|
319
|
+
connectionId: string,
|
|
320
|
+
callId: number,
|
|
321
|
+
streamId: number,
|
|
322
|
+
) {
|
|
323
|
+
const key = this.getCallKey(connectionId, callId)
|
|
324
|
+
const set = this.serverCallStreams.get(key)
|
|
325
|
+
if (set) {
|
|
326
|
+
set.delete(streamId)
|
|
327
|
+
if (set.size === 0) {
|
|
328
|
+
this.serverCallStreams.delete(key)
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
private trackConnectionClientStream(connectionId: string, streamId: number) {
|
|
334
|
+
let set = this.connectionClientStreams.get(connectionId)
|
|
335
|
+
if (!set) {
|
|
336
|
+
set = new Set()
|
|
337
|
+
this.connectionClientStreams.set(connectionId, set)
|
|
338
|
+
}
|
|
339
|
+
set.add(streamId)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
private untrackConnectionClientStream(
|
|
343
|
+
connectionId: string,
|
|
344
|
+
streamId: number,
|
|
345
|
+
) {
|
|
346
|
+
const set = this.connectionClientStreams.get(connectionId)
|
|
347
|
+
if (set) {
|
|
348
|
+
set.delete(streamId)
|
|
349
|
+
if (set.size === 0) {
|
|
350
|
+
this.connectionClientStreams.delete(connectionId)
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
private trackConnectionServerStream(connectionId: string, streamId: number) {
|
|
356
|
+
let set = this.connectionServerStreams.get(connectionId)
|
|
357
|
+
if (!set) {
|
|
358
|
+
set = new Set()
|
|
359
|
+
this.connectionServerStreams.set(connectionId, set)
|
|
360
|
+
}
|
|
361
|
+
set.add(streamId)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
private untrackConnectionServerStream(
|
|
365
|
+
connectionId: string,
|
|
366
|
+
streamId: number,
|
|
367
|
+
) {
|
|
368
|
+
const set = this.connectionServerStreams.get(connectionId)
|
|
369
|
+
if (set) {
|
|
370
|
+
set.delete(streamId)
|
|
371
|
+
if (set.size === 0) {
|
|
372
|
+
this.connectionServerStreams.delete(connectionId)
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// --- Cleanup ---
|
|
378
|
+
|
|
379
|
+
abortClientCallStreams(
|
|
380
|
+
connectionId: string,
|
|
381
|
+
callId: number,
|
|
382
|
+
reason = 'Call aborted',
|
|
383
|
+
) {
|
|
384
|
+
const key = this.getCallKey(connectionId, callId)
|
|
385
|
+
const clientStreamIds = this.clientCallStreams.get(key)
|
|
386
|
+
if (clientStreamIds) {
|
|
387
|
+
for (const streamId of [...clientStreamIds]) {
|
|
388
|
+
this.abortClientStream(connectionId, streamId, reason)
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
cleanupConnection(connectionId: string) {
|
|
394
|
+
const clientStreamIds = this.connectionClientStreams.get(connectionId)
|
|
395
|
+
if (clientStreamIds) {
|
|
396
|
+
for (const streamId of [...clientStreamIds]) {
|
|
397
|
+
this.abortClientStream(connectionId, streamId, 'Connection closed')
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const serverStreamIds = this.connectionServerStreams.get(connectionId)
|
|
402
|
+
if (serverStreamIds) {
|
|
403
|
+
for (const streamId of [...serverStreamIds]) {
|
|
404
|
+
this.abortServerStream(connectionId, streamId, 'Connection closed')
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
package/src/transport.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { Async } from '@nmtjs/common'
|
|
2
|
+
import type { Injection, LazyInjectable, Scope } from '@nmtjs/core'
|
|
3
|
+
import type { ConnectionType, ProtocolVersion } from '@nmtjs/protocol'
|
|
4
|
+
import type { ProtocolFormats } from '@nmtjs/protocol/server'
|
|
5
|
+
|
|
6
|
+
import type { GatewayConnection } from './connections.ts'
|
|
7
|
+
import type { ProxyableTransportType } from './enums.ts'
|
|
8
|
+
import type { GatewayRpc } from './types.ts'
|
|
9
|
+
|
|
10
|
+
export interface TransportConnection {
|
|
11
|
+
connectionId: string
|
|
12
|
+
type: ConnectionType
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface TransportOnConnectOptions<
|
|
16
|
+
Type extends ConnectionType = ConnectionType,
|
|
17
|
+
> {
|
|
18
|
+
type: Type extends ConnectionType.Bidirectional
|
|
19
|
+
? Type
|
|
20
|
+
: ConnectionType.Unidirectional
|
|
21
|
+
protocolVersion: ProtocolVersion
|
|
22
|
+
accept: string | null
|
|
23
|
+
contentType: string | null
|
|
24
|
+
data: unknown
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface TransportOnMessageOptions {
|
|
28
|
+
connectionId: string
|
|
29
|
+
data: ArrayBuffer
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type TransportWorkerParams<
|
|
33
|
+
Type extends ConnectionType = ConnectionType,
|
|
34
|
+
> = {
|
|
35
|
+
formats: ProtocolFormats
|
|
36
|
+
onConnect: (
|
|
37
|
+
options: TransportOnConnectOptions<Type>,
|
|
38
|
+
...injections: Injection[]
|
|
39
|
+
) => Promise<GatewayConnection & AsyncDisposable>
|
|
40
|
+
onDisconnect: (connectionId: GatewayConnection['id']) => Promise<void>
|
|
41
|
+
onMessage: (
|
|
42
|
+
options: TransportOnMessageOptions,
|
|
43
|
+
...injections: Injection[]
|
|
44
|
+
) => Promise<void>
|
|
45
|
+
onRpc: (
|
|
46
|
+
connection: GatewayConnection,
|
|
47
|
+
rpc: GatewayRpc,
|
|
48
|
+
signal: AbortSignal,
|
|
49
|
+
...injections: Injection[]
|
|
50
|
+
) => Promise<unknown>
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface TransportWorkerStartOptions<
|
|
54
|
+
Type extends ConnectionType = ConnectionType,
|
|
55
|
+
> extends TransportWorkerParams<Type> {
|
|
56
|
+
// for extra props in the future
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface TransportWorker<Type extends ConnectionType = ConnectionType> {
|
|
60
|
+
start: (params: TransportWorkerParams<Type>) => Async<string>
|
|
61
|
+
stop: (params: Pick<TransportWorkerParams<Type>, 'formats'>) => Async<void>
|
|
62
|
+
send?: Type extends 'unidirectional'
|
|
63
|
+
? never
|
|
64
|
+
: (connectionId: string, buffer: ArrayBufferView) => boolean | null
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface Transport<
|
|
68
|
+
Type extends ConnectionType = ConnectionType,
|
|
69
|
+
TransportOptions = any,
|
|
70
|
+
Injections extends {
|
|
71
|
+
[key: string]: LazyInjectable<any, Scope.Connection | Scope.Call>
|
|
72
|
+
} = { [key: string]: LazyInjectable<any, Scope.Connection | Scope.Call> },
|
|
73
|
+
Proxyable extends ProxyableTransportType | undefined =
|
|
74
|
+
| ProxyableTransportType
|
|
75
|
+
| undefined,
|
|
76
|
+
> {
|
|
77
|
+
proxyable: Proxyable
|
|
78
|
+
injectables?: Injections
|
|
79
|
+
factory: (options: TransportOptions) => Async<TransportWorker<Type>>
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function createTransport<
|
|
83
|
+
Type extends ConnectionType = ConnectionType,
|
|
84
|
+
TransportOptions = any,
|
|
85
|
+
Injections extends {
|
|
86
|
+
[key: string]: LazyInjectable<any, Scope.Connection | Scope.Call>
|
|
87
|
+
} = { [key: string]: LazyInjectable<any, Scope.Connection | Scope.Call> },
|
|
88
|
+
Proxyable extends ProxyableTransportType | undefined =
|
|
89
|
+
| ProxyableTransportType
|
|
90
|
+
| undefined,
|
|
91
|
+
>(
|
|
92
|
+
config: Transport<Type, TransportOptions, Injections, Proxyable>,
|
|
93
|
+
): Transport<Type, TransportOptions, Injections, Proxyable> {
|
|
94
|
+
return config
|
|
95
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { AnyInjectable, Container, Logger, Scope } from '@nmtjs/core'
|
|
2
|
+
import type { MessageContext as ProtocolMessageContext } from '@nmtjs/protocol/server'
|
|
3
|
+
|
|
4
|
+
import type { GatewayApiCallOptions } from './api.ts'
|
|
5
|
+
|
|
6
|
+
export type ConnectionIdentityType = string
|
|
7
|
+
export type ConnectionIdentity = AnyInjectable<
|
|
8
|
+
ConnectionIdentityType,
|
|
9
|
+
Scope.Global
|
|
10
|
+
>
|
|
11
|
+
|
|
12
|
+
export interface GatewayRpc {
|
|
13
|
+
callId: number
|
|
14
|
+
procedure: string
|
|
15
|
+
payload: unknown
|
|
16
|
+
metadata?: GatewayApiCallOptions['metadata']
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface GatewayRpcContext extends ProtocolMessageContext, GatewayRpc {
|
|
20
|
+
container: Container
|
|
21
|
+
signal: AbortSignal
|
|
22
|
+
logger: Logger
|
|
23
|
+
[Symbol.asyncDispose](): Promise<void>
|
|
24
|
+
}
|