@bigmistqke/rpc 0.1.3 → 0.1.5
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/.claude/settings.local.json +7 -0
- package/dist/fetch-node.js +140 -1
- package/dist/fetch-node.js.map +1 -1
- package/dist/fetch.d.ts +1 -1
- package/dist/fetch.js +148 -7
- package/dist/fetch.js.map +1 -1
- package/dist/handle-18d6fe9b.d.ts +24 -0
- package/dist/messenger.d.ts +33 -11
- package/dist/messenger.js +149 -39
- package/dist/messenger.js.map +1 -1
- package/dist/stream.d.ts +1 -1
- package/dist/stream.js +69 -28
- package/dist/stream.js.map +1 -1
- package/dist/{types-a5ce9c9a.d.ts → types-9f54da43.d.ts} +5 -1
- package/dist/websocket.d.ts +50 -0
- package/dist/websocket.js +617 -0
- package/dist/websocket.js.map +1 -0
- package/package.json +5 -1
- package/src/core.ts +100 -0
- package/src/fetch/index.ts +2 -1
- package/src/fetch/node.ts +1 -1
- package/src/handle.ts +48 -0
- package/src/{messenger.ts → messenger/index.ts} +67 -25
- package/src/{message-protocol.ts → protocol.ts} +11 -1
- package/src/server-send-events/index.ts +3 -2
- package/src/stream/index.ts +3 -14
- package/src/types.ts +6 -1
- package/src/utils.ts +0 -32
- package/src/websocket/index.ts +142 -0
- package/test/messenger.test.ts +249 -35
- package/test/{message-protocol.test.ts → protocol.test.ts} +7 -7
- package/test/sse.test.ts +3 -3
- package/test/stream.test.ts +1 -5
- package/test/utils.test.ts +1 -2
- package/test/websocket.test.ts +514 -0
- package/tsup.config.ts +2 -1
package/src/core.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { HANDLE_NAMESPACE_PREFIX, isHandleMarker, nextHandleNamespaceId } from './handle'
|
|
2
|
+
import { $MESSENGER_HANDLE, HandleResponseShape } from './protocol'
|
|
3
|
+
import type { RPC } from './types'
|
|
4
|
+
|
|
5
|
+
export function createCommander<T extends object = object>(
|
|
6
|
+
apply: (topics: Array<string>, args: Array<any>) => void,
|
|
7
|
+
): T {
|
|
8
|
+
function _createCommander(
|
|
9
|
+
topics: Array<string>,
|
|
10
|
+
apply: (topics: Array<string>, args: Array<any>) => void,
|
|
11
|
+
): T {
|
|
12
|
+
return new Proxy(function () { } as T, {
|
|
13
|
+
get(target, topic) {
|
|
14
|
+
if (typeof topic === 'symbol') return (target as any)[topic]
|
|
15
|
+
// Return undefined for 'then' so proxy isn't treated as thenable
|
|
16
|
+
if (topic === 'then') return undefined
|
|
17
|
+
return _createCommander([...topics, topic], apply)
|
|
18
|
+
},
|
|
19
|
+
apply(_, __, args) {
|
|
20
|
+
return apply(topics, args)
|
|
21
|
+
},
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
return _createCommander([], apply)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function callMethod(methods: object, topics: string[], args: unknown[]) {
|
|
28
|
+
const method = topics.reduce((acc, topic) => {
|
|
29
|
+
const result = (acc as any)?.[topic]
|
|
30
|
+
return result
|
|
31
|
+
}, methods)
|
|
32
|
+
if (typeof method !== 'function') {
|
|
33
|
+
throw new Error(`Topics did not resolve to a function: [${topics.join(',')}]`)
|
|
34
|
+
}
|
|
35
|
+
return method.call(methods, ...args)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Check if a response payload is a handle response
|
|
40
|
+
*/
|
|
41
|
+
export function isHandleResponse(value: unknown): value is { [$MESSENGER_HANDLE]: string } {
|
|
42
|
+
return (
|
|
43
|
+
!!value &&
|
|
44
|
+
typeof value === 'object' &&
|
|
45
|
+
$MESSENGER_HANDLE in value &&
|
|
46
|
+
typeof (value as any)[$MESSENGER_HANDLE] === 'string'
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Creates a request handler for expose() that manages namespace routing and handle() sub-proxies.
|
|
52
|
+
* Returns the processed result ready to be sent back over the transport.
|
|
53
|
+
*/
|
|
54
|
+
export function createExposeRequestHandler(methods: object) {
|
|
55
|
+
const namespaceHandlers = new Map<string, object>()
|
|
56
|
+
|
|
57
|
+
const processResult = (result: unknown): unknown => {
|
|
58
|
+
if (isHandleMarker(result)) {
|
|
59
|
+
const namespaceId = nextHandleNamespaceId()
|
|
60
|
+
namespaceHandlers.set(namespaceId, result.methods)
|
|
61
|
+
return HandleResponseShape.create(namespaceId)
|
|
62
|
+
}
|
|
63
|
+
return result
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return async (topics: string[], args: unknown[]): Promise<unknown> => {
|
|
67
|
+
const firstTopic = topics[0]
|
|
68
|
+
if (firstTopic && firstTopic.startsWith(HANDLE_NAMESPACE_PREFIX)) {
|
|
69
|
+
const handler = namespaceHandlers.get(firstTopic)
|
|
70
|
+
if (!handler) {
|
|
71
|
+
throw new Error(`Unknown namespace: ${firstTopic}`)
|
|
72
|
+
}
|
|
73
|
+
const result = await callMethod(handler, topics.slice(1), args)
|
|
74
|
+
return processResult(result)
|
|
75
|
+
}
|
|
76
|
+
const result = await callMethod(methods, topics, args)
|
|
77
|
+
return processResult(result)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Creates an RPC commander proxy that handles handle() sub-proxy responses.
|
|
83
|
+
*/
|
|
84
|
+
export function createRpcCommander<T extends object>(
|
|
85
|
+
request: (topics: string[], args: any[]) => Promise<unknown>,
|
|
86
|
+
topicPrefix: string[] = [],
|
|
87
|
+
): RPC<T> {
|
|
88
|
+
return createCommander<RPC<T>>((topics, methodArgs) => {
|
|
89
|
+
const fullTopics = [...topicPrefix, ...topics]
|
|
90
|
+
return request(fullTopics, methodArgs).then((result: unknown) => {
|
|
91
|
+
if (isHandleResponse(result)) {
|
|
92
|
+
return createRpcCommander(
|
|
93
|
+
request,
|
|
94
|
+
result[$MESSENGER_HANDLE] ? [result[$MESSENGER_HANDLE]] : [],
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
return result
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
}
|
package/src/fetch/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as v from 'valibot'
|
|
2
2
|
import { RPC } from '../types'
|
|
3
|
-
import { callMethod, createCommander
|
|
3
|
+
import { callMethod, createCommander } from '../core'
|
|
4
|
+
import { createShape } from '../utils'
|
|
4
5
|
|
|
5
6
|
const $FETCH_HEADER = 'RPC_RR_PROXY'
|
|
6
7
|
|
package/src/fetch/node.ts
CHANGED
package/src/handle.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const $HANDLE_MARKER = Symbol('RPC-HANDLE-MARKER')
|
|
2
|
+
|
|
3
|
+
/** Internal marker type for handle() */
|
|
4
|
+
interface HandleMarker<T extends object> {
|
|
5
|
+
[$HANDLE_MARKER]: true
|
|
6
|
+
methods: T
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Type for values returned by handle() - unwrapped to RPC<T> by RPC system */
|
|
10
|
+
export type Handled<T extends object> = T & { readonly ['__rpc_handled__']: T }
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Mark methods to be returned as a sub-proxy from an RPC method.
|
|
14
|
+
* Use this when a method needs to return an object with callable methods.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* expose({
|
|
19
|
+
* init(canvas: OffscreenCanvas) {
|
|
20
|
+
* const renderer = createRenderer(canvas)
|
|
21
|
+
* return handle({
|
|
22
|
+
* render: () => renderer.render(),
|
|
23
|
+
* resize: (w, h) => renderer.resize(w, h),
|
|
24
|
+
* })
|
|
25
|
+
* }
|
|
26
|
+
* })
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export function handle<T extends object>(methods: T): Handled<T> {
|
|
30
|
+
return { [$HANDLE_MARKER]: true, methods } as unknown as Handled<T>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Check if a value is a handle marker
|
|
35
|
+
*/
|
|
36
|
+
export function isHandleMarker<T extends object>(value: unknown): value is HandleMarker<T> {
|
|
37
|
+
return !!value && typeof value === 'object' && $HANDLE_MARKER in value
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Prefix for namespace IDs to avoid collisions
|
|
41
|
+
export const HANDLE_NAMESPACE_PREFIX = '__rpc_handle_'
|
|
42
|
+
|
|
43
|
+
// Counter for generating unique namespace IDs
|
|
44
|
+
export let handleNamespaceCounter = 0
|
|
45
|
+
|
|
46
|
+
export function nextHandleNamespaceId(): string {
|
|
47
|
+
return `${HANDLE_NAMESPACE_PREFIX}${handleNamespaceCounter++}`
|
|
48
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createExposeRequestHandler, createRpcCommander } from '../core'
|
|
1
2
|
import {
|
|
2
3
|
$MESSENGER_ERROR,
|
|
3
4
|
$MESSENGER_RESPONSE,
|
|
@@ -6,13 +7,13 @@ import {
|
|
|
6
7
|
RequestShape,
|
|
7
8
|
ResponseShape,
|
|
8
9
|
RPCPayloadShape,
|
|
9
|
-
} from '
|
|
10
|
-
import { RPC } from '
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
} from '../protocol'
|
|
11
|
+
import type { RPC } from '../types'
|
|
12
|
+
import { createIdRegistry, defer } from '../utils'
|
|
13
|
+
export { handle, type Handled } from '../handle'
|
|
13
14
|
|
|
14
15
|
export const $TRANSFER = 'RPC-TRANSFER'
|
|
15
|
-
export const $MESSENGER = Symbol('RPC-MESSENGER')
|
|
16
|
+
export const $MESSENGER = Symbol.for('RPC-MESSENGER')
|
|
16
17
|
|
|
17
18
|
/** Wrapper type for transferable values */
|
|
18
19
|
export type Transferred<T> = T & {
|
|
@@ -166,7 +167,10 @@ export function createResponder(
|
|
|
166
167
|
try {
|
|
167
168
|
const result = await callback(data)
|
|
168
169
|
// Extract transferables from the result
|
|
169
|
-
const {
|
|
170
|
+
const {
|
|
171
|
+
args: [processedResult],
|
|
172
|
+
transferables,
|
|
173
|
+
} = extractTransferables([result])
|
|
170
174
|
postMessage(ResponseShape.create(data, processedResult), transferables)
|
|
171
175
|
} catch (error) {
|
|
172
176
|
postMessage(ErrorShape.create(data, error))
|
|
@@ -188,29 +192,64 @@ export function createResponder(
|
|
|
188
192
|
/**********************************************************************************/
|
|
189
193
|
|
|
190
194
|
/**
|
|
191
|
-
* Exposes
|
|
195
|
+
* Exposes methods as an RPC endpoint over the given messenger.
|
|
192
196
|
*
|
|
193
|
-
* @param methods -
|
|
197
|
+
* @param methods - Object containing methods to expose
|
|
194
198
|
* @param options - Optional target Messenger and abort signal
|
|
199
|
+
*
|
|
200
|
+
* @example
|
|
201
|
+
* ```ts
|
|
202
|
+
* // Worker side - simple methods
|
|
203
|
+
* expose({
|
|
204
|
+
* add: (a, b) => a + b,
|
|
205
|
+
* multiply: (a, b) => a * b,
|
|
206
|
+
* })
|
|
207
|
+
*
|
|
208
|
+
* // Worker side - with initialization returning sub-proxy
|
|
209
|
+
* expose({
|
|
210
|
+
* init(canvas: OffscreenCanvas) {
|
|
211
|
+
* const renderer = createRenderer(canvas)
|
|
212
|
+
* return handle({
|
|
213
|
+
* render: () => renderer.render(),
|
|
214
|
+
* resize: (w, h) => renderer.resize(w, h),
|
|
215
|
+
* })
|
|
216
|
+
* }
|
|
217
|
+
* })
|
|
218
|
+
* ```
|
|
195
219
|
*/
|
|
196
|
-
export function expose<
|
|
197
|
-
methods:
|
|
220
|
+
export function expose<TMethods extends object>(
|
|
221
|
+
methods: TMethods,
|
|
198
222
|
{ to = self, signal }: { to?: Messenger; signal?: AbortSignal } = {},
|
|
199
|
-
) {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
223
|
+
): void {
|
|
224
|
+
const postMessage = usePostMessage(to)
|
|
225
|
+
const handleRequest = createExposeRequestHandler(methods)
|
|
226
|
+
|
|
227
|
+
to.addEventListener(
|
|
228
|
+
'message',
|
|
229
|
+
async event => {
|
|
230
|
+
const data = (event as MessageEvent).data
|
|
231
|
+
if (RequestShape.validate(data)) {
|
|
204
232
|
try {
|
|
205
|
-
|
|
206
|
-
|
|
233
|
+
if (RPCPayloadShape.validate(data.payload)) {
|
|
234
|
+
const result = await handleRequest(data.payload.topics, data.payload.args)
|
|
235
|
+
const {
|
|
236
|
+
args: [finalResult],
|
|
237
|
+
transferables,
|
|
238
|
+
} = extractTransferables([result])
|
|
239
|
+
postMessage(ResponseShape.create(data, finalResult), transferables)
|
|
240
|
+
}
|
|
207
241
|
} catch (error) {
|
|
208
|
-
console.error('Error while processing rpc request:', error, data.payload
|
|
242
|
+
console.error('Error while processing rpc request:', error, data.payload)
|
|
243
|
+
postMessage(ErrorShape.create(data, error))
|
|
209
244
|
}
|
|
210
245
|
}
|
|
211
246
|
},
|
|
212
247
|
{ signal },
|
|
213
248
|
)
|
|
249
|
+
|
|
250
|
+
if ('start' in to) {
|
|
251
|
+
to.start?.()
|
|
252
|
+
}
|
|
214
253
|
}
|
|
215
254
|
|
|
216
255
|
/**
|
|
@@ -218,18 +257,19 @@ export function expose<T extends object>(
|
|
|
218
257
|
*
|
|
219
258
|
* @param messenger - The Messenger to communicate with (e.g. Worker or Window)
|
|
220
259
|
* @param options - Optional abort signal
|
|
221
|
-
* @returns A proxy object
|
|
260
|
+
* @returns A proxy object for calling remote methods
|
|
222
261
|
*
|
|
223
262
|
* @example
|
|
224
263
|
* ```ts
|
|
225
|
-
*
|
|
226
|
-
* const
|
|
264
|
+
* // Create RPC proxy (synchronous)
|
|
265
|
+
* const worker = rpc<WorkerMethods>(new Worker('worker.js'))
|
|
227
266
|
*
|
|
228
|
-
* // Call
|
|
229
|
-
* await
|
|
267
|
+
* // Call methods that return handle() get sub-proxies
|
|
268
|
+
* const renderer = await worker.init(transfer(canvas))
|
|
269
|
+
* await renderer.render()
|
|
230
270
|
*
|
|
231
271
|
* // Access underlying messenger
|
|
232
|
-
*
|
|
272
|
+
* worker[$MESSENGER].terminate()
|
|
233
273
|
* ```
|
|
234
274
|
*/
|
|
235
275
|
// Overloads for specific messenger types to enable proper type inference
|
|
@@ -269,9 +309,11 @@ export function rpc<T extends object, M extends Messenger>(
|
|
|
269
309
|
options?: { signal?: AbortSignal },
|
|
270
310
|
): RPC<T> & { [$MESSENGER]: M } {
|
|
271
311
|
const request = createRequester(messenger, options)
|
|
272
|
-
|
|
312
|
+
|
|
313
|
+
const proxy = createRpcCommander<T>((topics, args) => {
|
|
273
314
|
const { args: processedArgs, transferables } = extractTransferables(args)
|
|
274
315
|
return request(RPCPayloadShape.create(topics, processedArgs), transferables)
|
|
275
316
|
})
|
|
317
|
+
|
|
276
318
|
return Object.assign(proxy, { [$MESSENGER]: messenger })
|
|
277
319
|
}
|
|
@@ -22,7 +22,7 @@ export const $MESSENGER_RESPONSE = 'RPC_PROXY_RESPONSE'
|
|
|
22
22
|
export const ResponseShape = createShape(
|
|
23
23
|
v.object({
|
|
24
24
|
[$MESSENGER_RESPONSE]: v.number(),
|
|
25
|
-
payload: v.unknown(),
|
|
25
|
+
payload: v.optional(v.unknown()),
|
|
26
26
|
}),
|
|
27
27
|
(request: RequestData, payload: any) => ({
|
|
28
28
|
[$MESSENGER_RESPONSE]: request[$MESSENGER_REQUEST],
|
|
@@ -55,3 +55,13 @@ export const RPCPayloadShape = createShape(
|
|
|
55
55
|
}),
|
|
56
56
|
(topics: Array<string>, args: Array<any>) => ({ [$MESSENGER_RPC_REQUEST]: true, topics, args }),
|
|
57
57
|
)
|
|
58
|
+
|
|
59
|
+
// Handle response - when a method returns handle(), client creates sub-proxy
|
|
60
|
+
export const $MESSENGER_HANDLE = 'RPC_PROXY_HANDLE'
|
|
61
|
+
|
|
62
|
+
export const HandleResponseShape = createShape(
|
|
63
|
+
v.object({
|
|
64
|
+
[$MESSENGER_HANDLE]: v.string(), // namespace ID
|
|
65
|
+
}),
|
|
66
|
+
(namespaceId: string) => ({ [$MESSENGER_HANDLE]: namespaceId }),
|
|
67
|
+
)
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { $MESSENGER_REQUEST, RequestShape, RPCPayloadShape } from '../message-protocol'
|
|
2
1
|
import * as v from 'valibot'
|
|
2
|
+
import { $MESSENGER_REQUEST, RequestShape, RPCPayloadShape } from '../protocol'
|
|
3
3
|
import { RPC } from '../types'
|
|
4
|
-
import { callMethod, createCommander
|
|
4
|
+
import { callMethod, createCommander } from '../core'
|
|
5
|
+
import { createIdRegistry, createShape, defer } from '../utils'
|
|
5
6
|
|
|
6
7
|
const $SSE_RESPONSE_HEADER = 'RPC_SSE_RESPONSE'
|
|
7
8
|
|
package/src/stream/index.ts
CHANGED
|
@@ -1,18 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
$MESSENGER_RESPONSE,
|
|
3
|
-
RequestShape,
|
|
4
|
-
ResponseShape,
|
|
5
|
-
RPCPayloadShape,
|
|
6
|
-
} from '../message-protocol'
|
|
1
|
+
import { $MESSENGER_RESPONSE, RequestShape, ResponseShape, RPCPayloadShape } from '../protocol'
|
|
7
2
|
import { RPC } from '../types'
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
createCommander,
|
|
11
|
-
createPromiseRegistry,
|
|
12
|
-
createReadableStream,
|
|
13
|
-
defer,
|
|
14
|
-
streamToAsyncIterable,
|
|
15
|
-
} from '../utils'
|
|
3
|
+
import { callMethod, createCommander } from '../core'
|
|
4
|
+
import { createPromiseRegistry, createReadableStream, defer, streamToAsyncIterable } from '../utils'
|
|
16
5
|
|
|
17
6
|
interface StreamCodec {
|
|
18
7
|
serialize(value: any, onChunk: (chunk: any) => void): void
|
package/src/types.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
export type Fn = (...arg: Array<any>) => any
|
|
2
2
|
export type MaybePromise<T> = T | Promise<T>
|
|
3
3
|
|
|
4
|
+
/** Check if T is a Handled type and extract the inner type */
|
|
5
|
+
type UnwrapHandled<T> = T extends { readonly ['__rpc_handled__']: infer U extends object }
|
|
6
|
+
? RPC<U>
|
|
7
|
+
: T
|
|
8
|
+
|
|
4
9
|
// To prevent error: `Type instantiation is excessively deep and possibly infinite.`
|
|
5
10
|
type isObject<T> = T extends object ? true : false
|
|
6
11
|
|
|
@@ -47,7 +52,7 @@ type FilterNoResponseMethod<T> = {
|
|
|
47
52
|
/**********************************************************************************/
|
|
48
53
|
|
|
49
54
|
export interface ResponseMethod<T extends Fn> {
|
|
50
|
-
(...args: Parameters<T>): Promise<Awaited<ReturnType<T
|
|
55
|
+
(...args: Parameters<T>): Promise<UnwrapHandled<Awaited<ReturnType<T>>>>
|
|
51
56
|
}
|
|
52
57
|
|
|
53
58
|
export type ResponseRPCNode<T> = T extends Fn
|
package/src/utils.ts
CHANGED
|
@@ -58,26 +58,6 @@ export function defer<T = void>() {
|
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
export function createCommander<T extends object = object>(
|
|
62
|
-
apply: (topics: Array<string>, args: Array<any>) => void,
|
|
63
|
-
): T {
|
|
64
|
-
function _createCommander(
|
|
65
|
-
topics: Array<string>,
|
|
66
|
-
apply: (topics: Array<string>, args: Array<any>) => void,
|
|
67
|
-
): T {
|
|
68
|
-
return new Proxy(function () {} as T, {
|
|
69
|
-
get(target, topic) {
|
|
70
|
-
if (typeof topic === 'symbol') return (target as any)[topic]
|
|
71
|
-
return _createCommander([...topics, topic], apply)
|
|
72
|
-
},
|
|
73
|
-
apply(_, __, args) {
|
|
74
|
-
return apply(topics, args)
|
|
75
|
-
},
|
|
76
|
-
})
|
|
77
|
-
}
|
|
78
|
-
return _createCommander([], apply)
|
|
79
|
-
}
|
|
80
|
-
|
|
81
61
|
/**
|
|
82
62
|
* Creates a schema-backed shape definition with a validator and constructor.
|
|
83
63
|
*
|
|
@@ -94,18 +74,6 @@ export function createShape<
|
|
|
94
74
|
}
|
|
95
75
|
}
|
|
96
76
|
|
|
97
|
-
// expose-core.ts
|
|
98
|
-
export function callMethod(methods: object, topics: string[], args: unknown[]) {
|
|
99
|
-
const method = topics.reduce((acc, topic) => {
|
|
100
|
-
const result = (acc as any)?.[topic]
|
|
101
|
-
return result
|
|
102
|
-
}, methods)
|
|
103
|
-
if (typeof method !== 'function') {
|
|
104
|
-
throw new Error(`Topics did not resolve to a function: [${topics.join(',')}]`)
|
|
105
|
-
}
|
|
106
|
-
return method(...args)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
77
|
// NOTE: safari does not implement AsyncIterator for ReadableStream
|
|
110
78
|
// see https://caniuse.com/mdn-api_readablestream_--asynciterator
|
|
111
79
|
export function streamToAsyncIterable<T>(stream: ReadableStream<T>): AsyncIterable<T> {
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import * as v from 'valibot'
|
|
2
|
+
import { createExposeRequestHandler, createRpcCommander } from '../core'
|
|
3
|
+
import {
|
|
4
|
+
$MESSENGER_ERROR,
|
|
5
|
+
$MESSENGER_RESPONSE,
|
|
6
|
+
ErrorShape,
|
|
7
|
+
RequestShape,
|
|
8
|
+
ResponseShape,
|
|
9
|
+
RPCPayloadShape,
|
|
10
|
+
} from '../protocol'
|
|
11
|
+
import { RPC as BaseRPC } from '../types'
|
|
12
|
+
import { createIdRegistry, createShape, defer } from '../utils'
|
|
13
|
+
export { handle, type Handled } from '../handle'
|
|
14
|
+
|
|
15
|
+
export const $WEBSOCKET = Symbol.for('RPC-WEBSOCKET')
|
|
16
|
+
|
|
17
|
+
export interface WebSocketLike {
|
|
18
|
+
send(data: string): void
|
|
19
|
+
close(): void
|
|
20
|
+
addEventListener(type: string, listener: (event: unknown) => void): void
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type RPC<T extends object, WS extends WebSocketLike = WebSocketLike> = BaseRPC<T> & {
|
|
24
|
+
[$WEBSOCKET]: WS
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**********************************************************************************/
|
|
28
|
+
/* */
|
|
29
|
+
/* Requester / Responder */
|
|
30
|
+
/* */
|
|
31
|
+
/**********************************************************************************/
|
|
32
|
+
|
|
33
|
+
const MessageEventShape = createShape(v.object({ data: v.unknown() }), (data: unknown) => ({
|
|
34
|
+
data,
|
|
35
|
+
}))
|
|
36
|
+
|
|
37
|
+
function parseData(raw: unknown): unknown {
|
|
38
|
+
return JSON.parse(typeof raw === 'string' ? raw : String(raw))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function createRequester(ws: WebSocketLike) {
|
|
42
|
+
const promiseRegistry = createIdRegistry<{
|
|
43
|
+
resolve(value: any): void
|
|
44
|
+
reject(value: unknown): void
|
|
45
|
+
}>()
|
|
46
|
+
|
|
47
|
+
ws.addEventListener('message', (event: unknown) => {
|
|
48
|
+
if (!MessageEventShape.validate(event)) return
|
|
49
|
+
try {
|
|
50
|
+
const data = parseData(event.data)
|
|
51
|
+
if (ErrorShape.validate(data)) {
|
|
52
|
+
promiseRegistry.free(data[$MESSENGER_ERROR])?.reject(data.error)
|
|
53
|
+
} else if (ResponseShape.validate(data)) {
|
|
54
|
+
promiseRegistry.free(data[$MESSENGER_RESPONSE])?.resolve(data.payload)
|
|
55
|
+
}
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error(error)
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
return (payload: any) => {
|
|
62
|
+
const { promise, resolve, reject } = defer()
|
|
63
|
+
const id = promiseRegistry.register({ resolve, reject })
|
|
64
|
+
ws.send(JSON.stringify(RequestShape.create(id, payload)))
|
|
65
|
+
return promise
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**********************************************************************************/
|
|
70
|
+
/* */
|
|
71
|
+
/* Expose / Rpc */
|
|
72
|
+
/* */
|
|
73
|
+
/**********************************************************************************/
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Exposes methods as an RPC endpoint over the given WebSocket.
|
|
77
|
+
*
|
|
78
|
+
* @param methods - Object containing methods to expose
|
|
79
|
+
* @param options - Target WebSocket and optional abort signal
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* ```ts
|
|
83
|
+
* expose({
|
|
84
|
+
* add: (a, b) => a + b,
|
|
85
|
+
* multiply: (a, b) => a * b,
|
|
86
|
+
* }, { to: ws })
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
export function expose<TMethods extends object>(
|
|
90
|
+
methods: TMethods,
|
|
91
|
+
{ to }: { to: WebSocketLike },
|
|
92
|
+
): void {
|
|
93
|
+
const handleRequest = createExposeRequestHandler(methods)
|
|
94
|
+
|
|
95
|
+
to.addEventListener('message', async (event: unknown) => {
|
|
96
|
+
if (!MessageEventShape.validate(event)) return
|
|
97
|
+
try {
|
|
98
|
+
const data = parseData(event.data)
|
|
99
|
+
if (RequestShape.validate(data)) {
|
|
100
|
+
try {
|
|
101
|
+
if (RPCPayloadShape.validate(data.payload)) {
|
|
102
|
+
const result = await handleRequest(data.payload.topics, data.payload.args)
|
|
103
|
+
to.send(JSON.stringify(ResponseShape.create(data, result)))
|
|
104
|
+
}
|
|
105
|
+
} catch (error) {
|
|
106
|
+
console.error('Error while processing rpc request:', error, data.payload)
|
|
107
|
+
to.send(JSON.stringify(ErrorShape.create(data, error)))
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
} catch (error) {
|
|
111
|
+
console.error(error)
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Creates an RPC proxy for calling remote methods on the given WebSocket.
|
|
118
|
+
*
|
|
119
|
+
* @param ws - The WebSocket to communicate with
|
|
120
|
+
* @param options - Optional abort signal
|
|
121
|
+
* @returns A proxy object for calling remote methods
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* ```ts
|
|
125
|
+
* const server = rpc<ServerMethods>(ws)
|
|
126
|
+
* const result = await server.add(2, 3)
|
|
127
|
+
*
|
|
128
|
+
* // Access underlying WebSocket
|
|
129
|
+
* server[$WEBSOCKET].close()
|
|
130
|
+
* ```
|
|
131
|
+
*/
|
|
132
|
+
export function rpc<T extends object, WS extends WebSocketLike = WebSocketLike>(
|
|
133
|
+
ws: WS,
|
|
134
|
+
): RPC<T, WS> {
|
|
135
|
+
const request = createRequester(ws)
|
|
136
|
+
|
|
137
|
+
const proxy = createRpcCommander<T>((topics, args) => {
|
|
138
|
+
return request(RPCPayloadShape.create(topics, args))
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
return Object.assign(proxy, { [$WEBSOCKET]: ws }) as RPC<T, WS>
|
|
142
|
+
}
|