@bigmistqke/rpc 0.1.2 → 0.1.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/.claude/settings.local.json +7 -0
- package/dist/fetch-node.js +139 -0
- 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 +80 -9
- package/dist/messenger.js +186 -40
- 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-4d4495dd.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 +13 -7
- 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} +163 -21
- package/src/{message-protocol.ts → protocol.ts} +11 -1
- package/src/server-send-events/index.ts +3 -2
- package/src/stream/encoding.ts +2 -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/encoding.test.ts +4 -4
- package/test/fetch.test.ts +3 -3
- package/test/messenger.test.ts +255 -41
- package/test/{message-protocol.test.ts → protocol.test.ts} +7 -7
- package/test/sse.test.ts +3 -3
- package/test/stream.test.ts +4 -8
- package/test/utils.test.ts +1 -2
- package/test/websocket.test.ts +514 -0
- package/tsup.config.ts +2 -1
- package/LICENSE +0 -21
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bigmistqke/rpc",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "RPC toolkit for type-safe communication across Workers, iframes, and network boundaries.",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
"import": "./dist/messenger.js",
|
|
11
11
|
"types": "./dist/messenger.d.ts"
|
|
12
12
|
},
|
|
13
|
+
"./websocket": {
|
|
14
|
+
"import": "./dist/websocket.js",
|
|
15
|
+
"types": "./dist/websocket.d.ts"
|
|
16
|
+
},
|
|
13
17
|
"./stream": {
|
|
14
18
|
"import": "./dist/stream.js",
|
|
15
19
|
"types": "./dist/stream.d.ts"
|
|
@@ -23,6 +27,13 @@
|
|
|
23
27
|
"types": "./dist/fetch-node.d.ts"
|
|
24
28
|
}
|
|
25
29
|
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsup",
|
|
32
|
+
"test": "vitest run",
|
|
33
|
+
"types": "pnpm exec tsc --noEmit",
|
|
34
|
+
"bump": "bumpp",
|
|
35
|
+
"prepublishOnly": "pnpm test && pnpm build"
|
|
36
|
+
},
|
|
26
37
|
"license": "MIT",
|
|
27
38
|
"devDependencies": {
|
|
28
39
|
"tsup": "6.6.3",
|
|
@@ -33,10 +44,5 @@
|
|
|
33
44
|
"@types/node": "^22.15.30",
|
|
34
45
|
"bumpp": "^10.3.2",
|
|
35
46
|
"valibot": "^1.0.0"
|
|
36
|
-
},
|
|
37
|
-
"scripts": {
|
|
38
|
-
"build": "tsup",
|
|
39
|
-
"test": "vitest run",
|
|
40
|
-
"bump": "bumpp"
|
|
41
47
|
}
|
|
42
|
-
}
|
|
48
|
+
}
|
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(...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,11 +7,61 @@ import {
|
|
|
6
7
|
RequestShape,
|
|
7
8
|
ResponseShape,
|
|
8
9
|
RPCPayloadShape,
|
|
9
|
-
} from '
|
|
10
|
-
import { RPC } from '
|
|
11
|
-
import {
|
|
10
|
+
} from '../protocol'
|
|
11
|
+
import type { RPC } from '../types'
|
|
12
|
+
import { createIdRegistry, defer } from '../utils'
|
|
13
|
+
export { handle, type Handled } from '../handle'
|
|
12
14
|
|
|
13
|
-
export const $TRANSFER = '
|
|
15
|
+
export const $TRANSFER = 'RPC-TRANSFER'
|
|
16
|
+
export const $MESSENGER = Symbol.for('RPC-MESSENGER')
|
|
17
|
+
|
|
18
|
+
/** Wrapper type for transferable values */
|
|
19
|
+
export type Transferred<T> = T & {
|
|
20
|
+
[$TRANSFER]: true
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Mark a value as transferable for postMessage.
|
|
25
|
+
* Use this to transfer ownership of ArrayBuffer, ReadableStream, etc.
|
|
26
|
+
*/
|
|
27
|
+
export function transfer<T extends object>(value: T): Transferred<T> {
|
|
28
|
+
return Object.assign(value, { [$TRANSFER]: true } as const)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Check if a value is marked for transfer
|
|
33
|
+
*/
|
|
34
|
+
function isTransferred(value: unknown): value is Transferred<unknown> {
|
|
35
|
+
return !!value && typeof value === 'object' && $TRANSFER in value
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Extract transferables from args and unwrap transferred values
|
|
40
|
+
*/
|
|
41
|
+
function extractTransferables(args: any[]): { args: any[]; transferables: Transferable[] } {
|
|
42
|
+
const transferables: Transferable[] = []
|
|
43
|
+
|
|
44
|
+
const processValue = (value: any): any => {
|
|
45
|
+
if (isTransferred(value)) {
|
|
46
|
+
transferables.push(value)
|
|
47
|
+
return value
|
|
48
|
+
}
|
|
49
|
+
if (Array.isArray(value)) {
|
|
50
|
+
return value.map(processValue)
|
|
51
|
+
}
|
|
52
|
+
if (value && typeof value === 'object' && value.constructor === Object) {
|
|
53
|
+
const result: any = {}
|
|
54
|
+
for (const key in value) {
|
|
55
|
+
result[key] = processValue(value[key])
|
|
56
|
+
}
|
|
57
|
+
return result
|
|
58
|
+
}
|
|
59
|
+
return value
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const processedArgs = args.map(processValue)
|
|
63
|
+
return { args: processedArgs, transferables }
|
|
64
|
+
}
|
|
14
65
|
|
|
15
66
|
interface WorkerMessenger {
|
|
16
67
|
postMessage(message: any, transferables?: any[]): void
|
|
@@ -115,7 +166,12 @@ export function createResponder(
|
|
|
115
166
|
if (RequestShape.validate(data)) {
|
|
116
167
|
try {
|
|
117
168
|
const result = await callback(data)
|
|
118
|
-
|
|
169
|
+
// Extract transferables from the result
|
|
170
|
+
const {
|
|
171
|
+
args: [processedResult],
|
|
172
|
+
transferables,
|
|
173
|
+
} = extractTransferables([result])
|
|
174
|
+
postMessage(ResponseShape.create(data, processedResult), transferables)
|
|
119
175
|
} catch (error) {
|
|
120
176
|
postMessage(ErrorShape.create(data, error))
|
|
121
177
|
}
|
|
@@ -136,29 +192,64 @@ export function createResponder(
|
|
|
136
192
|
/**********************************************************************************/
|
|
137
193
|
|
|
138
194
|
/**
|
|
139
|
-
* Exposes
|
|
195
|
+
* Exposes methods as an RPC endpoint over the given messenger.
|
|
140
196
|
*
|
|
141
|
-
* @param methods -
|
|
197
|
+
* @param methods - Object containing methods to expose
|
|
142
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
|
+
* ```
|
|
143
219
|
*/
|
|
144
|
-
export function expose<
|
|
145
|
-
methods:
|
|
220
|
+
export function expose<TMethods extends object>(
|
|
221
|
+
methods: TMethods,
|
|
146
222
|
{ to = self, signal }: { to?: Messenger; signal?: AbortSignal } = {},
|
|
147
|
-
) {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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)) {
|
|
152
232
|
try {
|
|
153
|
-
|
|
154
|
-
|
|
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
|
+
}
|
|
155
241
|
} catch (error) {
|
|
156
|
-
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))
|
|
157
244
|
}
|
|
158
245
|
}
|
|
159
246
|
},
|
|
160
247
|
{ signal },
|
|
161
248
|
)
|
|
249
|
+
|
|
250
|
+
if ('start' in to) {
|
|
251
|
+
to.start?.()
|
|
252
|
+
}
|
|
162
253
|
}
|
|
163
254
|
|
|
164
255
|
/**
|
|
@@ -166,12 +257,63 @@ export function expose<T extends object>(
|
|
|
166
257
|
*
|
|
167
258
|
* @param messenger - The Messenger to communicate with (e.g. Worker or Window)
|
|
168
259
|
* @param options - Optional abort signal
|
|
169
|
-
* @returns A proxy object
|
|
260
|
+
* @returns A proxy object for calling remote methods
|
|
261
|
+
*
|
|
262
|
+
* @example
|
|
263
|
+
* ```ts
|
|
264
|
+
* // Create RPC proxy (synchronous)
|
|
265
|
+
* const worker = rpc<WorkerMethods>(new Worker('worker.js'))
|
|
266
|
+
*
|
|
267
|
+
* // Call methods that return handle() get sub-proxies
|
|
268
|
+
* const renderer = await worker.init(transfer(canvas))
|
|
269
|
+
* await renderer.render()
|
|
270
|
+
*
|
|
271
|
+
* // Access underlying messenger
|
|
272
|
+
* worker[$MESSENGER].terminate()
|
|
273
|
+
* ```
|
|
170
274
|
*/
|
|
275
|
+
// Overloads for specific messenger types to enable proper type inference
|
|
171
276
|
export function rpc<T extends object>(
|
|
172
|
-
messenger:
|
|
277
|
+
messenger: Worker,
|
|
278
|
+
options?: { signal?: AbortSignal },
|
|
279
|
+
): RPC<T> & { [$MESSENGER]: Worker }
|
|
280
|
+
|
|
281
|
+
export function rpc<T extends object>(
|
|
282
|
+
messenger: MessagePort,
|
|
283
|
+
options?: { signal?: AbortSignal },
|
|
284
|
+
): RPC<T> & { [$MESSENGER]: MessagePort }
|
|
285
|
+
|
|
286
|
+
export function rpc<T extends object>(
|
|
287
|
+
messenger: Window,
|
|
173
288
|
options?: { signal?: AbortSignal },
|
|
174
|
-
): RPC<T> {
|
|
289
|
+
): RPC<T> & { [$MESSENGER]: Window }
|
|
290
|
+
|
|
291
|
+
export function rpc<T extends object>(
|
|
292
|
+
messenger: BroadcastChannel,
|
|
293
|
+
options?: { signal?: AbortSignal },
|
|
294
|
+
): RPC<T> & { [$MESSENGER]: BroadcastChannel }
|
|
295
|
+
|
|
296
|
+
export function rpc<T extends object>(
|
|
297
|
+
messenger: ServiceWorker,
|
|
298
|
+
options?: { signal?: AbortSignal },
|
|
299
|
+
): RPC<T> & { [$MESSENGER]: ServiceWorker }
|
|
300
|
+
|
|
301
|
+
export function rpc<T extends object, M extends Messenger = Messenger>(
|
|
302
|
+
messenger: M,
|
|
303
|
+
options?: { signal?: AbortSignal },
|
|
304
|
+
): RPC<T> & { [$MESSENGER]: M }
|
|
305
|
+
|
|
306
|
+
// Implementation
|
|
307
|
+
export function rpc<T extends object, M extends Messenger>(
|
|
308
|
+
messenger: M,
|
|
309
|
+
options?: { signal?: AbortSignal },
|
|
310
|
+
): RPC<T> & { [$MESSENGER]: M } {
|
|
175
311
|
const request = createRequester(messenger, options)
|
|
176
|
-
|
|
312
|
+
|
|
313
|
+
const proxy = createRpcCommander<T>((topics, args) => {
|
|
314
|
+
const { args: processedArgs, transferables } = extractTransferables(args)
|
|
315
|
+
return request(RPCPayloadShape.create(topics, processedArgs), transferables)
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
return Object.assign(proxy, { [$MESSENGER]: messenger })
|
|
177
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/encoding.ts
CHANGED
|
@@ -351,10 +351,10 @@ export function createStreamCodec(config: Array<Codec>, fallback: PrimitiveCodec
|
|
|
351
351
|
}
|
|
352
352
|
|
|
353
353
|
// helper to concatenate Uint8Arrays
|
|
354
|
-
function concat(...arrays: Array<Uint8Array>): Uint8Array {
|
|
354
|
+
function concat(...arrays: Array<Uint8Array>): Uint8Array<ArrayBuffer> {
|
|
355
355
|
const result = new Uint8Array(new ArrayBuffer(arrays.reduce((a, b) => a + b.length, 0)))
|
|
356
356
|
let index = 0
|
|
357
|
-
return arrays.reduce<Uint8Array
|
|
357
|
+
return arrays.reduce<Uint8Array<ArrayBuffer>>((result, current) => {
|
|
358
358
|
result.set(current, index)
|
|
359
359
|
index += current.length
|
|
360
360
|
return result
|
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<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(_, topic) {
|
|
70
|
-
if (typeof topic === 'symbol') return undefined
|
|
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> {
|