@nmtjs/protocol 0.6.0
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/LICENSE.md +7 -0
- package/README.md +9 -0
- package/dist/client/events.js +41 -0
- package/dist/client/events.js.map +1 -0
- package/dist/client/format.js +2 -0
- package/dist/client/format.js.map +1 -0
- package/dist/client/index.js +4 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/protocol.js +311 -0
- package/dist/client/protocol.js.map +1 -0
- package/dist/client/stream.js +99 -0
- package/dist/client/stream.js.map +1 -0
- package/dist/common/binary.js +25 -0
- package/dist/common/binary.js.map +1 -0
- package/dist/common/blob.js +42 -0
- package/dist/common/blob.js.map +1 -0
- package/dist/common/enums.js +44 -0
- package/dist/common/enums.js.map +1 -0
- package/dist/common/index.js +4 -0
- package/dist/common/index.js.map +1 -0
- package/dist/common/types.js +1 -0
- package/dist/common/types.js.map +1 -0
- package/dist/server/api.js +1 -0
- package/dist/server/api.js.map +1 -0
- package/dist/server/connection.js +21 -0
- package/dist/server/connection.js.map +1 -0
- package/dist/server/constants.js +1 -0
- package/dist/server/constants.js.map +1 -0
- package/dist/server/format.js +48 -0
- package/dist/server/format.js.map +1 -0
- package/dist/server/index.js +10 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/injectables.js +22 -0
- package/dist/server/injectables.js.map +1 -0
- package/dist/server/protocol.js +293 -0
- package/dist/server/protocol.js.map +1 -0
- package/dist/server/registry.js +19 -0
- package/dist/server/registry.js.map +1 -0
- package/dist/server/stream.js +30 -0
- package/dist/server/stream.js.map +1 -0
- package/dist/server/transport.js +7 -0
- package/dist/server/transport.js.map +1 -0
- package/dist/server/utils.js +10 -0
- package/dist/server/utils.js.map +1 -0
- package/lib/client/events.ts +66 -0
- package/lib/client/format.ts +22 -0
- package/lib/client/index.ts +4 -0
- package/lib/client/protocol.ts +440 -0
- package/lib/client/stream.ts +116 -0
- package/lib/common/binary.ts +60 -0
- package/lib/common/blob.ts +70 -0
- package/lib/common/enums.ts +46 -0
- package/lib/common/index.ts +4 -0
- package/lib/common/types.ts +64 -0
- package/lib/server/api.ts +47 -0
- package/lib/server/connection.ts +57 -0
- package/lib/server/constants.ts +4 -0
- package/lib/server/format.ts +107 -0
- package/lib/server/index.ts +10 -0
- package/lib/server/injectables.ts +51 -0
- package/lib/server/protocol.ts +422 -0
- package/lib/server/registry.ts +24 -0
- package/lib/server/stream.ts +43 -0
- package/lib/server/transport.ts +36 -0
- package/lib/server/utils.ts +22 -0
- package/package.json +39 -0
- package/tsconfig.json +3 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../lib/server/transport.ts"],"sourcesContent":["import type { Async } from '@nmtjs/common'\nimport type { BasePlugin, PluginContext } from '@nmtjs/core'\nimport type { ServerMessageType } from '../common/enums.ts'\nimport type { Connection } from './connection.ts'\nimport { kTransportPlugin } from './constants.ts'\nimport type { Protocol } from './protocol.ts'\nimport type { ProtocolRegistry } from './registry.ts'\n\nexport interface Transport<T = unknown> {\n start: () => Promise<void>\n stop: () => Promise<void>\n send: (\n connection: Connection<T>,\n messageType: ServerMessageType,\n buffer: ArrayBuffer,\n ) => Async<void>\n}\n\nexport interface TransportPluginContext extends PluginContext {\n protocol: Protocol\n registry: ProtocolRegistry\n}\n\nexport interface TransportPlugin<Type = unknown, Options = unknown>\n extends BasePlugin<Transport<Type>, Options, TransportPluginContext> {\n [kTransportPlugin]: any\n}\n\nexport const createTransport = <Type = unknown, Options = unknown>(\n name: string,\n init: TransportPlugin<Type, Options>['init'],\n): TransportPlugin<Type, Options> => ({ name, init, [kTransportPlugin]: true })\n\nexport const isTransportPlugin = (\n plugin: BasePlugin<any, any, any>,\n): plugin is TransportPlugin => kTransportPlugin in plugin\n"],"names":["kTransportPlugin","createTransport","name","init","isTransportPlugin","plugin"],"mappings":"AAIA,SAASA,gBAAgB,QAAQ,iBAAgB;AAwBjD,OAAO,MAAMC,kBAAkB,CAC7BC,MACAC,OACoC,CAAA;QAAED;QAAMC;QAAM,CAACH,iBAAiB,EAAE;IAAK,CAAA,EAAE;AAE/E,OAAO,MAAMI,oBAAoB,CAC/BC,SAC8BL,oBAAoBK,OAAM"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const getFormat = (format, { acceptType, contentType })=>{
|
|
2
|
+
const encoder = contentType ? format.supportsEncoder(contentType) : undefined;
|
|
3
|
+
if (!encoder) throw new Error('Unsupported content-type');
|
|
4
|
+
const decoder = acceptType ? format.supportsDecoder(acceptType) : undefined;
|
|
5
|
+
if (!decoder) throw new Error('Unsupported accept');
|
|
6
|
+
return {
|
|
7
|
+
encoder,
|
|
8
|
+
decoder
|
|
9
|
+
};
|
|
10
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../lib/server/utils.ts"],"sourcesContent":["import type { Format } from './format.ts'\n\nexport type ResolveFormatParams = {\n contentType?: string | null\n acceptType?: string | null\n}\n\nexport const getFormat = (\n format: Format,\n { acceptType, contentType }: ResolveFormatParams,\n) => {\n const encoder = contentType ? format.supportsEncoder(contentType) : undefined\n if (!encoder) throw new Error('Unsupported content-type')\n\n const decoder = acceptType ? format.supportsDecoder(acceptType) : undefined\n if (!decoder) throw new Error('Unsupported accept')\n\n return {\n encoder,\n decoder,\n }\n}\n"],"names":["getFormat","format","acceptType","contentType","encoder","supportsEncoder","undefined","Error","decoder","supportsDecoder"],"mappings":"AAOA,OAAO,MAAMA,YAAY,CACvBC,QACA,EAAEC,UAAU,EAAEC,WAAW,EAAuB;IAEhD,MAAMC,UAAUD,cAAcF,OAAOI,eAAe,CAACF,eAAeG;IACpE,IAAI,CAACF,SAAS,MAAM,IAAIG,MAAM;IAE9B,MAAMC,UAAUN,aAAaD,OAAOQ,eAAe,CAACP,cAAcI;IAClE,IAAI,CAACE,SAAS,MAAM,IAAID,MAAM;IAE9B,OAAO;QACLH;QACAI;IACF;AACF,EAAC"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { Callback } from '@nmtjs/common'
|
|
2
|
+
|
|
3
|
+
export type EventMap = { [K: string]: any[] }
|
|
4
|
+
|
|
5
|
+
export function untilAborted(signal: AbortSignal) {
|
|
6
|
+
return new Promise((_, reject) => {
|
|
7
|
+
const handler = () => reject(new Error('aborted'))
|
|
8
|
+
const options = { once: true }
|
|
9
|
+
signal.addEventListener('abort', handler, options)
|
|
10
|
+
})
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function onAbort(signal: AbortSignal, listener: () => void) {
|
|
14
|
+
signal.addEventListener('abort', listener, { once: true })
|
|
15
|
+
return () => signal.removeEventListener('abort', listener)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Very simple node-like event emitter wrapper around EventTarget
|
|
20
|
+
*
|
|
21
|
+
* @todo add errors and promise rejections handling
|
|
22
|
+
*/
|
|
23
|
+
export class EventEmitter<
|
|
24
|
+
Events extends EventMap = EventMap,
|
|
25
|
+
EventNames extends Extract<keyof Events, string> = Extract<
|
|
26
|
+
keyof Events,
|
|
27
|
+
string
|
|
28
|
+
>,
|
|
29
|
+
> {
|
|
30
|
+
#target = new EventTarget()
|
|
31
|
+
#listeners = new Map<Callback, Callback>()
|
|
32
|
+
|
|
33
|
+
on<E extends EventNames>(
|
|
34
|
+
event: E | (Object & string),
|
|
35
|
+
listener: (...args: Events[E]) => void,
|
|
36
|
+
options?: AddEventListenerOptions,
|
|
37
|
+
) {
|
|
38
|
+
const wrapper = (event) => listener(...event.detail)
|
|
39
|
+
this.#listeners.set(listener, wrapper)
|
|
40
|
+
this.#target.addEventListener(event, wrapper, options)
|
|
41
|
+
return () => this.#target.removeEventListener(event, wrapper)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
once<E extends EventNames>(
|
|
45
|
+
event: E | (Object & string),
|
|
46
|
+
listener: (...args: Events[E]) => void,
|
|
47
|
+
options?: AddEventListenerOptions,
|
|
48
|
+
) {
|
|
49
|
+
return this.on(event, listener, { ...options, once: true })
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
off(event: EventNames | (Object & string), listener: Callback) {
|
|
53
|
+
const wrapper = this.#listeners.get(listener)
|
|
54
|
+
if (wrapper) this.#target.removeEventListener(event, wrapper)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
emit<E extends EventNames | (Object & string)>(
|
|
58
|
+
event: E,
|
|
59
|
+
...args: E extends EventEmitter ? Events[E] : any[]
|
|
60
|
+
) {
|
|
61
|
+
return this.#target.dispatchEvent(new CustomEvent(event, { detail: args }))
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const once = (ee: EventEmitter, event: string) =>
|
|
66
|
+
new Promise((resolve) => ee.once(event, resolve))
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BaseClientDecoder,
|
|
3
|
+
BaseClientEncoder,
|
|
4
|
+
DecodeRPCContext,
|
|
5
|
+
EncodeRPCContext,
|
|
6
|
+
ProtocolRPC,
|
|
7
|
+
ProtocolRPCResponse,
|
|
8
|
+
} from '../common/types.ts'
|
|
9
|
+
|
|
10
|
+
export abstract class BaseClientFormat
|
|
11
|
+
implements BaseClientDecoder, BaseClientEncoder
|
|
12
|
+
{
|
|
13
|
+
abstract contentType: string
|
|
14
|
+
|
|
15
|
+
abstract encode(data: any): ArrayBuffer
|
|
16
|
+
abstract encodeRPC(rpc: ProtocolRPC, context: EncodeRPCContext): ArrayBuffer
|
|
17
|
+
abstract decode(buffer: ArrayBuffer): any
|
|
18
|
+
abstract decodeRPC(
|
|
19
|
+
buffer: ArrayBuffer,
|
|
20
|
+
context: DecodeRPCContext,
|
|
21
|
+
): ProtocolRPCResponse
|
|
22
|
+
}
|
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
import { type InteractivePromise, createPromise } from '@nmtjs/common'
|
|
2
|
+
import { concat, decodeNumber, encodeNumber } from '../common/binary.ts'
|
|
3
|
+
import type { ProtocolBlobMetadata } from '../common/blob.ts'
|
|
4
|
+
import { ClientMessageType, ServerMessageType } from '../common/enums.ts'
|
|
5
|
+
import type { ProtocolRPC } from '../common/types.ts'
|
|
6
|
+
import { EventEmitter } from './events.ts'
|
|
7
|
+
import type { BaseClientFormat } from './format.ts'
|
|
8
|
+
import {
|
|
9
|
+
ProtocolClientBlobStream,
|
|
10
|
+
ProtocolServerBlobStream,
|
|
11
|
+
ProtocolServerStream,
|
|
12
|
+
} from './stream.ts'
|
|
13
|
+
|
|
14
|
+
export class ProtocolError extends Error {
|
|
15
|
+
code: string
|
|
16
|
+
data?: any
|
|
17
|
+
|
|
18
|
+
constructor(code: string, message?: string, data?: any) {
|
|
19
|
+
super(message)
|
|
20
|
+
this.code = code
|
|
21
|
+
this.data = data
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
get message() {
|
|
25
|
+
return `${this.code} ${super.message}`
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
toString() {
|
|
29
|
+
return `${this.code} ${this.message}`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
toJSON() {
|
|
33
|
+
return {
|
|
34
|
+
code: this.code,
|
|
35
|
+
message: this.message,
|
|
36
|
+
data: this.data,
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class ProtocolClientStreams {
|
|
42
|
+
readonly #collection = new Map<number, ProtocolClientBlobStream>()
|
|
43
|
+
|
|
44
|
+
get(streamId: number) {
|
|
45
|
+
const stream = this.#collection.get(streamId)
|
|
46
|
+
if (!stream) throw new Error('Stream not found')
|
|
47
|
+
return stream
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
add(
|
|
51
|
+
source: ReadableStream,
|
|
52
|
+
streamId: number,
|
|
53
|
+
metadata: ProtocolBlobMetadata,
|
|
54
|
+
) {
|
|
55
|
+
const stream = new ProtocolClientBlobStream(source, streamId, metadata)
|
|
56
|
+
this.#collection.set(streamId, stream)
|
|
57
|
+
return stream
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
remove(streamId: number) {
|
|
61
|
+
this.#collection.delete(streamId)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
abort(streamId: number) {
|
|
65
|
+
const stream = this.get(streamId)
|
|
66
|
+
stream.abort()
|
|
67
|
+
this.remove(streamId)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
pull(streamId: number, size: number) {
|
|
71
|
+
const stream = this.get(streamId)
|
|
72
|
+
return stream.read(size)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
end(streamId: number) {
|
|
76
|
+
const stream = this.get(streamId)
|
|
77
|
+
stream.end()
|
|
78
|
+
this.remove(streamId)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export class ProtocolServerStreams {
|
|
83
|
+
readonly #collection = new Map<number, ProtocolServerStream>()
|
|
84
|
+
|
|
85
|
+
has(streamId: number) {
|
|
86
|
+
return this.#collection.has(streamId)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
get(streamId: number) {
|
|
90
|
+
const stream = this.#collection.get(streamId)
|
|
91
|
+
if (!stream) throw new Error('Stream not found')
|
|
92
|
+
return stream
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
add(streamId: number, stream: ProtocolServerStream) {
|
|
96
|
+
this.#collection.set(streamId, stream)
|
|
97
|
+
return stream
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
remove(streamId: number) {
|
|
101
|
+
this.#collection.delete(streamId)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
abort(streamId: number) {
|
|
105
|
+
if (this.has(streamId)) {
|
|
106
|
+
const stream = this.get(streamId)
|
|
107
|
+
stream.abort()
|
|
108
|
+
this.remove(streamId)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async push(streamId: number, chunk: ArrayBuffer) {
|
|
113
|
+
const stream = this.get(streamId)
|
|
114
|
+
return await stream.push(chunk)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
end(streamId: number) {
|
|
118
|
+
const stream = this.get(streamId)
|
|
119
|
+
stream.end()
|
|
120
|
+
this.remove(streamId)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface ProtocolTransport
|
|
125
|
+
extends EventEmitter<{
|
|
126
|
+
[K in `${ServerMessageType}`]: [ArrayBuffer]
|
|
127
|
+
}> {
|
|
128
|
+
connect(
|
|
129
|
+
auth: any,
|
|
130
|
+
contentType: BaseClientFormat['contentType'],
|
|
131
|
+
): Promise<void>
|
|
132
|
+
disconnect(): Promise<void>
|
|
133
|
+
send(messageType: ClientMessageType, buffer: ArrayBuffer): Promise<void>
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export class ProtocolBaseTransformer {
|
|
137
|
+
encodeRPC(namespace: string, procedure: string, payload: any) {
|
|
138
|
+
return payload
|
|
139
|
+
}
|
|
140
|
+
decodeRPC(namespace: string, procedure: string, payload: any) {
|
|
141
|
+
return payload
|
|
142
|
+
}
|
|
143
|
+
decodeRPCChunk(namespace: string, procedure: string, payload: any) {
|
|
144
|
+
return payload
|
|
145
|
+
}
|
|
146
|
+
decodeEvent(namespace: string, event: string, payload: any) {
|
|
147
|
+
return payload
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export type ProtocolClientCall = InteractivePromise<any> &
|
|
152
|
+
Pick<ProtocolRPC, 'namespace' | 'procedure'>
|
|
153
|
+
|
|
154
|
+
export abstract class ProtocolBaseClient<
|
|
155
|
+
T extends Record<string, Record<string, any>>,
|
|
156
|
+
> extends EventEmitter<
|
|
157
|
+
{
|
|
158
|
+
[N in keyof T]: {
|
|
159
|
+
[E in keyof T[N] as `${Extract<N, string>}/${Extract<E, string>}`]: [
|
|
160
|
+
payload: T[N][E],
|
|
161
|
+
]
|
|
162
|
+
}
|
|
163
|
+
}[keyof T]
|
|
164
|
+
> {
|
|
165
|
+
readonly #clientStreams: ProtocolClientStreams
|
|
166
|
+
readonly #serverStreams: ProtocolServerStreams
|
|
167
|
+
readonly #serverRPCStreams: ProtocolServerStreams
|
|
168
|
+
readonly #serverRPCStreamCalls = new Map<
|
|
169
|
+
number,
|
|
170
|
+
Pick<ProtocolRPC, 'namespace' | 'procedure'>
|
|
171
|
+
>()
|
|
172
|
+
readonly #calls = new Map<number, ProtocolClientCall>()
|
|
173
|
+
|
|
174
|
+
#callId = 0
|
|
175
|
+
#streamId = 0
|
|
176
|
+
|
|
177
|
+
constructor(
|
|
178
|
+
protected readonly transport: ProtocolTransport,
|
|
179
|
+
protected readonly format: BaseClientFormat,
|
|
180
|
+
protected readonly transformer: ProtocolBaseTransformer = new ProtocolBaseTransformer(),
|
|
181
|
+
) {
|
|
182
|
+
super()
|
|
183
|
+
|
|
184
|
+
this.#clientStreams = new ProtocolClientStreams()
|
|
185
|
+
this.#serverStreams = new ProtocolServerStreams()
|
|
186
|
+
this.#serverRPCStreams = new ProtocolServerStreams()
|
|
187
|
+
|
|
188
|
+
this.transport.on(`${ServerMessageType.Event}`, (buffer) => {
|
|
189
|
+
const [namespace, event, payload] = this.format.decode(buffer)
|
|
190
|
+
const name = `${namespace}/${event}`
|
|
191
|
+
const transformed = this.transformer.decodeEvent(
|
|
192
|
+
namespace,
|
|
193
|
+
event,
|
|
194
|
+
payload,
|
|
195
|
+
)
|
|
196
|
+
this.emit(name, transformed)
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
this.transport.on(`${ServerMessageType.RpcResponse}`, (buffer) => {
|
|
200
|
+
const { call, error, payload } = this.#handleResponse(buffer)
|
|
201
|
+
if (error) call.reject(error)
|
|
202
|
+
else call.resolve(payload)
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
this.transport.on(`${ServerMessageType.RpcStreamResponse}`, (buffer) => {
|
|
206
|
+
const { call, response, payload, error } = this.#handleResponse(buffer)
|
|
207
|
+
if (error) return call.reject(error)
|
|
208
|
+
console.log('Creating RPC stream', response)
|
|
209
|
+
const stream = new ProtocolServerStream()
|
|
210
|
+
this.#serverRPCStreams.add(response.callId, stream)
|
|
211
|
+
this.#serverRPCStreamCalls.set(response.callId, {
|
|
212
|
+
namespace: call.namespace,
|
|
213
|
+
procedure: call.procedure,
|
|
214
|
+
})
|
|
215
|
+
call.resolve([payload, stream])
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
this.transport.on(`${ServerMessageType.RpcStreamChunk}`, async (buffer) => {
|
|
219
|
+
const callId = decodeNumber(buffer, 'Uint32')
|
|
220
|
+
console.log('RPC stream chunk', callId)
|
|
221
|
+
|
|
222
|
+
const chunk = buffer.slice(Uint32Array.BYTES_PER_ELEMENT)
|
|
223
|
+
if (chunk.byteLength === 0) {
|
|
224
|
+
this.#serverRPCStreams.end(callId)
|
|
225
|
+
this.#serverRPCStreamCalls.delete(callId)
|
|
226
|
+
} else {
|
|
227
|
+
const call = this.#serverRPCStreamCalls.get(callId)
|
|
228
|
+
console.log('RPC stream call', call)
|
|
229
|
+
if (call) {
|
|
230
|
+
const payload = this.format.decode(chunk)
|
|
231
|
+
console.log('RPC stream payload', payload)
|
|
232
|
+
try {
|
|
233
|
+
const transformed = this.transformer.decodeRPCChunk(
|
|
234
|
+
call.namespace,
|
|
235
|
+
call.procedure,
|
|
236
|
+
payload,
|
|
237
|
+
)
|
|
238
|
+
await this.#serverRPCStreams.push(callId, transformed)
|
|
239
|
+
} catch (error) {
|
|
240
|
+
this._send(
|
|
241
|
+
ClientMessageType.RpcStreamAbort,
|
|
242
|
+
encodeNumber(callId, 'Uint32'),
|
|
243
|
+
)
|
|
244
|
+
this.#serverRPCStreams.remove(callId)
|
|
245
|
+
this.#serverRPCStreamCalls.delete(callId)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
this.transport.on(`${ServerMessageType.RpcStreamAbort}`, (buffer) => {
|
|
252
|
+
const callId = decodeNumber(buffer, 'Uint32')
|
|
253
|
+
console.log('RPC stream abort', callId)
|
|
254
|
+
const call = this.#calls.get(callId)
|
|
255
|
+
if (call) {
|
|
256
|
+
this.#serverStreams.end(callId)
|
|
257
|
+
this.#serverRPCStreams.abort(callId)
|
|
258
|
+
}
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
this.transport.on(
|
|
262
|
+
`${ServerMessageType.ServerStreamPush}`,
|
|
263
|
+
async (buffer) => {
|
|
264
|
+
const streamId = decodeNumber(buffer, 'Uint32')
|
|
265
|
+
const chunk = buffer.slice(Uint32Array.BYTES_PER_ELEMENT)
|
|
266
|
+
console.log('Server stream push', streamId, chunk.byteLength)
|
|
267
|
+
try {
|
|
268
|
+
await this.#serverStreams.push(streamId, chunk)
|
|
269
|
+
this._send(
|
|
270
|
+
ClientMessageType.ServerStreamPull,
|
|
271
|
+
encodeNumber(streamId, 'Uint32'),
|
|
272
|
+
)
|
|
273
|
+
} catch (error) {
|
|
274
|
+
this._send(
|
|
275
|
+
ClientMessageType.ServerStreamAbort,
|
|
276
|
+
encodeNumber(streamId, 'Uint32'),
|
|
277
|
+
)
|
|
278
|
+
this.#serverStreams.remove(streamId)
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
this.transport.on(`${ServerMessageType.ServerStreamEnd}`, (buffer) => {
|
|
284
|
+
const streamId = decodeNumber(buffer, 'Uint32')
|
|
285
|
+
console.log('Server stream end', streamId)
|
|
286
|
+
this.#serverStreams.end(streamId)
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
this.transport.on(`${ServerMessageType.ServerStreamAbort}`, (buffer) => {
|
|
290
|
+
const streamId = decodeNumber(buffer, 'Uint32')
|
|
291
|
+
console.log('Server stream abort', streamId)
|
|
292
|
+
this.#serverStreams.abort(streamId)
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
this.transport.on(`${ServerMessageType.ClientStreamAbort}`, (buffer) => {
|
|
296
|
+
const streamId = decodeNumber(buffer, 'Uint32')
|
|
297
|
+
console.log('Client stream abort', streamId)
|
|
298
|
+
this.#clientStreams.abort(streamId)
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
this.transport.on(
|
|
302
|
+
`${ServerMessageType.ClientStreamPull}`,
|
|
303
|
+
async (buffer) => {
|
|
304
|
+
const streamId = decodeNumber(buffer, 'Uint32')
|
|
305
|
+
console.log('Client stream pull', streamId)
|
|
306
|
+
const size = decodeNumber(
|
|
307
|
+
buffer,
|
|
308
|
+
'Uint32',
|
|
309
|
+
Uint32Array.BYTES_PER_ELEMENT,
|
|
310
|
+
)
|
|
311
|
+
const streamIdEncoded = encodeNumber(streamId, 'Uint32')
|
|
312
|
+
try {
|
|
313
|
+
const chunk = await this.#clientStreams.pull(streamId, size)
|
|
314
|
+
if (chunk) {
|
|
315
|
+
this._send(
|
|
316
|
+
ClientMessageType.ClientStreamPush,
|
|
317
|
+
concat(streamIdEncoded, chunk),
|
|
318
|
+
)
|
|
319
|
+
} else {
|
|
320
|
+
this._send(ClientMessageType.ClientStreamEnd, streamIdEncoded)
|
|
321
|
+
this.#clientStreams.end(streamId)
|
|
322
|
+
}
|
|
323
|
+
} catch (error) {
|
|
324
|
+
console.error(error)
|
|
325
|
+
this._send(ClientMessageType.ClientStreamAbort, streamIdEncoded)
|
|
326
|
+
}
|
|
327
|
+
},
|
|
328
|
+
)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async connect(auth: any) {
|
|
332
|
+
return await this.transport.connect(auth, this.format.contentType)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async disconnect() {
|
|
336
|
+
return await this.transport.disconnect()
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
protected async _send(messageType: ClientMessageType, buffer: ArrayBuffer) {
|
|
340
|
+
console.log(
|
|
341
|
+
'Client transport send',
|
|
342
|
+
ClientMessageType[messageType],
|
|
343
|
+
buffer.byteLength,
|
|
344
|
+
)
|
|
345
|
+
return await this.transport.send(messageType, buffer)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
protected async _call(
|
|
349
|
+
namespace: string,
|
|
350
|
+
procedure: string,
|
|
351
|
+
payload: any,
|
|
352
|
+
options = {},
|
|
353
|
+
) {
|
|
354
|
+
const callId = ++this.#callId
|
|
355
|
+
const call = Object.assign(createPromise(), {
|
|
356
|
+
namespace,
|
|
357
|
+
procedure,
|
|
358
|
+
})
|
|
359
|
+
const buffer = this.format.encodeRPC(
|
|
360
|
+
{
|
|
361
|
+
callId,
|
|
362
|
+
namespace,
|
|
363
|
+
procedure,
|
|
364
|
+
payload: this.transformer.encodeRPC(namespace, procedure, payload),
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
addStream: (blob) => {
|
|
368
|
+
const streamId = ++this.#streamId
|
|
369
|
+
const stream = this.#clientStreams.add(
|
|
370
|
+
blob.source,
|
|
371
|
+
streamId,
|
|
372
|
+
blob.metadata,
|
|
373
|
+
)
|
|
374
|
+
return stream
|
|
375
|
+
},
|
|
376
|
+
getStream: (id) => {
|
|
377
|
+
const stream = this.#clientStreams.get(id)
|
|
378
|
+
return stream
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
this.transport.send(ClientMessageType.Rpc, buffer).catch(console.error)
|
|
384
|
+
|
|
385
|
+
this.#calls.set(callId, call)
|
|
386
|
+
|
|
387
|
+
return call.promise
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
#handleResponse(buffer: ArrayBuffer) {
|
|
391
|
+
const callStreams: ProtocolServerBlobStream[] = []
|
|
392
|
+
const response = this.format.decodeRPC(buffer, {
|
|
393
|
+
addStream: (id, metadata) => {
|
|
394
|
+
console.log('Client transport blob stream', id, metadata)
|
|
395
|
+
const stream = new ProtocolServerBlobStream(id, metadata, () => {
|
|
396
|
+
this._send(
|
|
397
|
+
ClientMessageType.ServerStreamPull,
|
|
398
|
+
encodeNumber(id, 'Uint32'),
|
|
399
|
+
)
|
|
400
|
+
})
|
|
401
|
+
callStreams.push(stream)
|
|
402
|
+
this.#serverStreams.add(id, stream)
|
|
403
|
+
return stream
|
|
404
|
+
},
|
|
405
|
+
getStream: (id) => {
|
|
406
|
+
return this.#serverStreams.get(id)
|
|
407
|
+
},
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
console.log('Client transport response', response)
|
|
411
|
+
|
|
412
|
+
const call = this.#calls.get(response.callId)
|
|
413
|
+
|
|
414
|
+
if (call) {
|
|
415
|
+
this.#calls.delete(response.callId)
|
|
416
|
+
|
|
417
|
+
if (response.error) {
|
|
418
|
+
const error = new ProtocolError(
|
|
419
|
+
response.error.code,
|
|
420
|
+
response.error.message,
|
|
421
|
+
response.error.data,
|
|
422
|
+
)
|
|
423
|
+
return { call, response, error }
|
|
424
|
+
} else {
|
|
425
|
+
const payload = this.transformer.decodeRPC(
|
|
426
|
+
call.namespace,
|
|
427
|
+
call.procedure,
|
|
428
|
+
response.payload,
|
|
429
|
+
)
|
|
430
|
+
return { call, response, payload }
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
for (const stream of callStreams) {
|
|
435
|
+
this.#serverStreams.abort(stream.id)
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
throw new Error('Call not found')
|
|
439
|
+
}
|
|
440
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { type Callback, defer } from '@nmtjs/common'
|
|
2
|
+
import { encodeText } from '../common/binary.ts'
|
|
3
|
+
import type { ProtocolBlobMetadata } from '../common/blob.ts'
|
|
4
|
+
|
|
5
|
+
export class ProtocolClientBlobStream extends TransformStream<
|
|
6
|
+
any,
|
|
7
|
+
ArrayBuffer
|
|
8
|
+
> {
|
|
9
|
+
#queue: ArrayBuffer
|
|
10
|
+
#reader: ReadableStreamDefaultReader
|
|
11
|
+
|
|
12
|
+
constructor(
|
|
13
|
+
readonly source: ReadableStream,
|
|
14
|
+
readonly id: number,
|
|
15
|
+
readonly metadata: ProtocolBlobMetadata,
|
|
16
|
+
) {
|
|
17
|
+
super({
|
|
18
|
+
start: () => {
|
|
19
|
+
defer(() => source.pipeThrough(this))
|
|
20
|
+
},
|
|
21
|
+
transform: (chunk, controller) => {
|
|
22
|
+
if (chunk instanceof ArrayBuffer) {
|
|
23
|
+
controller.enqueue(chunk)
|
|
24
|
+
} else if (chunk instanceof Uint8Array) {
|
|
25
|
+
controller.enqueue(chunk.buffer)
|
|
26
|
+
} else if (typeof chunk === 'string') {
|
|
27
|
+
controller.enqueue(encodeText(chunk))
|
|
28
|
+
} else {
|
|
29
|
+
throw new Error(
|
|
30
|
+
'Invalid chunk data type. Expected ArrayBuffer, Uint8Array, or string.',
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
this.#queue = new ArrayBuffer(0)
|
|
37
|
+
this.#reader = this.readable.getReader()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async read(size: number) {
|
|
41
|
+
if (this.#queue.byteLength >= size) {
|
|
42
|
+
const chunk = this.#queue.slice(0, size)
|
|
43
|
+
const remaining = this.#queue.slice(size)
|
|
44
|
+
this.#queue = remaining
|
|
45
|
+
return chunk
|
|
46
|
+
} else {
|
|
47
|
+
const { done, value } = await this.#reader.read()
|
|
48
|
+
if (!done) {
|
|
49
|
+
const buffer = value as ArrayBuffer
|
|
50
|
+
const chunk = buffer.slice(0, size)
|
|
51
|
+
const remaining = buffer.slice(size)
|
|
52
|
+
this.#queue = remaining
|
|
53
|
+
return chunk
|
|
54
|
+
}
|
|
55
|
+
return null
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
abort(error = new Error('Stream aborted')) {
|
|
60
|
+
this.#reader.cancel(error)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async end() {
|
|
64
|
+
if (!this.writable.locked && (await this.writable.getWriter().closed)) {
|
|
65
|
+
await this.writable.close()
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export class ProtocolServerStream<T = any> extends TransformStream<any, T> {
|
|
71
|
+
#writer: WritableStreamDefaultWriter
|
|
72
|
+
|
|
73
|
+
constructor(start?: Callback) {
|
|
74
|
+
super({ start })
|
|
75
|
+
this.#writer = this.writable.getWriter()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async *[Symbol.asyncIterator]() {
|
|
79
|
+
const reader = this.readable.getReader()
|
|
80
|
+
while (true) {
|
|
81
|
+
const { done, value } = await reader.read()
|
|
82
|
+
if (!done) yield value
|
|
83
|
+
else return void 0
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async push(chunk: any) {
|
|
88
|
+
await this.#writer.write(chunk)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async end() {
|
|
92
|
+
await this.#writer.close()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
abort(error = new Error('Stream aborted')) {
|
|
96
|
+
this.#writer.abort(error)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export class ProtocolServerBlobStream extends ProtocolServerStream<ArrayBuffer> {
|
|
101
|
+
constructor(
|
|
102
|
+
readonly id: number,
|
|
103
|
+
readonly metadata: ProtocolBlobMetadata,
|
|
104
|
+
start: Callback,
|
|
105
|
+
) {
|
|
106
|
+
super(start)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
push(chunk: ArrayBuffer) {
|
|
110
|
+
return super.push(chunk)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
end() {
|
|
114
|
+
return super.end()
|
|
115
|
+
}
|
|
116
|
+
}
|