@nmtjs/client 0.15.0-beta.3 → 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/package.json +11 -10
- package/src/clients/runtime.ts +133 -0
- package/src/clients/static.ts +77 -0
- package/src/core.ts +637 -0
- package/src/events.ts +70 -0
- package/src/index.ts +5 -0
- package/src/streams.ts +131 -0
- package/src/transformers.ts +8 -0
- package/src/transport.ts +71 -0
- package/src/types.ts +145 -0
package/package.json
CHANGED
|
@@ -7,24 +7,25 @@
|
|
|
7
7
|
"./static": "./dist/clients/static.js"
|
|
8
8
|
},
|
|
9
9
|
"peerDependencies": {
|
|
10
|
-
"@nmtjs/
|
|
11
|
-
"@nmtjs/
|
|
12
|
-
"@nmtjs/
|
|
13
|
-
"@nmtjs/
|
|
10
|
+
"@nmtjs/type": "0.15.0-beta.4",
|
|
11
|
+
"@nmtjs/common": "0.15.0-beta.4",
|
|
12
|
+
"@nmtjs/protocol": "0.15.0-beta.4",
|
|
13
|
+
"@nmtjs/contract": "0.15.0-beta.4"
|
|
14
14
|
},
|
|
15
15
|
"devDependencies": {
|
|
16
|
-
"@nmtjs/_tests": "0.15.0-beta.
|
|
17
|
-
"@nmtjs/
|
|
18
|
-
"@nmtjs/
|
|
19
|
-
"@nmtjs/
|
|
20
|
-
"@nmtjs/
|
|
16
|
+
"@nmtjs/_tests": "0.15.0-beta.4",
|
|
17
|
+
"@nmtjs/contract": "0.15.0-beta.4",
|
|
18
|
+
"@nmtjs/common": "0.15.0-beta.4",
|
|
19
|
+
"@nmtjs/protocol": "0.15.0-beta.4",
|
|
20
|
+
"@nmtjs/type": "0.15.0-beta.4"
|
|
21
21
|
},
|
|
22
22
|
"files": [
|
|
23
23
|
"dist",
|
|
24
|
+
"src",
|
|
24
25
|
"LICENSE.md",
|
|
25
26
|
"README.md"
|
|
26
27
|
],
|
|
27
|
-
"version": "0.15.0-beta.
|
|
28
|
+
"version": "0.15.0-beta.4",
|
|
28
29
|
"scripts": {
|
|
29
30
|
"clean-build": "rm -rf ./dist",
|
|
30
31
|
"build": "tsc --declaration --sourcemap",
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
TAnyProcedureContract,
|
|
3
|
+
TAnyRouterContract,
|
|
4
|
+
TRouteContract,
|
|
5
|
+
} from '@nmtjs/contract'
|
|
6
|
+
import { IsProcedureContract, IsRouterContract } from '@nmtjs/contract'
|
|
7
|
+
|
|
8
|
+
import type { BaseClientOptions } from '../core.ts'
|
|
9
|
+
import type { ClientTransportFactory } from '../transport.ts'
|
|
10
|
+
import type {
|
|
11
|
+
ClientCallers,
|
|
12
|
+
ClientCallOptions,
|
|
13
|
+
RuntimeInputContractTypeProvider,
|
|
14
|
+
RuntimeOutputContractTypeProvider,
|
|
15
|
+
} from '../types.ts'
|
|
16
|
+
import { BaseClient } from '../core.ts'
|
|
17
|
+
|
|
18
|
+
export class RuntimeContractTransformer {
|
|
19
|
+
#procedures = new Map<string, TAnyProcedureContract>()
|
|
20
|
+
|
|
21
|
+
constructor(router: TAnyRouterContract) {
|
|
22
|
+
const registerProcedures = (r: TRouteContract, path: string[] = []) => {
|
|
23
|
+
if (IsRouterContract(r)) {
|
|
24
|
+
for (const [key, route] of Object.entries(r.routes)) {
|
|
25
|
+
registerProcedures(route, [...path, key])
|
|
26
|
+
}
|
|
27
|
+
} else if (IsProcedureContract(r)) {
|
|
28
|
+
const fullName = [...path].join('/')
|
|
29
|
+
this.#procedures.set(fullName, r)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
registerProcedures(router)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
encode(_procedure: string, payload: any) {
|
|
36
|
+
const procedure = this.#procedures.get(_procedure)
|
|
37
|
+
if (!procedure) throw new Error(`Procedure not found: ${_procedure}`)
|
|
38
|
+
return procedure.input.encode(payload)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
decode(_procedure: string, payload: any) {
|
|
42
|
+
const procedure = this.#procedures.get(_procedure)
|
|
43
|
+
if (!procedure) throw new Error(`Procedure not found: ${_procedure}`)
|
|
44
|
+
return procedure.output.decode(payload)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export class RuntimeClient<
|
|
49
|
+
Transport extends ClientTransportFactory<any, any> = ClientTransportFactory<
|
|
50
|
+
any,
|
|
51
|
+
any
|
|
52
|
+
>,
|
|
53
|
+
RouterContract extends TAnyRouterContract = TAnyRouterContract,
|
|
54
|
+
SafeCall extends boolean = false,
|
|
55
|
+
> extends BaseClient<
|
|
56
|
+
Transport,
|
|
57
|
+
RouterContract,
|
|
58
|
+
SafeCall,
|
|
59
|
+
RuntimeInputContractTypeProvider,
|
|
60
|
+
RuntimeOutputContractTypeProvider
|
|
61
|
+
> {
|
|
62
|
+
protected readonly transformer: RuntimeContractTransformer
|
|
63
|
+
|
|
64
|
+
readonly #procedures = new Map<string, TAnyProcedureContract>()
|
|
65
|
+
readonly #callers!: ClientCallers<this['_']['routes'], SafeCall, boolean>
|
|
66
|
+
|
|
67
|
+
constructor(
|
|
68
|
+
options: BaseClientOptions<RouterContract, SafeCall>,
|
|
69
|
+
transport: Transport,
|
|
70
|
+
transportOptions: Transport extends ClientTransportFactory<
|
|
71
|
+
any,
|
|
72
|
+
infer Options
|
|
73
|
+
>
|
|
74
|
+
? Options
|
|
75
|
+
: never,
|
|
76
|
+
) {
|
|
77
|
+
super(options, transport, transportOptions)
|
|
78
|
+
|
|
79
|
+
this.resolveProcedures(this.options.contract)
|
|
80
|
+
this.transformer = new RuntimeContractTransformer(this.options.contract)
|
|
81
|
+
this.#callers = this.buildCallers()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
override get call() {
|
|
85
|
+
return this.#callers as ClientCallers<this['_']['routes'], SafeCall, false>
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
override get stream() {
|
|
89
|
+
return this.#callers as ClientCallers<this['_']['routes'], SafeCall, true>
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
protected resolveProcedures(router: TAnyRouterContract, path: string[] = []) {
|
|
93
|
+
for (const [key, route] of Object.entries(router.routes)) {
|
|
94
|
+
if (IsRouterContract(route)) {
|
|
95
|
+
this.resolveProcedures(route, [...path, key])
|
|
96
|
+
} else if (IsProcedureContract(route)) {
|
|
97
|
+
const fullName = [...path, key].join('/')
|
|
98
|
+
this.#procedures.set(fullName, route)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
protected buildCallers(): ClientCallers<
|
|
104
|
+
this['_']['routes'],
|
|
105
|
+
SafeCall,
|
|
106
|
+
boolean
|
|
107
|
+
> {
|
|
108
|
+
const callers: Record<string, any> = Object.create(null)
|
|
109
|
+
|
|
110
|
+
for (const [name, { stream }] of this.#procedures) {
|
|
111
|
+
const parts = name.split('/')
|
|
112
|
+
let current = callers
|
|
113
|
+
for (let i = 0; i < parts.length; i++) {
|
|
114
|
+
const part = parts[i]
|
|
115
|
+
if (i === parts.length - 1) {
|
|
116
|
+
current[part] = (
|
|
117
|
+
payload?: unknown,
|
|
118
|
+
options?: Partial<ClientCallOptions>,
|
|
119
|
+
) =>
|
|
120
|
+
this._call(name, payload, {
|
|
121
|
+
...options,
|
|
122
|
+
_stream_response: !!stream,
|
|
123
|
+
})
|
|
124
|
+
} else {
|
|
125
|
+
current[part] = current[part] ?? Object.create(null)
|
|
126
|
+
current = current[part]
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return callers as ClientCallers<this['_']['routes'], SafeCall, boolean>
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { TAnyRouterContract } from '@nmtjs/contract'
|
|
2
|
+
|
|
3
|
+
import type { BaseClientOptions } from '../core.ts'
|
|
4
|
+
import type { ClientTransportFactory } from '../transport.ts'
|
|
5
|
+
import type {
|
|
6
|
+
ClientCallers,
|
|
7
|
+
ClientCallOptions,
|
|
8
|
+
StaticInputContractTypeProvider,
|
|
9
|
+
StaticOutputContractTypeProvider,
|
|
10
|
+
} from '../types.ts'
|
|
11
|
+
import { BaseClient } from '../core.ts'
|
|
12
|
+
import { BaseClientTransformer } from '../transformers.ts'
|
|
13
|
+
|
|
14
|
+
export class StaticClient<
|
|
15
|
+
Transport extends ClientTransportFactory<any, any> = ClientTransportFactory<
|
|
16
|
+
any,
|
|
17
|
+
any
|
|
18
|
+
>,
|
|
19
|
+
RouterContract extends TAnyRouterContract = TAnyRouterContract,
|
|
20
|
+
SafeCall extends boolean = false,
|
|
21
|
+
> extends BaseClient<
|
|
22
|
+
Transport,
|
|
23
|
+
RouterContract,
|
|
24
|
+
SafeCall,
|
|
25
|
+
StaticInputContractTypeProvider,
|
|
26
|
+
StaticOutputContractTypeProvider
|
|
27
|
+
> {
|
|
28
|
+
protected readonly transformer: BaseClientTransformer
|
|
29
|
+
|
|
30
|
+
constructor(
|
|
31
|
+
options: BaseClientOptions<RouterContract, SafeCall>,
|
|
32
|
+
transport: Transport,
|
|
33
|
+
transportOptions: Transport extends ClientTransportFactory<
|
|
34
|
+
any,
|
|
35
|
+
infer Options
|
|
36
|
+
>
|
|
37
|
+
? Options
|
|
38
|
+
: never,
|
|
39
|
+
) {
|
|
40
|
+
super(options, transport, transportOptions)
|
|
41
|
+
this.transformer = new BaseClientTransformer()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
override get call() {
|
|
45
|
+
return this.createProxy(Object.create(null), false) as ClientCallers<
|
|
46
|
+
this['_']['routes'],
|
|
47
|
+
SafeCall,
|
|
48
|
+
false
|
|
49
|
+
>
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
override get stream() {
|
|
53
|
+
return this.createProxy(Object.create(null), true) as ClientCallers<
|
|
54
|
+
this['_']['routes'],
|
|
55
|
+
SafeCall,
|
|
56
|
+
true
|
|
57
|
+
>
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
protected createProxy<T>(
|
|
61
|
+
target: Record<string, unknown>,
|
|
62
|
+
isStream: boolean,
|
|
63
|
+
path: string[] = [],
|
|
64
|
+
) {
|
|
65
|
+
return new Proxy(target, {
|
|
66
|
+
get: (obj, prop) => {
|
|
67
|
+
if (prop === 'then') return obj
|
|
68
|
+
const newPath = [...path, String(prop)]
|
|
69
|
+
const caller = (
|
|
70
|
+
payload?: unknown,
|
|
71
|
+
options?: Partial<ClientCallOptions>,
|
|
72
|
+
) => this._call(newPath.join('/'), payload, options)
|
|
73
|
+
return this.createProxy(caller as any, isStream, newPath)
|
|
74
|
+
},
|
|
75
|
+
}) as T
|
|
76
|
+
}
|
|
77
|
+
}
|
package/src/core.ts
ADDED
|
@@ -0,0 +1,637 @@
|
|
|
1
|
+
import type { Future, TypeProvider } from '@nmtjs/common'
|
|
2
|
+
import type { TAnyRouterContract } from '@nmtjs/contract'
|
|
3
|
+
import type { ProtocolBlobMetadata, ProtocolVersion } from '@nmtjs/protocol'
|
|
4
|
+
import type {
|
|
5
|
+
BaseClientFormat,
|
|
6
|
+
MessageContext,
|
|
7
|
+
ProtocolVersionInterface,
|
|
8
|
+
ServerMessageTypePayload,
|
|
9
|
+
} from '@nmtjs/protocol/client'
|
|
10
|
+
import { anyAbortSignal, createFuture, MAX_UINT32, noopFn } from '@nmtjs/common'
|
|
11
|
+
import {
|
|
12
|
+
ClientMessageType,
|
|
13
|
+
ConnectionType,
|
|
14
|
+
ErrorCode,
|
|
15
|
+
ProtocolBlob,
|
|
16
|
+
ServerMessageType,
|
|
17
|
+
} from '@nmtjs/protocol'
|
|
18
|
+
import {
|
|
19
|
+
ProtocolError,
|
|
20
|
+
ProtocolServerBlobStream,
|
|
21
|
+
ProtocolServerRPCStream,
|
|
22
|
+
ProtocolServerStream,
|
|
23
|
+
versions,
|
|
24
|
+
} from '@nmtjs/protocol/client'
|
|
25
|
+
|
|
26
|
+
import type { BaseClientTransformer } from './transformers.ts'
|
|
27
|
+
import type { ClientCallResponse, ClientTransportFactory } from './transport.ts'
|
|
28
|
+
import type {
|
|
29
|
+
ClientCallers,
|
|
30
|
+
ClientCallOptions,
|
|
31
|
+
ResolveAPIRouterRoutes,
|
|
32
|
+
} from './types.ts'
|
|
33
|
+
import { EventEmitter } from './events.ts'
|
|
34
|
+
import { ClientStreams, ServerStreams } from './streams.ts'
|
|
35
|
+
|
|
36
|
+
export {
|
|
37
|
+
ErrorCode,
|
|
38
|
+
ProtocolBlob,
|
|
39
|
+
type ProtocolBlobMetadata,
|
|
40
|
+
} from '@nmtjs/protocol'
|
|
41
|
+
|
|
42
|
+
export * from './types.ts'
|
|
43
|
+
|
|
44
|
+
export class ClientError extends ProtocolError {}
|
|
45
|
+
|
|
46
|
+
export type ProtocolClientCall = Future<any> & {
|
|
47
|
+
procedure: string
|
|
48
|
+
signal?: AbortSignal
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const DEFAULT_RECONNECT_TIMEOUT = 1000
|
|
52
|
+
const DEFAULT_MAX_RECONNECT_TIMEOUT = 60000
|
|
53
|
+
|
|
54
|
+
export interface BaseClientOptions<
|
|
55
|
+
RouterContract extends TAnyRouterContract = TAnyRouterContract,
|
|
56
|
+
SafeCall extends boolean = false,
|
|
57
|
+
> {
|
|
58
|
+
contract: RouterContract
|
|
59
|
+
protocol: ProtocolVersion
|
|
60
|
+
format: BaseClientFormat
|
|
61
|
+
application?: string
|
|
62
|
+
timeout?: number
|
|
63
|
+
autoreconnect?: boolean
|
|
64
|
+
safe?: SafeCall
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @todo Add error logging in ClientStreamPull rejection handler for easier debugging
|
|
69
|
+
* @todo Consider edge case where callId/streamId overflow at MAX_UINT32 with existing entries
|
|
70
|
+
*/
|
|
71
|
+
export abstract class BaseClient<
|
|
72
|
+
TransportFactory extends ClientTransportFactory<
|
|
73
|
+
any,
|
|
74
|
+
any
|
|
75
|
+
> = ClientTransportFactory<any, any>,
|
|
76
|
+
RouterContract extends TAnyRouterContract = TAnyRouterContract,
|
|
77
|
+
SafeCall extends boolean = false,
|
|
78
|
+
InputTypeProvider extends TypeProvider = TypeProvider,
|
|
79
|
+
OutputTypeProvider extends TypeProvider = TypeProvider,
|
|
80
|
+
> extends EventEmitter<{
|
|
81
|
+
connected: []
|
|
82
|
+
disconnected: [reason: 'server' | 'client' | (string & {})]
|
|
83
|
+
}> {
|
|
84
|
+
_!: {
|
|
85
|
+
routes: ResolveAPIRouterRoutes<
|
|
86
|
+
RouterContract,
|
|
87
|
+
InputTypeProvider,
|
|
88
|
+
OutputTypeProvider
|
|
89
|
+
>
|
|
90
|
+
safe: SafeCall
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
protected abstract readonly transformer: BaseClientTransformer
|
|
94
|
+
|
|
95
|
+
abstract call: ClientCallers<this['_']['routes'], SafeCall, false>
|
|
96
|
+
abstract stream: ClientCallers<this['_']['routes'], SafeCall, true>
|
|
97
|
+
|
|
98
|
+
protected calls = new Map<number, ProtocolClientCall>()
|
|
99
|
+
protected transport: TransportFactory extends ClientTransportFactory<
|
|
100
|
+
any,
|
|
101
|
+
any,
|
|
102
|
+
infer T
|
|
103
|
+
>
|
|
104
|
+
? T
|
|
105
|
+
: never
|
|
106
|
+
protected protocol: ProtocolVersionInterface
|
|
107
|
+
protected messageContext!: MessageContext | null
|
|
108
|
+
protected clientStreams = new ClientStreams()
|
|
109
|
+
protected serverStreams = new ServerStreams()
|
|
110
|
+
protected rpcStreams = new ServerStreams()
|
|
111
|
+
protected callId = 0
|
|
112
|
+
protected streamId = 0
|
|
113
|
+
protected cab: AbortController | null = null
|
|
114
|
+
protected reconnectTimeout = DEFAULT_RECONNECT_TIMEOUT
|
|
115
|
+
|
|
116
|
+
#auth: any
|
|
117
|
+
|
|
118
|
+
constructor(
|
|
119
|
+
readonly options: BaseClientOptions<RouterContract, SafeCall>,
|
|
120
|
+
readonly transportFactory: TransportFactory,
|
|
121
|
+
readonly transportOptions: TransportFactory extends ClientTransportFactory<
|
|
122
|
+
any,
|
|
123
|
+
infer U
|
|
124
|
+
>
|
|
125
|
+
? U
|
|
126
|
+
: never,
|
|
127
|
+
) {
|
|
128
|
+
super()
|
|
129
|
+
|
|
130
|
+
this.protocol = versions[options.protocol]
|
|
131
|
+
|
|
132
|
+
const { format, protocol } = this.options
|
|
133
|
+
|
|
134
|
+
this.transport = this.transportFactory(
|
|
135
|
+
{ protocol, format },
|
|
136
|
+
this.transportOptions,
|
|
137
|
+
) as any
|
|
138
|
+
|
|
139
|
+
if (
|
|
140
|
+
this.transport.type === ConnectionType.Bidirectional &&
|
|
141
|
+
this.options.autoreconnect
|
|
142
|
+
) {
|
|
143
|
+
this.on('disconnected', async (reason) => {
|
|
144
|
+
if (reason === 'server') {
|
|
145
|
+
this.connect()
|
|
146
|
+
} else if (reason === 'error') {
|
|
147
|
+
const timeout = new Promise((resolve) =>
|
|
148
|
+
setTimeout(resolve, this.reconnectTimeout),
|
|
149
|
+
)
|
|
150
|
+
const connected = new Promise((_, reject) =>
|
|
151
|
+
this.once('connected', reject),
|
|
152
|
+
)
|
|
153
|
+
this.reconnectTimeout = Math.min(
|
|
154
|
+
this.reconnectTimeout * 2,
|
|
155
|
+
DEFAULT_MAX_RECONNECT_TIMEOUT,
|
|
156
|
+
)
|
|
157
|
+
await Promise.race([timeout, connected]).then(
|
|
158
|
+
this.connect.bind(this),
|
|
159
|
+
noopFn,
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
this.on('connected', () => {
|
|
165
|
+
this.reconnectTimeout = DEFAULT_RECONNECT_TIMEOUT
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
if (globalThis.window) {
|
|
169
|
+
globalThis.window.addEventListener('pageshow', () => {
|
|
170
|
+
if (!this.cab) this.connect()
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
get auth() {
|
|
177
|
+
return this.#auth
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
set auth(value) {
|
|
181
|
+
this.#auth = value
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async connect() {
|
|
185
|
+
if (this.transport.type === ConnectionType.Bidirectional) {
|
|
186
|
+
this.cab = new AbortController()
|
|
187
|
+
const protocol = this.protocol
|
|
188
|
+
const serverStreams = this.serverStreams
|
|
189
|
+
|
|
190
|
+
const transport = {
|
|
191
|
+
send: (buffer) => {
|
|
192
|
+
this.#send(buffer).catch(noopFn)
|
|
193
|
+
},
|
|
194
|
+
}
|
|
195
|
+
this.messageContext = {
|
|
196
|
+
transport,
|
|
197
|
+
encoder: this.options.format,
|
|
198
|
+
decoder: this.options.format,
|
|
199
|
+
addClientStream: (blob) => {
|
|
200
|
+
const streamId = this.#getStreamId()
|
|
201
|
+
return this.clientStreams.add(blob.source, streamId, blob.metadata)
|
|
202
|
+
},
|
|
203
|
+
addServerStream(streamId, metadata) {
|
|
204
|
+
const stream = new ProtocolServerBlobStream(metadata, {
|
|
205
|
+
pull: (controller) => {
|
|
206
|
+
transport.send(
|
|
207
|
+
protocol.encodeMessage(
|
|
208
|
+
this,
|
|
209
|
+
ClientMessageType.ServerStreamPull,
|
|
210
|
+
{ streamId, size: 65535 /* 64kb by default */ },
|
|
211
|
+
),
|
|
212
|
+
)
|
|
213
|
+
},
|
|
214
|
+
close: () => {
|
|
215
|
+
serverStreams.remove(streamId)
|
|
216
|
+
},
|
|
217
|
+
readableStrategy: { highWaterMark: 0 },
|
|
218
|
+
})
|
|
219
|
+
serverStreams.add(streamId, stream)
|
|
220
|
+
return ({ signal }: { signal?: AbortSignal } = {}) => {
|
|
221
|
+
if (signal)
|
|
222
|
+
signal.addEventListener(
|
|
223
|
+
'abort',
|
|
224
|
+
() => {
|
|
225
|
+
transport.send(
|
|
226
|
+
protocol.encodeMessage(
|
|
227
|
+
this,
|
|
228
|
+
ClientMessageType.ServerStreamAbort,
|
|
229
|
+
{ streamId },
|
|
230
|
+
),
|
|
231
|
+
)
|
|
232
|
+
serverStreams.abort(streamId)
|
|
233
|
+
},
|
|
234
|
+
{ once: true },
|
|
235
|
+
)
|
|
236
|
+
return stream
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
streamId: this.#getStreamId.bind(this),
|
|
240
|
+
}
|
|
241
|
+
return this.transport.connect({
|
|
242
|
+
auth: this.auth,
|
|
243
|
+
application: this.options.application,
|
|
244
|
+
onMessage: this.onMessage.bind(this),
|
|
245
|
+
onConnect: this.onConnect.bind(this),
|
|
246
|
+
onDisconnect: this.onDisconnect.bind(this),
|
|
247
|
+
})
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async disconnect() {
|
|
252
|
+
if (this.transport.type === ConnectionType.Bidirectional) {
|
|
253
|
+
this.cab!.abort()
|
|
254
|
+
await this.transport.disconnect()
|
|
255
|
+
this.messageContext = null
|
|
256
|
+
this.cab = null
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
blob(
|
|
261
|
+
source: Blob | ReadableStream | string | AsyncIterable<Uint8Array>,
|
|
262
|
+
metadata?: ProtocolBlobMetadata,
|
|
263
|
+
) {
|
|
264
|
+
return ProtocolBlob.from(source, metadata)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
protected async _call(
|
|
268
|
+
procedure: string,
|
|
269
|
+
payload: any,
|
|
270
|
+
options: ClientCallOptions = {},
|
|
271
|
+
) {
|
|
272
|
+
const timeout = options.timeout ?? this.options.timeout
|
|
273
|
+
const controller = new AbortController()
|
|
274
|
+
|
|
275
|
+
// attach all abort signals
|
|
276
|
+
const signals: AbortSignal[] = [controller.signal]
|
|
277
|
+
|
|
278
|
+
if (timeout) signals.push(AbortSignal.timeout(timeout))
|
|
279
|
+
if (options.signal) signals.push(options.signal)
|
|
280
|
+
if (this.cab?.signal) signals.push(this.cab.signal)
|
|
281
|
+
|
|
282
|
+
const signal = signals.length ? anyAbortSignal(...signals) : undefined
|
|
283
|
+
|
|
284
|
+
const callId = this.#getCallId()
|
|
285
|
+
const call = createFuture() as ProtocolClientCall
|
|
286
|
+
call.procedure = procedure
|
|
287
|
+
call.signal = signal
|
|
288
|
+
|
|
289
|
+
this.calls.set(callId, call)
|
|
290
|
+
|
|
291
|
+
// Check if signal is already aborted before proceeding
|
|
292
|
+
if (signal?.aborted) {
|
|
293
|
+
this.calls.delete(callId)
|
|
294
|
+
const error = new ProtocolError(
|
|
295
|
+
ErrorCode.ClientRequestError,
|
|
296
|
+
signal.reason,
|
|
297
|
+
)
|
|
298
|
+
call.reject(error)
|
|
299
|
+
} else {
|
|
300
|
+
if (signal) {
|
|
301
|
+
signal.addEventListener(
|
|
302
|
+
'abort',
|
|
303
|
+
() => {
|
|
304
|
+
call.reject(
|
|
305
|
+
new ProtocolError(ErrorCode.ClientRequestError, signal!.reason),
|
|
306
|
+
)
|
|
307
|
+
if (
|
|
308
|
+
this.transport.type === ConnectionType.Bidirectional &&
|
|
309
|
+
this.messageContext
|
|
310
|
+
) {
|
|
311
|
+
const buffer = this.protocol.encodeMessage(
|
|
312
|
+
this.messageContext,
|
|
313
|
+
ClientMessageType.RpcAbort,
|
|
314
|
+
{ callId },
|
|
315
|
+
)
|
|
316
|
+
this.#send(buffer).catch(noopFn)
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
{ once: true },
|
|
320
|
+
)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
try {
|
|
324
|
+
const transformedPayload = this.transformer.encode(procedure, payload)
|
|
325
|
+
if (this.transport.type === ConnectionType.Bidirectional) {
|
|
326
|
+
const buffer = this.protocol.encodeMessage(
|
|
327
|
+
this.messageContext!,
|
|
328
|
+
ClientMessageType.Rpc,
|
|
329
|
+
{ callId, procedure, payload: transformedPayload },
|
|
330
|
+
)
|
|
331
|
+
await this.#send(buffer, signal)
|
|
332
|
+
} else {
|
|
333
|
+
const response = await this.transport.call(
|
|
334
|
+
{
|
|
335
|
+
application: this.options.application,
|
|
336
|
+
format: this.options.format,
|
|
337
|
+
auth: this.auth,
|
|
338
|
+
},
|
|
339
|
+
{ callId, procedure, payload: transformedPayload },
|
|
340
|
+
{ signal, _stream_response: options._stream_response },
|
|
341
|
+
)
|
|
342
|
+
this.#handleCallResponse(callId, response)
|
|
343
|
+
}
|
|
344
|
+
} catch (error) {
|
|
345
|
+
call.reject(error)
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const result = call.promise.then(
|
|
350
|
+
(value) => {
|
|
351
|
+
if (value instanceof ProtocolServerRPCStream) {
|
|
352
|
+
return value.createAsyncIterable(() => {
|
|
353
|
+
controller.abort()
|
|
354
|
+
})
|
|
355
|
+
}
|
|
356
|
+
controller.abort()
|
|
357
|
+
return value
|
|
358
|
+
},
|
|
359
|
+
(err) => {
|
|
360
|
+
controller.abort()
|
|
361
|
+
throw err
|
|
362
|
+
},
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
if (this.options.safe) {
|
|
366
|
+
return await result
|
|
367
|
+
.then((result) => ({ result }))
|
|
368
|
+
.catch((error) => ({ error }))
|
|
369
|
+
.finally(() => {
|
|
370
|
+
this.calls.delete(callId)
|
|
371
|
+
})
|
|
372
|
+
} else {
|
|
373
|
+
return await result.finally(() => {
|
|
374
|
+
this.calls.delete(callId)
|
|
375
|
+
})
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
protected async onConnect() {
|
|
380
|
+
this.emit('connected')
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
protected async onDisconnect(reason: 'client' | 'server' | (string & {})) {
|
|
384
|
+
this.emit('disconnected', reason)
|
|
385
|
+
this.clientStreams.clear(reason)
|
|
386
|
+
this.serverStreams.clear(reason)
|
|
387
|
+
this.rpcStreams.clear(reason)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
protected async onMessage(buffer: ArrayBufferView) {
|
|
391
|
+
if (!this.messageContext) return
|
|
392
|
+
|
|
393
|
+
const message = this.protocol.decodeMessage(this.messageContext, buffer)
|
|
394
|
+
|
|
395
|
+
switch (message.type) {
|
|
396
|
+
case ServerMessageType.RpcResponse:
|
|
397
|
+
this.#handleRPCResponseMessage(message)
|
|
398
|
+
break
|
|
399
|
+
case ServerMessageType.RpcStreamResponse:
|
|
400
|
+
this.#handleRPCStreamResponseMessage(message)
|
|
401
|
+
break
|
|
402
|
+
case ServerMessageType.RpcStreamChunk:
|
|
403
|
+
this.rpcStreams.push(message.callId, message.chunk)
|
|
404
|
+
break
|
|
405
|
+
case ServerMessageType.RpcStreamEnd:
|
|
406
|
+
this.rpcStreams.end(message.callId)
|
|
407
|
+
this.calls.delete(message.callId)
|
|
408
|
+
break
|
|
409
|
+
case ServerMessageType.RpcStreamAbort:
|
|
410
|
+
this.rpcStreams.abort(message.callId)
|
|
411
|
+
this.calls.delete(message.callId)
|
|
412
|
+
break
|
|
413
|
+
case ServerMessageType.ServerStreamPush:
|
|
414
|
+
this.serverStreams.push(message.streamId, message.chunk)
|
|
415
|
+
break
|
|
416
|
+
case ServerMessageType.ServerStreamEnd:
|
|
417
|
+
this.serverStreams.end(message.streamId)
|
|
418
|
+
break
|
|
419
|
+
case ServerMessageType.ServerStreamAbort:
|
|
420
|
+
this.serverStreams.abort(message.streamId)
|
|
421
|
+
break
|
|
422
|
+
case ServerMessageType.ClientStreamPull:
|
|
423
|
+
this.clientStreams.pull(message.streamId, message.size).then(
|
|
424
|
+
(chunk) => {
|
|
425
|
+
if (chunk) {
|
|
426
|
+
const buffer = this.protocol.encodeMessage(
|
|
427
|
+
this.messageContext!,
|
|
428
|
+
ClientMessageType.ClientStreamPush,
|
|
429
|
+
{ streamId: message.streamId, chunk },
|
|
430
|
+
)
|
|
431
|
+
this.#send(buffer).catch(noopFn)
|
|
432
|
+
} else {
|
|
433
|
+
const buffer = this.protocol.encodeMessage(
|
|
434
|
+
this.messageContext!,
|
|
435
|
+
ClientMessageType.ClientStreamEnd,
|
|
436
|
+
{ streamId: message.streamId },
|
|
437
|
+
)
|
|
438
|
+
this.#send(buffer).catch(noopFn)
|
|
439
|
+
this.clientStreams.end(message.streamId)
|
|
440
|
+
}
|
|
441
|
+
},
|
|
442
|
+
() => {
|
|
443
|
+
const buffer = this.protocol.encodeMessage(
|
|
444
|
+
this.messageContext!,
|
|
445
|
+
ClientMessageType.ClientStreamAbort,
|
|
446
|
+
{ streamId: message.streamId },
|
|
447
|
+
)
|
|
448
|
+
this.#send(buffer).catch(noopFn)
|
|
449
|
+
this.clientStreams.remove(message.streamId)
|
|
450
|
+
},
|
|
451
|
+
)
|
|
452
|
+
break
|
|
453
|
+
case ServerMessageType.ClientStreamAbort:
|
|
454
|
+
this.clientStreams.abort(message.streamId)
|
|
455
|
+
break
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
#handleRPCResponseMessage(
|
|
460
|
+
message: ServerMessageTypePayload[ServerMessageType.RpcResponse],
|
|
461
|
+
) {
|
|
462
|
+
const { callId, result, error } = message
|
|
463
|
+
const call = this.calls.get(callId)
|
|
464
|
+
if (!call) return
|
|
465
|
+
if (error) {
|
|
466
|
+
call.reject(new ProtocolError(error.code, error.message, error.data))
|
|
467
|
+
} else {
|
|
468
|
+
try {
|
|
469
|
+
const transformed = this.transformer.decode(call.procedure, result)
|
|
470
|
+
call.resolve(transformed)
|
|
471
|
+
} catch (error) {
|
|
472
|
+
call.reject(
|
|
473
|
+
new ProtocolError(
|
|
474
|
+
ErrorCode.ClientRequestError,
|
|
475
|
+
'Unable to decode response',
|
|
476
|
+
error,
|
|
477
|
+
),
|
|
478
|
+
)
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
#handleRPCStreamResponseMessage(
|
|
484
|
+
message: ServerMessageTypePayload[ServerMessageType.RpcStreamResponse],
|
|
485
|
+
) {
|
|
486
|
+
const call = this.calls.get(message.callId)
|
|
487
|
+
if (message.error) {
|
|
488
|
+
if (!call) return
|
|
489
|
+
call.reject(
|
|
490
|
+
new ProtocolError(
|
|
491
|
+
message.error.code,
|
|
492
|
+
message.error.message,
|
|
493
|
+
message.error.data,
|
|
494
|
+
),
|
|
495
|
+
)
|
|
496
|
+
} else {
|
|
497
|
+
if (call) {
|
|
498
|
+
const { procedure, signal } = call
|
|
499
|
+
const stream = new ProtocolServerRPCStream({
|
|
500
|
+
start: (controller) => {
|
|
501
|
+
if (signal) {
|
|
502
|
+
if (signal.aborted) controller.error(signal.reason)
|
|
503
|
+
else
|
|
504
|
+
signal.addEventListener(
|
|
505
|
+
'abort',
|
|
506
|
+
() => {
|
|
507
|
+
controller.error(signal.reason)
|
|
508
|
+
if (this.rpcStreams.has(message.callId)) {
|
|
509
|
+
this.rpcStreams.remove(message.callId)
|
|
510
|
+
this.calls.delete(message.callId)
|
|
511
|
+
if (this.messageContext) {
|
|
512
|
+
const buffer = this.protocol.encodeMessage(
|
|
513
|
+
this.messageContext,
|
|
514
|
+
ClientMessageType.RpcAbort,
|
|
515
|
+
{ callId: message.callId, reason: signal.reason },
|
|
516
|
+
)
|
|
517
|
+
this.#send(buffer).catch(noopFn)
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
},
|
|
521
|
+
{ once: true },
|
|
522
|
+
)
|
|
523
|
+
}
|
|
524
|
+
},
|
|
525
|
+
transform: (chunk) => {
|
|
526
|
+
return this.transformer.decode(
|
|
527
|
+
procedure,
|
|
528
|
+
this.options.format.decode(chunk),
|
|
529
|
+
)
|
|
530
|
+
},
|
|
531
|
+
pull: () => {
|
|
532
|
+
const buffer = this.protocol.encodeMessage(
|
|
533
|
+
this.messageContext!,
|
|
534
|
+
ClientMessageType.RpcPull,
|
|
535
|
+
{ callId: message.callId },
|
|
536
|
+
)
|
|
537
|
+
this.#send(buffer).catch(noopFn)
|
|
538
|
+
},
|
|
539
|
+
readableStrategy: { highWaterMark: 0 },
|
|
540
|
+
})
|
|
541
|
+
this.rpcStreams.add(message.callId, stream)
|
|
542
|
+
call.resolve(stream)
|
|
543
|
+
} else {
|
|
544
|
+
// Call not found, but stream response received
|
|
545
|
+
// This can happen if the call was aborted or timed out
|
|
546
|
+
// Need to send an abort for the stream to avoid resource leaks from server side
|
|
547
|
+
if (this.messageContext) {
|
|
548
|
+
const buffer = this.protocol.encodeMessage(
|
|
549
|
+
this.messageContext,
|
|
550
|
+
ClientMessageType.RpcAbort,
|
|
551
|
+
{ callId: message.callId },
|
|
552
|
+
)
|
|
553
|
+
this.#send(buffer).catch(noopFn)
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
#handleCallResponse(callId: number, response: ClientCallResponse) {
|
|
560
|
+
const call = this.calls.get(callId)
|
|
561
|
+
|
|
562
|
+
if (response.type === 'rpc_stream') {
|
|
563
|
+
if (call) {
|
|
564
|
+
const stream = new ProtocolServerStream({
|
|
565
|
+
transform: (chunk) => {
|
|
566
|
+
return this.transformer.decode(
|
|
567
|
+
call.procedure,
|
|
568
|
+
this.options.format.decode(chunk),
|
|
569
|
+
)
|
|
570
|
+
},
|
|
571
|
+
})
|
|
572
|
+
this.rpcStreams.add(callId, stream)
|
|
573
|
+
call.resolve(({ signal }: { signal?: AbortSignal }) => {
|
|
574
|
+
response.stream.pipeTo(stream.writable, { signal }).catch(noopFn)
|
|
575
|
+
return stream
|
|
576
|
+
})
|
|
577
|
+
} else {
|
|
578
|
+
// Call not found, but stream response received
|
|
579
|
+
// This can happen if the call was aborted or timed out
|
|
580
|
+
// Need to cancel the stream to avoid resource leaks from server side
|
|
581
|
+
response.stream.cancel().catch(noopFn)
|
|
582
|
+
}
|
|
583
|
+
} else if (response.type === 'blob') {
|
|
584
|
+
if (call) {
|
|
585
|
+
const { metadata, source } = response
|
|
586
|
+
const stream = new ProtocolServerBlobStream(metadata)
|
|
587
|
+
this.serverStreams.add(this.#getStreamId(), stream)
|
|
588
|
+
call.resolve(({ signal }: { signal?: AbortSignal }) => {
|
|
589
|
+
source.pipeTo(stream.writable, { signal }).catch(noopFn)
|
|
590
|
+
return stream
|
|
591
|
+
})
|
|
592
|
+
} else {
|
|
593
|
+
// Call not found, but blob response received
|
|
594
|
+
// This can happen if the call was aborted or timed out
|
|
595
|
+
// Need to cancel the stream to avoid resource leaks from server side
|
|
596
|
+
response.source.cancel().catch(noopFn)
|
|
597
|
+
}
|
|
598
|
+
} else if (response.type === 'rpc') {
|
|
599
|
+
if (!call) return
|
|
600
|
+
try {
|
|
601
|
+
const transformed = this.transformer.decode(
|
|
602
|
+
call.procedure,
|
|
603
|
+
response.result,
|
|
604
|
+
)
|
|
605
|
+
call.resolve(transformed)
|
|
606
|
+
} catch (error) {
|
|
607
|
+
call.reject(
|
|
608
|
+
new ProtocolError(
|
|
609
|
+
ErrorCode.ClientRequestError,
|
|
610
|
+
'Unable to decode response',
|
|
611
|
+
error,
|
|
612
|
+
),
|
|
613
|
+
)
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
#send(buffer: ArrayBufferView, signal?: AbortSignal) {
|
|
619
|
+
if (this.transport.type === ConnectionType.Unidirectional)
|
|
620
|
+
throw new Error('Invalid transport type for send')
|
|
621
|
+
return this.transport.send(buffer, { signal })
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
#getStreamId() {
|
|
625
|
+
if (this.streamId >= MAX_UINT32) {
|
|
626
|
+
this.streamId = 0
|
|
627
|
+
}
|
|
628
|
+
return this.streamId++
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
#getCallId() {
|
|
632
|
+
if (this.callId >= MAX_UINT32) {
|
|
633
|
+
this.callId = 0
|
|
634
|
+
}
|
|
635
|
+
return this.callId++
|
|
636
|
+
}
|
|
637
|
+
}
|
package/src/events.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { Callback } from '@nmtjs/common'
|
|
2
|
+
|
|
3
|
+
export type EventMap = { [K: string]: any[] }
|
|
4
|
+
|
|
5
|
+
// TODO: add errors and promise rejections handling
|
|
6
|
+
/**
|
|
7
|
+
* Thin node-like event emitter wrapper around EventTarget
|
|
8
|
+
*/
|
|
9
|
+
export class EventEmitter<
|
|
10
|
+
Events extends EventMap = EventMap,
|
|
11
|
+
EventName extends Extract<keyof Events, string> = Extract<
|
|
12
|
+
keyof Events,
|
|
13
|
+
string
|
|
14
|
+
>,
|
|
15
|
+
> {
|
|
16
|
+
static once<
|
|
17
|
+
T extends EventEmitter,
|
|
18
|
+
E extends T extends EventEmitter<any, infer Event> ? Event : never,
|
|
19
|
+
>(ee: T, event: E) {
|
|
20
|
+
return new Promise((resolve) => ee.once(event, resolve))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
#target = new EventTarget()
|
|
24
|
+
#listeners = new Map<Callback, Callback>()
|
|
25
|
+
|
|
26
|
+
on<E extends EventName>(
|
|
27
|
+
event: E | (Object & string),
|
|
28
|
+
listener: (...args: Events[E]) => void,
|
|
29
|
+
options?: AddEventListenerOptions,
|
|
30
|
+
) {
|
|
31
|
+
const wrapper = (event) => listener(...event.detail)
|
|
32
|
+
this.#listeners.set(listener, wrapper)
|
|
33
|
+
this.#target.addEventListener(event, wrapper, { ...options, once: false })
|
|
34
|
+
return () => this.#target.removeEventListener(event, wrapper)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
once<E extends EventName>(
|
|
38
|
+
event: E | (Object & string),
|
|
39
|
+
listener: (...args: Events[E]) => void,
|
|
40
|
+
options?: AddEventListenerOptions,
|
|
41
|
+
) {
|
|
42
|
+
return this.on(event, listener, { ...options, once: true })
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
off(event: EventName | (Object & string), listener: Callback) {
|
|
46
|
+
const wrapper = this.#listeners.get(listener)
|
|
47
|
+
if (wrapper) this.#target.removeEventListener(event, wrapper)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
emit<E extends EventName | (Object & string)>(
|
|
51
|
+
event: E,
|
|
52
|
+
...args: E extends EventName ? Events[E] : any[]
|
|
53
|
+
) {
|
|
54
|
+
return this.#target.dispatchEvent(new CustomEvent(event, { detail: args }))
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const once = <
|
|
59
|
+
T extends EventEmitter,
|
|
60
|
+
EventMap extends T extends EventEmitter<infer E, any> ? E : never,
|
|
61
|
+
EventName extends T extends EventEmitter<any, infer N> ? N : never,
|
|
62
|
+
>(
|
|
63
|
+
ee: T,
|
|
64
|
+
event: EventName,
|
|
65
|
+
signal?: AbortSignal,
|
|
66
|
+
) => {
|
|
67
|
+
return new Promise<EventMap[EventName]>((resolve) => {
|
|
68
|
+
ee.once(event, resolve, { signal })
|
|
69
|
+
})
|
|
70
|
+
}
|
package/src/index.ts
ADDED
package/src/streams.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { ProtocolBlobMetadata } from '@nmtjs/protocol'
|
|
2
|
+
import type { ProtocolServerStreamInterface } from '@nmtjs/protocol/client'
|
|
3
|
+
import { ProtocolClientBlobStream } from '@nmtjs/protocol/client'
|
|
4
|
+
|
|
5
|
+
export class ClientStreams {
|
|
6
|
+
readonly #collection = new Map<number, ProtocolClientBlobStream>()
|
|
7
|
+
|
|
8
|
+
get size() {
|
|
9
|
+
return this.#collection.size
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
get(streamId: number) {
|
|
13
|
+
const stream = this.#collection.get(streamId)
|
|
14
|
+
if (!stream) throw new Error('Stream not found')
|
|
15
|
+
return stream
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
add(
|
|
19
|
+
source: ReadableStream,
|
|
20
|
+
streamId: number,
|
|
21
|
+
metadata: ProtocolBlobMetadata,
|
|
22
|
+
) {
|
|
23
|
+
const stream = new ProtocolClientBlobStream(source, streamId, metadata)
|
|
24
|
+
this.#collection.set(streamId, stream)
|
|
25
|
+
return stream
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
remove(streamId: number) {
|
|
29
|
+
this.#collection.delete(streamId)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async abort(streamId: number, reason?: string) {
|
|
33
|
+
const stream = this.#collection.get(streamId)
|
|
34
|
+
if (!stream) return // Stream already cleaned up
|
|
35
|
+
await stream.abort(reason)
|
|
36
|
+
this.remove(streamId)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
pull(streamId: number, size: number) {
|
|
40
|
+
const stream = this.get(streamId)
|
|
41
|
+
return stream.read(size)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async end(streamId: number) {
|
|
45
|
+
await this.get(streamId).end()
|
|
46
|
+
this.remove(streamId)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async clear(reason?: string) {
|
|
50
|
+
if (reason) {
|
|
51
|
+
const abortPromises = [...this.#collection.values()].map((stream) =>
|
|
52
|
+
stream.abort(reason),
|
|
53
|
+
)
|
|
54
|
+
await Promise.all(abortPromises)
|
|
55
|
+
}
|
|
56
|
+
this.#collection.clear()
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export class ServerStreams<
|
|
61
|
+
T extends ProtocolServerStreamInterface = ProtocolServerStreamInterface,
|
|
62
|
+
> {
|
|
63
|
+
readonly #collection = new Map<number, T>()
|
|
64
|
+
readonly #writers = new Map<number, WritableStreamDefaultWriter>()
|
|
65
|
+
|
|
66
|
+
get size() {
|
|
67
|
+
return this.#collection.size
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
has(streamId: number) {
|
|
71
|
+
return this.#collection.has(streamId)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
get(streamId: number) {
|
|
75
|
+
const stream = this.#collection.get(streamId)
|
|
76
|
+
if (!stream) throw new Error('Stream not found')
|
|
77
|
+
return stream
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
add(streamId: number, stream: T) {
|
|
81
|
+
this.#collection.set(streamId, stream)
|
|
82
|
+
this.#writers.set(
|
|
83
|
+
streamId,
|
|
84
|
+
stream.writable.getWriter() as WritableStreamDefaultWriter,
|
|
85
|
+
)
|
|
86
|
+
return stream
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
remove(streamId: number) {
|
|
90
|
+
this.#collection.delete(streamId)
|
|
91
|
+
this.#writers.delete(streamId)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async abort(streamId: number) {
|
|
95
|
+
if (this.has(streamId)) {
|
|
96
|
+
const writer = this.#writers.get(streamId)
|
|
97
|
+
if (writer) {
|
|
98
|
+
await writer.abort()
|
|
99
|
+
writer.releaseLock()
|
|
100
|
+
}
|
|
101
|
+
this.remove(streamId)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async push(streamId: number, chunk: ArrayBufferView) {
|
|
106
|
+
const writer = this.#writers.get(streamId)
|
|
107
|
+
if (writer) {
|
|
108
|
+
return await writer.write(chunk)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async end(streamId: number) {
|
|
113
|
+
const writer = this.#writers.get(streamId)
|
|
114
|
+
if (writer) {
|
|
115
|
+
await writer.close()
|
|
116
|
+
writer.releaseLock()
|
|
117
|
+
}
|
|
118
|
+
this.remove(streamId)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async clear(reason?: string) {
|
|
122
|
+
if (reason) {
|
|
123
|
+
const abortPromises = [...this.#writers.values()].map((writer) =>
|
|
124
|
+
writer.abort(reason).finally(() => writer.releaseLock()),
|
|
125
|
+
)
|
|
126
|
+
await Promise.allSettled(abortPromises)
|
|
127
|
+
}
|
|
128
|
+
this.#collection.clear()
|
|
129
|
+
this.#writers.clear()
|
|
130
|
+
}
|
|
131
|
+
}
|
package/src/transport.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ConnectionType,
|
|
3
|
+
ProtocolBlobMetadata,
|
|
4
|
+
ProtocolVersion,
|
|
5
|
+
} from '@nmtjs/protocol'
|
|
6
|
+
import type { BaseClientFormat } from '@nmtjs/protocol/client'
|
|
7
|
+
|
|
8
|
+
export type ClientTransportMessageOptions = {
|
|
9
|
+
signal?: AbortSignal
|
|
10
|
+
_stream_response?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ClientTransportStartParams {
|
|
14
|
+
auth?: string
|
|
15
|
+
application?: string
|
|
16
|
+
onMessage: (message: ArrayBufferView) => any
|
|
17
|
+
onConnect: () => any
|
|
18
|
+
onDisconnect: (reason: 'client' | 'server' | (string & {})) => any
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ClientTransportRpcParams {
|
|
22
|
+
format: BaseClientFormat
|
|
23
|
+
auth?: string
|
|
24
|
+
application?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type ClientCallResponse =
|
|
28
|
+
| { type: 'rpc'; result: ArrayBufferView }
|
|
29
|
+
| { type: 'rpc_stream'; stream: ReadableStream<ArrayBufferView> }
|
|
30
|
+
| {
|
|
31
|
+
type: 'blob'
|
|
32
|
+
metadata: ProtocolBlobMetadata
|
|
33
|
+
source: ReadableStream<ArrayBufferView>
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type ClientTransport<T extends ConnectionType = ConnectionType> =
|
|
37
|
+
T extends ConnectionType.Bidirectional
|
|
38
|
+
? {
|
|
39
|
+
type: ConnectionType.Bidirectional
|
|
40
|
+
connect(params: ClientTransportStartParams): Promise<void>
|
|
41
|
+
disconnect(): Promise<void>
|
|
42
|
+
send(
|
|
43
|
+
message: ArrayBufferView,
|
|
44
|
+
options: ClientTransportMessageOptions,
|
|
45
|
+
): Promise<void>
|
|
46
|
+
}
|
|
47
|
+
: {
|
|
48
|
+
type: ConnectionType.Unidirectional
|
|
49
|
+
connect?(params: ClientTransportStartParams): Promise<void>
|
|
50
|
+
disconnect?(): Promise<void>
|
|
51
|
+
call(
|
|
52
|
+
client: {
|
|
53
|
+
format: BaseClientFormat
|
|
54
|
+
auth?: string
|
|
55
|
+
application?: string
|
|
56
|
+
},
|
|
57
|
+
rpc: { callId: number; procedure: string; payload: any },
|
|
58
|
+
options: ClientTransportMessageOptions,
|
|
59
|
+
): Promise<ClientCallResponse>
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface ClientTransportParams {
|
|
63
|
+
protocol: ProtocolVersion
|
|
64
|
+
format: BaseClientFormat
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export type ClientTransportFactory<
|
|
68
|
+
Type extends ConnectionType,
|
|
69
|
+
Options = unknown,
|
|
70
|
+
Transport extends ClientTransport<Type> = ClientTransport<Type>,
|
|
71
|
+
> = (params: ClientTransportParams, options: Options) => Transport
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { CallTypeProvider, OneOf, TypeProvider } from '@nmtjs/common'
|
|
2
|
+
import type { TAnyProcedureContract, TAnyRouterContract } from '@nmtjs/contract'
|
|
3
|
+
import type { ProtocolBlobInterface } from '@nmtjs/protocol'
|
|
4
|
+
import type {
|
|
5
|
+
ProtocolError,
|
|
6
|
+
ProtocolServerBlobStream,
|
|
7
|
+
ProtocolServerStreamInterface,
|
|
8
|
+
} from '@nmtjs/protocol/client'
|
|
9
|
+
import type { BaseTypeAny, PlainType, t } from '@nmtjs/type'
|
|
10
|
+
|
|
11
|
+
export const ResolvedType: unique symbol = Symbol('ResolvedType')
|
|
12
|
+
export type ResolvedType = typeof ResolvedType
|
|
13
|
+
|
|
14
|
+
export type ClientCallOptions = {
|
|
15
|
+
timeout?: number
|
|
16
|
+
signal?: AbortSignal
|
|
17
|
+
/**
|
|
18
|
+
* @internal
|
|
19
|
+
*/
|
|
20
|
+
_stream_response?: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type ClientOutputType<T> = T extends ProtocolBlobInterface
|
|
24
|
+
? (options?: { signal?: AbortSignal }) => ProtocolServerBlobStream
|
|
25
|
+
: T extends { [PlainType]?: true }
|
|
26
|
+
? { [K in keyof Omit<T, PlainType>]: ClientOutputType<T[K]> }
|
|
27
|
+
: T
|
|
28
|
+
|
|
29
|
+
export interface StaticInputContractTypeProvider extends TypeProvider {
|
|
30
|
+
output: this['input'] extends BaseTypeAny
|
|
31
|
+
? t.infer.decode.input<this['input']>
|
|
32
|
+
: never
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface RuntimeInputContractTypeProvider extends TypeProvider {
|
|
36
|
+
output: this['input'] extends BaseTypeAny
|
|
37
|
+
? t.infer.encode.input<this['input']>
|
|
38
|
+
: never
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface StaticOutputContractTypeProvider extends TypeProvider {
|
|
42
|
+
output: this['input'] extends BaseTypeAny
|
|
43
|
+
? ClientOutputType<t.infer.encodeRaw.output<this['input']>>
|
|
44
|
+
: never
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface RuntimeOutputContractTypeProvider extends TypeProvider {
|
|
48
|
+
output: this['input'] extends BaseTypeAny
|
|
49
|
+
? ClientOutputType<t.infer.decodeRaw.output<this['input']>>
|
|
50
|
+
: never
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type AnyResolvedContractProcedure = {
|
|
54
|
+
[ResolvedType]: 'procedure'
|
|
55
|
+
contract: TAnyProcedureContract
|
|
56
|
+
stream: boolean
|
|
57
|
+
input: any
|
|
58
|
+
output: any
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export type AnyResolvedContractRouter = {
|
|
62
|
+
[ResolvedType]: 'router'
|
|
63
|
+
[key: string]:
|
|
64
|
+
| AnyResolvedContractProcedure
|
|
65
|
+
| { [ResolvedType]: 'router'; [key: string]: AnyResolvedContractProcedure }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type ResolveAPIRouterRoutes<
|
|
69
|
+
T extends TAnyRouterContract,
|
|
70
|
+
InputTypeProvider extends TypeProvider = TypeProvider,
|
|
71
|
+
OutputTypeProvider extends TypeProvider = TypeProvider,
|
|
72
|
+
> = { [ResolvedType]: 'router' } & {
|
|
73
|
+
[K in keyof T['routes']]: T['routes'][K] extends TAnyProcedureContract
|
|
74
|
+
? {
|
|
75
|
+
[ResolvedType]: 'procedure'
|
|
76
|
+
contract: T['routes'][K]
|
|
77
|
+
stream: T['routes'][K]['stream'] extends true ? true : false
|
|
78
|
+
input: CallTypeProvider<InputTypeProvider, T['routes'][K]['input']>
|
|
79
|
+
output: T['routes'][K]['stream'] extends true
|
|
80
|
+
? AsyncIterable<
|
|
81
|
+
CallTypeProvider<OutputTypeProvider, T['routes'][K]['output']>
|
|
82
|
+
>
|
|
83
|
+
: CallTypeProvider<OutputTypeProvider, T['routes'][K]['output']>
|
|
84
|
+
}
|
|
85
|
+
: T['routes'][K] extends TAnyRouterContract
|
|
86
|
+
? ResolveAPIRouterRoutes<
|
|
87
|
+
T['routes'][K],
|
|
88
|
+
InputTypeProvider,
|
|
89
|
+
OutputTypeProvider
|
|
90
|
+
>
|
|
91
|
+
: never
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export type ResolveContract<
|
|
95
|
+
C extends TAnyRouterContract = TAnyRouterContract,
|
|
96
|
+
InputTypeProvider extends TypeProvider = TypeProvider,
|
|
97
|
+
OutputTypeProvider extends TypeProvider = TypeProvider,
|
|
98
|
+
> = ResolveAPIRouterRoutes<C, InputTypeProvider, OutputTypeProvider>
|
|
99
|
+
|
|
100
|
+
export type ClientCaller<
|
|
101
|
+
Procedure extends AnyResolvedContractProcedure,
|
|
102
|
+
SafeCall extends boolean,
|
|
103
|
+
> = (
|
|
104
|
+
...args: Procedure['input'] extends t.NeverType
|
|
105
|
+
? [data?: undefined, options?: Partial<ClientCallOptions>]
|
|
106
|
+
: undefined extends t.infer.encode.input<Procedure['contract']['input']>
|
|
107
|
+
? [data?: Procedure['input'], options?: Partial<ClientCallOptions>]
|
|
108
|
+
: [data: Procedure['input'], options?: Partial<ClientCallOptions>]
|
|
109
|
+
) => SafeCall extends true
|
|
110
|
+
? Promise<OneOf<[{ result: Procedure['output'] }, { error: ProtocolError }]>>
|
|
111
|
+
: Promise<Procedure['output']>
|
|
112
|
+
|
|
113
|
+
type OmitType<T extends object, E> = {
|
|
114
|
+
[K in keyof T as T[K] extends E ? never : K]: T[K]
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// export type FilterResolvedContractRouter<
|
|
118
|
+
// Resolved extends AnyResolvedContractRouter,
|
|
119
|
+
// Stream extends boolean,
|
|
120
|
+
// > = {
|
|
121
|
+
// [K in keyof Resolved]: Resolved[K] extends AnyResolvedContractProcedure
|
|
122
|
+
// ? Resolved[K]['stream'] extends Stream
|
|
123
|
+
// ? Resolved[K]
|
|
124
|
+
// : never
|
|
125
|
+
// : Resolved[K] extends AnyResolvedContractRouter
|
|
126
|
+
// ? FilterResolvedContractRouter<Resolved[K], Stream>
|
|
127
|
+
// : never
|
|
128
|
+
// }
|
|
129
|
+
|
|
130
|
+
export type ClientCallers<
|
|
131
|
+
Resolved extends AnyResolvedContractRouter,
|
|
132
|
+
SafeCall extends boolean,
|
|
133
|
+
Stream extends boolean,
|
|
134
|
+
> = OmitType<
|
|
135
|
+
{
|
|
136
|
+
[K in keyof Resolved]: Resolved[K] extends AnyResolvedContractProcedure
|
|
137
|
+
? Stream extends (Resolved[K]['stream'] extends true ? true : false)
|
|
138
|
+
? ClientCaller<Resolved[K], SafeCall>
|
|
139
|
+
: never
|
|
140
|
+
: Resolved[K] extends AnyResolvedContractRouter
|
|
141
|
+
? ClientCallers<Resolved[K], SafeCall, Stream>
|
|
142
|
+
: never
|
|
143
|
+
},
|
|
144
|
+
never
|
|
145
|
+
>
|