@bigmistqke/rpc 0.1.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 +21 -0
- package/dist/fetch-node.d.ts +11 -0
- package/dist/fetch-node.js +342 -0
- package/dist/fetch-node.js.map +1 -0
- package/dist/fetch.d.ts +36 -0
- package/dist/fetch.js +369 -0
- package/dist/fetch.js.map +1 -0
- package/dist/messenger.d.ts +50 -0
- package/dist/messenger.js +540 -0
- package/dist/messenger.js.map +1 -0
- package/dist/stream.d.ts +46 -0
- package/dist/stream.js +601 -0
- package/dist/stream.js.map +1 -0
- package/dist/types-4d4495dd.d.ts +40 -0
- package/package.json +42 -0
- package/src/fetch/index.ts +84 -0
- package/src/fetch/node.ts +44 -0
- package/src/message-protocol.ts +57 -0
- package/src/messenger.ts +176 -0
- package/src/server-send-events/index.ts +129 -0
- package/src/stream/encoding.ts +362 -0
- package/src/stream/index.ts +162 -0
- package/src/types.ts +104 -0
- package/src/utils.ts +159 -0
- package/test/encoding.test.ts +413 -0
- package/test/fetch.test.ts +310 -0
- package/test/message-protocol.test.ts +166 -0
- package/test/messenger.test.ts +316 -0
- package/test/sse.test.ts +356 -0
- package/test/stream.test.ts +351 -0
- package/test/utils.test.ts +336 -0
- package/tsconfig.json +23 -0
- package/tsup.config.ts +17 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import { createIdAllocator, streamToAsyncIterable } from '../utils'
|
|
2
|
+
|
|
3
|
+
const MAX_UINT_32 = Math.pow(2, 32) - 1
|
|
4
|
+
const encoder = new TextEncoder()
|
|
5
|
+
const decoder = new TextDecoder()
|
|
6
|
+
|
|
7
|
+
/**********************************************************************************/
|
|
8
|
+
/* */
|
|
9
|
+
/* Codec */
|
|
10
|
+
/* */
|
|
11
|
+
/**********************************************************************************/
|
|
12
|
+
|
|
13
|
+
type Codec = StructuralCodec | PrimitiveCodec | GeneratorCodec
|
|
14
|
+
|
|
15
|
+
class CodecBase<
|
|
16
|
+
TConfig extends {
|
|
17
|
+
test(value: any): boolean
|
|
18
|
+
encode(value: any): any
|
|
19
|
+
decode(value: any): any
|
|
20
|
+
},
|
|
21
|
+
> {
|
|
22
|
+
test: TConfig['test']
|
|
23
|
+
encode: TConfig['encode']
|
|
24
|
+
decode: TConfig['decode']
|
|
25
|
+
constructor({ test, encode, decode }: TConfig) {
|
|
26
|
+
this.test = test
|
|
27
|
+
this.encode = encode
|
|
28
|
+
this.decode = decode
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface PrimitiveCodecMethods<T> {
|
|
33
|
+
test: (value: any) => boolean
|
|
34
|
+
encode: (value: T) => Uint8Array
|
|
35
|
+
decode: (buffer: Uint8Array) => T
|
|
36
|
+
}
|
|
37
|
+
export class PrimitiveCodec<T = any> extends CodecBase<PrimitiveCodecMethods<T>> {
|
|
38
|
+
type = 'primitive' as const
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface StructuralCodecMethods<T> {
|
|
42
|
+
test: (value: any) => boolean
|
|
43
|
+
encode: (value: T) =>
|
|
44
|
+
| {
|
|
45
|
+
length?: never
|
|
46
|
+
keys: Array<string>
|
|
47
|
+
values: Array<any> | IterableIterator<any>
|
|
48
|
+
}
|
|
49
|
+
| {
|
|
50
|
+
length: number
|
|
51
|
+
keys?: never
|
|
52
|
+
values: Array<any> | IterableIterator<any>
|
|
53
|
+
}
|
|
54
|
+
decode: () => { value: T; set(value: any, key: string | number): void }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export class StructuralCodec<T = any> extends CodecBase<StructuralCodecMethods<T>> {
|
|
58
|
+
type = 'structural' as const
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface GeneratorCodecMethods<T> {
|
|
62
|
+
test: (value: any) => boolean
|
|
63
|
+
encode: (value: T) => AsyncGenerator<Uint8Array>
|
|
64
|
+
decode: () => AsyncGenerator<T, unknown, Uint8Array>
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export class GeneratorCodec<T = any> extends CodecBase<GeneratorCodecMethods<T>> {
|
|
68
|
+
type = 'generator' as const
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const JSONCodec = new PrimitiveCodec({
|
|
72
|
+
test() {
|
|
73
|
+
return true
|
|
74
|
+
},
|
|
75
|
+
encode(value) {
|
|
76
|
+
return encoder.encode(JSON.stringify(value))
|
|
77
|
+
},
|
|
78
|
+
decode(value) {
|
|
79
|
+
const json = decoder.decode(value)
|
|
80
|
+
return json ? JSON.parse(json) : undefined
|
|
81
|
+
},
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
/**********************************************************************************/
|
|
85
|
+
/* */
|
|
86
|
+
/* Binary Schema */
|
|
87
|
+
/* */
|
|
88
|
+
/**********************************************************************************/
|
|
89
|
+
|
|
90
|
+
function createBinarySchema<TConfig extends Record<string, number>>(
|
|
91
|
+
config: TConfig,
|
|
92
|
+
): { offsets: TConfig; size: number }
|
|
93
|
+
function createBinarySchema<
|
|
94
|
+
TConfig extends Record<string, number>,
|
|
95
|
+
TConstants extends Partial<TConfig> = {},
|
|
96
|
+
>(config: TConfig, constants: TConstants): { offsets: TConfig; constants: TConstants; size: number }
|
|
97
|
+
function createBinarySchema(config: Record<string, number>, constants?: Record<string, number>) {
|
|
98
|
+
let offset = 0
|
|
99
|
+
return {
|
|
100
|
+
offsets: Object.fromEntries(
|
|
101
|
+
Object.entries(config).map(([key, value]) => {
|
|
102
|
+
const entry = [key, offset]
|
|
103
|
+
offset += value
|
|
104
|
+
return entry
|
|
105
|
+
}),
|
|
106
|
+
),
|
|
107
|
+
size: offset,
|
|
108
|
+
constants,
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const defaultSchema = createBinarySchema(
|
|
113
|
+
{
|
|
114
|
+
kind: 1,
|
|
115
|
+
header: 1,
|
|
116
|
+
length: 4,
|
|
117
|
+
},
|
|
118
|
+
{ kind: 0x01 },
|
|
119
|
+
)
|
|
120
|
+
const chunkSchema = createBinarySchema(
|
|
121
|
+
{
|
|
122
|
+
kind: 1,
|
|
123
|
+
header: 1,
|
|
124
|
+
length: 4,
|
|
125
|
+
id: 4,
|
|
126
|
+
},
|
|
127
|
+
{ kind: 0x02 },
|
|
128
|
+
)
|
|
129
|
+
const chunkEndSchema = createBinarySchema(
|
|
130
|
+
{
|
|
131
|
+
kind: 1,
|
|
132
|
+
header: 1,
|
|
133
|
+
id: 4,
|
|
134
|
+
},
|
|
135
|
+
{ kind: 0x03 },
|
|
136
|
+
)
|
|
137
|
+
const kindToSchema = {
|
|
138
|
+
[defaultSchema.constants.kind]: defaultSchema,
|
|
139
|
+
[chunkSchema.constants.kind]: chunkSchema,
|
|
140
|
+
[chunkEndSchema.constants.kind]: chunkEndSchema,
|
|
141
|
+
} as const
|
|
142
|
+
type SchemaKind = keyof typeof kindToSchema
|
|
143
|
+
|
|
144
|
+
function packDefault(header: number, encoded: Uint8Array): Uint8Array {
|
|
145
|
+
const { offsets, size, constants } = defaultSchema
|
|
146
|
+
const buffer = new Uint8Array(size + encoded.length)
|
|
147
|
+
buffer[offsets.kind] = constants.kind
|
|
148
|
+
buffer[offsets.header] = header
|
|
149
|
+
const view = new DataView(buffer.buffer)
|
|
150
|
+
view.setUint32(offsets.length, encoded.length)
|
|
151
|
+
if (encoded.length > MAX_UINT_32)
|
|
152
|
+
throw new Error(`Tried to encode something larger than MAX_UINT_32`)
|
|
153
|
+
buffer.set(encoded, size)
|
|
154
|
+
return buffer
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function packChunk(id: number, header: number, encoded: Uint8Array): Uint8Array {
|
|
158
|
+
const { offsets, size, constants } = chunkSchema
|
|
159
|
+
const buffer = new Uint8Array(size + encoded?.length)
|
|
160
|
+
buffer[offsets.kind] = constants.kind
|
|
161
|
+
buffer[offsets.header] = header
|
|
162
|
+
const view = new DataView(buffer.buffer)
|
|
163
|
+
view.setUint32(offsets.id, id)
|
|
164
|
+
view.setUint32(offsets.length, encoded.length)
|
|
165
|
+
if (encoded.length > MAX_UINT_32)
|
|
166
|
+
throw new Error(`Tried to encode something larger than MAX_UINT_32`)
|
|
167
|
+
buffer.set(encoded, size)
|
|
168
|
+
return buffer
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function packChunkEnd(id: number, header: number): Uint8Array {
|
|
172
|
+
const { offsets, size, constants } = chunkEndSchema
|
|
173
|
+
const buffer = new Uint8Array(size)
|
|
174
|
+
buffer[offsets.kind] = constants.kind
|
|
175
|
+
buffer[offsets.header] = header
|
|
176
|
+
const view = new DataView(buffer.buffer)
|
|
177
|
+
view.setUint32(offsets.id, id)
|
|
178
|
+
return buffer
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function unpack(buffer: Uint8Array) {
|
|
182
|
+
const kind = buffer[0]!
|
|
183
|
+
const schema = kindToSchema[kind as SchemaKind]!
|
|
184
|
+
const header = buffer[schema.offsets.header]!
|
|
185
|
+
const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength)
|
|
186
|
+
|
|
187
|
+
const size =
|
|
188
|
+
schema.size + ('length' in schema.offsets ? view.getUint32(schema.offsets.length) : 0)
|
|
189
|
+
const payload = buffer.length < size ? undefined : buffer.slice(schema.size, size)
|
|
190
|
+
const rest = !payload ? undefined : buffer.slice(size)
|
|
191
|
+
const id = 'id' in schema.offsets ? view.getUint32(schema.offsets.id) : undefined
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
kind,
|
|
195
|
+
header,
|
|
196
|
+
id,
|
|
197
|
+
payload,
|
|
198
|
+
rest,
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**********************************************************************************/
|
|
203
|
+
/* */
|
|
204
|
+
/* Create Stream Codec */
|
|
205
|
+
/* */
|
|
206
|
+
/**********************************************************************************/
|
|
207
|
+
|
|
208
|
+
export function createStreamCodec(config: Array<Codec>, fallback: PrimitiveCodec = JSONCodec) {
|
|
209
|
+
config = [...config, fallback]
|
|
210
|
+
const generatorIdAllocator = createIdAllocator()
|
|
211
|
+
const generators: Record<string, AsyncGenerator<unknown, unknown, Uint8Array>> = {}
|
|
212
|
+
|
|
213
|
+
function getCodecFromHeader(header: number) {
|
|
214
|
+
const codec = config[header - 1]
|
|
215
|
+
if (!codec) throw `Unknown Codec ${header}`
|
|
216
|
+
return codec
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function getCodecFromValue(value: any) {
|
|
220
|
+
for (let index = 0; index < config.length; index++) {
|
|
221
|
+
const codec = config[index]!
|
|
222
|
+
if (codec.test(value)) {
|
|
223
|
+
return { codec, header: 0x01 + index }
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
throw new Error(`Unknown Codec`)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function* createChunkGenerator(stream: ReadableStream<Uint8Array>): AsyncGenerator<{
|
|
230
|
+
kind: number
|
|
231
|
+
header: number
|
|
232
|
+
payload: Uint8Array
|
|
233
|
+
id?: number
|
|
234
|
+
}> {
|
|
235
|
+
let buffer = new Uint8Array(new ArrayBuffer(0))
|
|
236
|
+
|
|
237
|
+
for await (const value of streamToAsyncIterable(stream)) {
|
|
238
|
+
// append new chunk to buffer
|
|
239
|
+
buffer = concat(buffer, value!)
|
|
240
|
+
|
|
241
|
+
while (buffer.length >= defaultSchema.size) {
|
|
242
|
+
const { kind, header, payload, rest, id } = unpack(buffer)
|
|
243
|
+
|
|
244
|
+
if (!payload || !rest) break
|
|
245
|
+
|
|
246
|
+
yield { kind, header, payload, id }
|
|
247
|
+
|
|
248
|
+
buffer = rest
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const api = {
|
|
254
|
+
serialize(value: any, onChunk: (chunk: Uint8Array) => void): void {
|
|
255
|
+
const { codec, header } = getCodecFromValue(value)
|
|
256
|
+
const currentGenerators = new Array<{
|
|
257
|
+
generator: AsyncGenerator<Uint8Array>
|
|
258
|
+
id: number
|
|
259
|
+
codec: GeneratorCodec
|
|
260
|
+
header: number
|
|
261
|
+
}>()
|
|
262
|
+
|
|
263
|
+
switch (codec.type) {
|
|
264
|
+
case 'structural':
|
|
265
|
+
const { keys, values, length } = codec.encode(value)
|
|
266
|
+
onChunk(packDefault(header, JSONCodec.encode(length ?? keys)))
|
|
267
|
+
for (const value of values) {
|
|
268
|
+
api.serialize(value, onChunk)
|
|
269
|
+
}
|
|
270
|
+
break
|
|
271
|
+
case 'generator':
|
|
272
|
+
const generator = codec.encode(value)
|
|
273
|
+
const id = generatorIdAllocator.create()
|
|
274
|
+
currentGenerators.push({ generator, codec, id, header })
|
|
275
|
+
onChunk(packDefault(header, JSONCodec.encode({ id })))
|
|
276
|
+
break
|
|
277
|
+
case 'primitive':
|
|
278
|
+
onChunk(packDefault(header, codec.encode(value)))
|
|
279
|
+
break
|
|
280
|
+
default:
|
|
281
|
+
throw new Error(`Unknown Codec`)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (currentGenerators.length) {
|
|
285
|
+
;(async function () {
|
|
286
|
+
for (const { generator, id, header } of currentGenerators) {
|
|
287
|
+
for await (const value of generator) {
|
|
288
|
+
onChunk(packChunk(id, header, value))
|
|
289
|
+
}
|
|
290
|
+
onChunk(packChunkEnd(id, header))
|
|
291
|
+
generatorIdAllocator.free(id)
|
|
292
|
+
}
|
|
293
|
+
})()
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
async deserialize(stream: ReadableStream, onChunk: (value: any) => void) {
|
|
297
|
+
const generator = createChunkGenerator(stream)
|
|
298
|
+
|
|
299
|
+
for await (const { payload, header, kind, id } of generator) {
|
|
300
|
+
switch (kind) {
|
|
301
|
+
case defaultSchema.constants.kind:
|
|
302
|
+
onChunk(await handleCodec({ codec: getCodecFromHeader(header), payload }))
|
|
303
|
+
break
|
|
304
|
+
case chunkSchema.constants.kind:
|
|
305
|
+
generators[id!]!.next(payload)
|
|
306
|
+
break
|
|
307
|
+
case chunkEndSchema.constants.kind:
|
|
308
|
+
await generators[id!]!.return(null)
|
|
309
|
+
delete generators[id!]
|
|
310
|
+
break
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async function handleCodec({ codec, payload }: { codec: Codec; payload: Uint8Array }) {
|
|
315
|
+
switch (codec.type) {
|
|
316
|
+
case 'structural': {
|
|
317
|
+
const paths = JSONCodec.decode(payload) as Array<string> | number
|
|
318
|
+
|
|
319
|
+
const { value, set } = codec.decode()
|
|
320
|
+
|
|
321
|
+
const total = typeof paths === 'number' ? paths : paths.length
|
|
322
|
+
|
|
323
|
+
for (let i = 0; i < total; i++) {
|
|
324
|
+
const key = typeof paths === 'number' ? i : paths[i]!
|
|
325
|
+
const { value, done } = await generator.next()
|
|
326
|
+
if (done) break
|
|
327
|
+
const { payload, header } = value
|
|
328
|
+
const codec = getCodecFromHeader(header)
|
|
329
|
+
set(await handleCodec({ codec, payload }), key)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return value
|
|
333
|
+
}
|
|
334
|
+
case 'generator': {
|
|
335
|
+
const { id } = JSONCodec.decode(payload)
|
|
336
|
+
const generator = codec.decode()
|
|
337
|
+
const { value } = await generator.next()
|
|
338
|
+
generators[id] = generator
|
|
339
|
+
return value
|
|
340
|
+
}
|
|
341
|
+
case 'primitive':
|
|
342
|
+
return codec.decode(payload)
|
|
343
|
+
default:
|
|
344
|
+
throw new Error('Unknown codec')
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
},
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return api
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// helper to concatenate Uint8Arrays
|
|
354
|
+
function concat(...arrays: Array<Uint8Array>): Uint8Array {
|
|
355
|
+
const result = new Uint8Array(new ArrayBuffer(arrays.reduce((a, b) => a + b.length, 0)))
|
|
356
|
+
let index = 0
|
|
357
|
+
return arrays.reduce<Uint8Array>((result, current) => {
|
|
358
|
+
result.set(current, index)
|
|
359
|
+
index += current.length
|
|
360
|
+
return result
|
|
361
|
+
}, result)
|
|
362
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import {
|
|
2
|
+
$MESSENGER_RESPONSE,
|
|
3
|
+
RequestShape,
|
|
4
|
+
ResponseShape,
|
|
5
|
+
RPCPayloadShape,
|
|
6
|
+
} from '../message-protocol'
|
|
7
|
+
import { RPC } from '../types'
|
|
8
|
+
import {
|
|
9
|
+
callMethod,
|
|
10
|
+
createCommander,
|
|
11
|
+
createPromiseRegistry,
|
|
12
|
+
createReadableStream,
|
|
13
|
+
defer,
|
|
14
|
+
streamToAsyncIterable,
|
|
15
|
+
} from '../utils'
|
|
16
|
+
|
|
17
|
+
interface StreamCodec {
|
|
18
|
+
serialize(value: any, onChunk: (chunk: any) => void): void
|
|
19
|
+
deserialize(stream: ReadableStream, onChunk: (chunk: any) => void): void
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const $STREAM_REQUEST_HEADER = 'RPC_STREAM_REQUEST_HEADER'
|
|
23
|
+
|
|
24
|
+
const encoder = new TextEncoder()
|
|
25
|
+
const decoder = new TextDecoder()
|
|
26
|
+
|
|
27
|
+
export const isStreamRequest = (event: { request: Request }) =>
|
|
28
|
+
event.request.headers.has($STREAM_REQUEST_HEADER)
|
|
29
|
+
|
|
30
|
+
/**********************************************************************************/
|
|
31
|
+
/* */
|
|
32
|
+
/* rpc */
|
|
33
|
+
/* */
|
|
34
|
+
/**********************************************************************************/
|
|
35
|
+
|
|
36
|
+
export function rpc<TProxy extends object, TExpose extends object = object>(
|
|
37
|
+
reader:
|
|
38
|
+
| ((stream: ReadableStream) => ReadableStream | Promise<ReadableStream>)
|
|
39
|
+
| ReadableStream
|
|
40
|
+
| Promise<ReadableStream>,
|
|
41
|
+
methods: TExpose,
|
|
42
|
+
codec: StreamCodec = {
|
|
43
|
+
serialize(value: any, onChunk: (chunk: any) => void) {
|
|
44
|
+
onChunk(encoder.encode(`${JSON.stringify(value)}\n`))
|
|
45
|
+
},
|
|
46
|
+
async deserialize(stream, onChunk: (chunk: any) => void) {
|
|
47
|
+
let buffer = ''
|
|
48
|
+
|
|
49
|
+
for await (const value of streamToAsyncIterable(stream)) {
|
|
50
|
+
buffer += decoder.decode(value, { stream: true })
|
|
51
|
+
let newlineIndex
|
|
52
|
+
while ((newlineIndex = buffer.indexOf('\n')) !== -1) {
|
|
53
|
+
const line = buffer.slice(0, newlineIndex)
|
|
54
|
+
buffer = buffer.slice(newlineIndex + 1)
|
|
55
|
+
|
|
56
|
+
onChunk(JSON.parse(line))
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
) {
|
|
62
|
+
const promiseRegistry = createPromiseRegistry()
|
|
63
|
+
const { stream, closed, onClose, enqueue } = createReadableStream()
|
|
64
|
+
|
|
65
|
+
;(async () => {
|
|
66
|
+
const inputStream = await (typeof reader === 'function' ? reader(stream) : reader)
|
|
67
|
+
codec.deserialize(inputStream, async data => {
|
|
68
|
+
if (methods && RequestShape.validate(data) && RPCPayloadShape.validate(data.payload)) {
|
|
69
|
+
const {
|
|
70
|
+
payload: { topics, args },
|
|
71
|
+
} = data
|
|
72
|
+
|
|
73
|
+
codec.serialize(
|
|
74
|
+
ResponseShape.create(data, await callMethod(methods, topics, args)),
|
|
75
|
+
enqueue,
|
|
76
|
+
)
|
|
77
|
+
} else if (ResponseShape.validate(data)) {
|
|
78
|
+
promiseRegistry.free(data[$MESSENGER_RESPONSE])?.resolve(data.payload)
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
})()
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
proxy: createCommander<RPC<TProxy>>(async (topics, args) => {
|
|
85
|
+
if (closed?.()) {
|
|
86
|
+
throw new Error(`[rpc/sse] Stream is closed.`)
|
|
87
|
+
}
|
|
88
|
+
const { promise, resolve, reject } = defer()
|
|
89
|
+
const id = promiseRegistry.register({ resolve, reject })
|
|
90
|
+
|
|
91
|
+
codec.serialize(RequestShape.create(id, RPCPayloadShape.create(topics, args)), enqueue)
|
|
92
|
+
|
|
93
|
+
return promise
|
|
94
|
+
}),
|
|
95
|
+
closed,
|
|
96
|
+
onClose,
|
|
97
|
+
stream,
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**********************************************************************************/
|
|
102
|
+
/* */
|
|
103
|
+
/* Client / Server */
|
|
104
|
+
/* */
|
|
105
|
+
/**********************************************************************************/
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Exposes a set of methods as an RPC endpoint over the given messenger.
|
|
109
|
+
*
|
|
110
|
+
* @param methods - An object containing functions to expose
|
|
111
|
+
* @param options - Optional target Messenger and abort signal
|
|
112
|
+
*/
|
|
113
|
+
export function client<TProxy extends object, TExpose extends object = object>(
|
|
114
|
+
url: string,
|
|
115
|
+
methods: TExpose,
|
|
116
|
+
codec?: StreamCodec,
|
|
117
|
+
) {
|
|
118
|
+
return rpc<TProxy, TExpose>(
|
|
119
|
+
stream =>
|
|
120
|
+
fetch(url, {
|
|
121
|
+
method: 'POST',
|
|
122
|
+
body: stream,
|
|
123
|
+
// @ts-expect-error
|
|
124
|
+
duplex: 'half',
|
|
125
|
+
headers: {
|
|
126
|
+
'Content-Type': 'text/event-stream',
|
|
127
|
+
'Cache-Control': 'no-cache',
|
|
128
|
+
Connection: 'keep-alive',
|
|
129
|
+
[$STREAM_REQUEST_HEADER]: '1',
|
|
130
|
+
},
|
|
131
|
+
}).then(response => response.body!),
|
|
132
|
+
methods,
|
|
133
|
+
codec,
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Creates an RPC proxy for calling remote methods on the given Messenger.
|
|
139
|
+
*
|
|
140
|
+
* @param messenger - The Messenger to communicate with (e.g. Worker or Window)
|
|
141
|
+
* @param options - Optional abort signal
|
|
142
|
+
* @returns A proxy object that lets you call methods remotely
|
|
143
|
+
*/
|
|
144
|
+
export function server<TProxy extends object, TExpose extends object = object>(
|
|
145
|
+
reader: ReadableStream,
|
|
146
|
+
methods: TExpose,
|
|
147
|
+
codec?: StreamCodec,
|
|
148
|
+
) {
|
|
149
|
+
const { proxy, closed, onClose, stream } = rpc<TProxy, TExpose>(reader, methods, codec)
|
|
150
|
+
return {
|
|
151
|
+
proxy,
|
|
152
|
+
closed,
|
|
153
|
+
onClose,
|
|
154
|
+
response: new Response(stream, {
|
|
155
|
+
headers: {
|
|
156
|
+
'Content-Type': 'text/event-stream',
|
|
157
|
+
'Cache-Control': 'no-cache',
|
|
158
|
+
Connection: 'keep-alive',
|
|
159
|
+
},
|
|
160
|
+
}),
|
|
161
|
+
}
|
|
162
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
export type Fn = (...arg: Array<any>) => any
|
|
2
|
+
export type MaybePromise<T> = T | Promise<T>
|
|
3
|
+
|
|
4
|
+
// To prevent error: `Type instantiation is excessively deep and possibly infinite.`
|
|
5
|
+
type isObject<T> = T extends object ? true : false
|
|
6
|
+
|
|
7
|
+
type HasMethod<T> = T extends object
|
|
8
|
+
? {
|
|
9
|
+
[K in keyof T]: T[K] extends Fn ? true : HasMethod<T[K]>
|
|
10
|
+
}[keyof T] extends false
|
|
11
|
+
? false
|
|
12
|
+
: true
|
|
13
|
+
: false
|
|
14
|
+
|
|
15
|
+
/**********************************************************************************/
|
|
16
|
+
/* */
|
|
17
|
+
/* Sync */
|
|
18
|
+
/* */
|
|
19
|
+
/**********************************************************************************/
|
|
20
|
+
|
|
21
|
+
export interface NoResponseMethod<T extends Fn> {
|
|
22
|
+
(...args: Parameters<T>): void
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type NoResponseRPCNode<T> = T extends Fn
|
|
26
|
+
? NoResponseMethod<T>
|
|
27
|
+
: T extends readonly [any, ...any[]] // is it a tuple?
|
|
28
|
+
? { [K in keyof T]: NoResponseRPCNode<T[K]> } // preserve tuple structure
|
|
29
|
+
: // To prevent error: `Type instantiation is excessively deep and possibly infinite.`
|
|
30
|
+
isObject<T> extends true
|
|
31
|
+
? // Filter branches that lead to no method
|
|
32
|
+
HasMethod<T> extends false
|
|
33
|
+
? never
|
|
34
|
+
: {
|
|
35
|
+
[TKey in keyof FilterNoResponseMethod<T>]: NoResponseRPCNode<T[TKey]>
|
|
36
|
+
}
|
|
37
|
+
: never
|
|
38
|
+
|
|
39
|
+
type FilterNoResponseMethod<T> = {
|
|
40
|
+
[TKey in keyof T as NoResponseRPCNode<T[TKey]> extends never ? never : TKey]: T[TKey]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**********************************************************************************/
|
|
44
|
+
/* */
|
|
45
|
+
/* Async */
|
|
46
|
+
/* */
|
|
47
|
+
/**********************************************************************************/
|
|
48
|
+
|
|
49
|
+
export interface ResponseMethod<T extends Fn> {
|
|
50
|
+
(...args: Parameters<T>): Promise<ReturnType<T>>
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type ResponseRPCNode<T> = T extends Fn
|
|
54
|
+
? ResponseMethod<T>
|
|
55
|
+
: T extends readonly [any, ...any[]] // is it a tuple?
|
|
56
|
+
? { [K in keyof T]: ResponseRPCNode<T[K]> } // preserve tuple structure
|
|
57
|
+
: // To prevent error: `Type instantiation is excessively deep and possibly infinite.`
|
|
58
|
+
isObject<T> extends true
|
|
59
|
+
? // Filter branches that lead to no method
|
|
60
|
+
HasMethod<T> extends false
|
|
61
|
+
? never
|
|
62
|
+
: {
|
|
63
|
+
[TKey in keyof FilterResponseMethod<T>]: ResponseRPCNode<T[TKey]>
|
|
64
|
+
}
|
|
65
|
+
: never
|
|
66
|
+
|
|
67
|
+
type FilterResponseMethod<T> = {
|
|
68
|
+
[TKey in keyof T as ResponseRPCNode<T[TKey]> extends never ? never : TKey]: T[TKey]
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**********************************************************************************/
|
|
72
|
+
/* */
|
|
73
|
+
/* RPC */
|
|
74
|
+
/* */
|
|
75
|
+
/**********************************************************************************/
|
|
76
|
+
|
|
77
|
+
export enum RPCKind {
|
|
78
|
+
NoResponse = 'no-response',
|
|
79
|
+
Response = 'response',
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export type RPC<
|
|
83
|
+
TMethods extends object,
|
|
84
|
+
TKind extends RPCKind = RPCKind.Response,
|
|
85
|
+
> = TKind extends RPCKind.Response ? ResponseRPCNode<TMethods> : NoResponseRPCNode<TMethods>
|
|
86
|
+
|
|
87
|
+
/**********************************************************************************/
|
|
88
|
+
/* */
|
|
89
|
+
/* Worker */
|
|
90
|
+
/* */
|
|
91
|
+
/**********************************************************************************/
|
|
92
|
+
|
|
93
|
+
// /** Branded `MessagePort` */
|
|
94
|
+
// export type RPCPort<T extends object> = MessagePort & { [$WORKER]: RPC<T> }
|
|
95
|
+
|
|
96
|
+
// export type $Transfer<T = Array<any>, U = Array<Transferable>> = T & {
|
|
97
|
+
// [$TRANSFER]: U
|
|
98
|
+
// }
|
|
99
|
+
|
|
100
|
+
// export type $Callback<T = Fn> = T & { [$CALLBACK]: number }
|
|
101
|
+
|
|
102
|
+
// export interface WorkerMethod<T extends Fn> {
|
|
103
|
+
// (...args: Parameters<T> | [$Transfer<Parameters<T>>]): void
|
|
104
|
+
// }
|