@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
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bigmistqke/rpc",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "RPC toolkit for type-safe communication across Workers, iframes, and network boundaries.",
|
|
6
|
+
"module": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
"./messenger": {
|
|
10
|
+
"import": "./dist/messenger.js",
|
|
11
|
+
"types": "./dist/messenger.d.ts"
|
|
12
|
+
},
|
|
13
|
+
"./stream": {
|
|
14
|
+
"import": "./dist/stream/index.js",
|
|
15
|
+
"types": "./dist/stream/index.d.ts"
|
|
16
|
+
},
|
|
17
|
+
"./fetch": {
|
|
18
|
+
"import": "./dist/fetch/index.js",
|
|
19
|
+
"types": "./dist/fetch/index.d.ts"
|
|
20
|
+
},
|
|
21
|
+
"./fetch/node": {
|
|
22
|
+
"import": "./dist/fetch/node.js",
|
|
23
|
+
"types": "./dist/fetch/node.d.ts"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"tsup": "6.6.3",
|
|
29
|
+
"typescript": "^5.7.2",
|
|
30
|
+
"vitest": "0.28.5"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@types/node": "^22.15.30",
|
|
34
|
+
"bumpp": "^10.3.2",
|
|
35
|
+
"valibot": "^1.0.0"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsup",
|
|
39
|
+
"test": "vitest run",
|
|
40
|
+
"bump": "bumpp"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import * as v from 'valibot'
|
|
2
|
+
import { RPC } from '../types'
|
|
3
|
+
import { callMethod, createCommander, createShape } from '../utils'
|
|
4
|
+
|
|
5
|
+
const $FETCH_HEADER = 'RPC_RR_PROXY'
|
|
6
|
+
|
|
7
|
+
export const isFetchRequest = (event: { request: Request }) =>
|
|
8
|
+
event.request.headers.has($FETCH_HEADER)
|
|
9
|
+
|
|
10
|
+
export const Payload = createShape(
|
|
11
|
+
v.object({ args: v.array(v.any()), topics: v.array(v.string()) }),
|
|
12
|
+
(topics: Array<string>, args: Array<any>) => ({ topics, args }),
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
/**********************************************************************************/
|
|
16
|
+
/* */
|
|
17
|
+
/* Expose / Rpc */
|
|
18
|
+
/* */
|
|
19
|
+
/**********************************************************************************/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Exposes a set of methods as an RPC endpoint over the given messenger.
|
|
23
|
+
*
|
|
24
|
+
* @param methods - An object containing functions to expose
|
|
25
|
+
* @param options - Optional target Messenger and abort signal
|
|
26
|
+
*/
|
|
27
|
+
export function expose<T extends object>(methods: T) {
|
|
28
|
+
return async (event: { request: Request }) => {
|
|
29
|
+
try {
|
|
30
|
+
const json = await event.request.json()
|
|
31
|
+
|
|
32
|
+
if (!Payload.validate(json)) {
|
|
33
|
+
throw new Error(`Incorrect shape`)
|
|
34
|
+
}
|
|
35
|
+
const { args, topics } = json
|
|
36
|
+
|
|
37
|
+
const payload = await callMethod(methods, topics, args)
|
|
38
|
+
return new Response(JSON.stringify({ payload }), {
|
|
39
|
+
status: 200,
|
|
40
|
+
headers: { 'Content-Type': 'application/json' },
|
|
41
|
+
})
|
|
42
|
+
} catch (error) {
|
|
43
|
+
return new Response(null, {
|
|
44
|
+
statusText:
|
|
45
|
+
typeof error === 'string'
|
|
46
|
+
? error
|
|
47
|
+
: typeof error === 'object' &&
|
|
48
|
+
error &&
|
|
49
|
+
'message' in error &&
|
|
50
|
+
typeof error.message === 'string'
|
|
51
|
+
? error.message
|
|
52
|
+
: undefined,
|
|
53
|
+
status: 500,
|
|
54
|
+
headers: { 'Content-Type': 'application/json' },
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Creates an RPC proxy for calling remote methods on the given Messenger.
|
|
62
|
+
*
|
|
63
|
+
* @param messenger - The Messenger to communicate with (e.g. Worker or Window)
|
|
64
|
+
* @param options - Optional abort signal
|
|
65
|
+
* @returns A proxy object that lets you call methods remotely
|
|
66
|
+
*/
|
|
67
|
+
export function rpc<T extends object>(base: string): RPC<T> {
|
|
68
|
+
return createCommander<RPC<T>>(async (topics, args) => {
|
|
69
|
+
const result = await fetch(
|
|
70
|
+
new Request(`${base}/${topics.join('/')}`, {
|
|
71
|
+
method: topics.length > 0 ? 'POST' : 'GET',
|
|
72
|
+
headers: { [$FETCH_HEADER]: 'true' },
|
|
73
|
+
body: JSON.stringify(Payload.create(topics, args)),
|
|
74
|
+
}),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
if (result.status !== 200) {
|
|
78
|
+
throw result.statusText
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const { payload } = await result.json()
|
|
82
|
+
return payload
|
|
83
|
+
})
|
|
84
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
|
|
3
|
+
import { IncomingMessage, ServerResponse } from 'http'
|
|
4
|
+
import { Payload } from '.'
|
|
5
|
+
import { callMethod } from '../utils'
|
|
6
|
+
|
|
7
|
+
export function exposeNode<T extends object>(
|
|
8
|
+
methods: T,
|
|
9
|
+
{
|
|
10
|
+
wrap = (res: ServerResponse, result: any) => {
|
|
11
|
+
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
12
|
+
res.end(JSON.stringify(result))
|
|
13
|
+
},
|
|
14
|
+
unwrap = async (req: IncomingMessage): Promise<{ topics: string[]; args: any[] }> => {
|
|
15
|
+
const body = await new Promise<string>((resolve, reject) => {
|
|
16
|
+
let data = ''
|
|
17
|
+
req.on('data', chunk => (data += chunk))
|
|
18
|
+
req.on('end', () => resolve(data))
|
|
19
|
+
req.on('error', reject)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
const json = JSON.parse(body)
|
|
23
|
+
if (!Payload.validate(json)) {
|
|
24
|
+
throw new Error(`Incorrect shape`)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return json
|
|
28
|
+
},
|
|
29
|
+
}: {
|
|
30
|
+
wrap?: (res: ServerResponse, result: any) => void
|
|
31
|
+
unwrap?: (req: IncomingMessage) => Promise<{ topics: string[]; args: any[] }>
|
|
32
|
+
} = {},
|
|
33
|
+
) {
|
|
34
|
+
return async (req: IncomingMessage, res: ServerResponse) => {
|
|
35
|
+
try {
|
|
36
|
+
const { topics, args } = await unwrap(req)
|
|
37
|
+
const result = await callMethod(methods, topics, args)
|
|
38
|
+
wrap(res, result)
|
|
39
|
+
} catch (err: any) {
|
|
40
|
+
res.writeHead(500)
|
|
41
|
+
res.statusMessage = err?.message
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import * as v from 'valibot'
|
|
2
|
+
import { createShape } from './utils'
|
|
3
|
+
|
|
4
|
+
// Schema and protocol for requests
|
|
5
|
+
export const $MESSENGER_REQUEST = 'RPC_PROXY_REQUEST'
|
|
6
|
+
|
|
7
|
+
export const requestSchema = v.object({
|
|
8
|
+
[$MESSENGER_REQUEST]: v.number(),
|
|
9
|
+
payload: v.unknown(),
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
export const RequestShape = createShape(requestSchema, <T>(id: number, payload: T) => ({
|
|
13
|
+
[$MESSENGER_REQUEST]: id,
|
|
14
|
+
payload,
|
|
15
|
+
}))
|
|
16
|
+
|
|
17
|
+
export type RequestData = v.InferOutput<typeof requestSchema>
|
|
18
|
+
|
|
19
|
+
// Schema and protocol for responses
|
|
20
|
+
export const $MESSENGER_RESPONSE = 'RPC_PROXY_RESPONSE'
|
|
21
|
+
|
|
22
|
+
export const ResponseShape = createShape(
|
|
23
|
+
v.object({
|
|
24
|
+
[$MESSENGER_RESPONSE]: v.number(),
|
|
25
|
+
payload: v.unknown(),
|
|
26
|
+
}),
|
|
27
|
+
(request: RequestData, payload: any) => ({
|
|
28
|
+
[$MESSENGER_RESPONSE]: request[$MESSENGER_REQUEST],
|
|
29
|
+
payload,
|
|
30
|
+
}),
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
// Schema and protocol of errors
|
|
34
|
+
export const $MESSENGER_ERROR = 'RPC_PROXY_ERROR'
|
|
35
|
+
|
|
36
|
+
export const ErrorShape = createShape(
|
|
37
|
+
v.object({
|
|
38
|
+
[$MESSENGER_ERROR]: v.number(),
|
|
39
|
+
error: v.unknown(),
|
|
40
|
+
}),
|
|
41
|
+
(data: RequestData, error: any) => ({
|
|
42
|
+
[$MESSENGER_ERROR]: data[$MESSENGER_REQUEST],
|
|
43
|
+
error,
|
|
44
|
+
}),
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
// RPC-specific request payload
|
|
48
|
+
export const $MESSENGER_RPC_REQUEST = 'RPC_PROXY_RPC_REQUEST'
|
|
49
|
+
|
|
50
|
+
export const RPCPayloadShape = createShape(
|
|
51
|
+
v.object({
|
|
52
|
+
[$MESSENGER_RPC_REQUEST]: v.boolean(),
|
|
53
|
+
topics: v.array(v.string()),
|
|
54
|
+
args: v.array(v.any()),
|
|
55
|
+
}),
|
|
56
|
+
(topics: Array<string>, args: Array<any>) => ({ [$MESSENGER_RPC_REQUEST]: true, topics, args }),
|
|
57
|
+
)
|
package/src/messenger.ts
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import {
|
|
2
|
+
$MESSENGER_ERROR,
|
|
3
|
+
$MESSENGER_RESPONSE,
|
|
4
|
+
ErrorShape,
|
|
5
|
+
RequestData,
|
|
6
|
+
RequestShape,
|
|
7
|
+
ResponseShape,
|
|
8
|
+
RPCPayloadShape,
|
|
9
|
+
} from './message-protocol'
|
|
10
|
+
import { RPC } from './types'
|
|
11
|
+
import { callMethod, createCommander, createIdRegistry, defer } from './utils'
|
|
12
|
+
|
|
13
|
+
export const $TRANSFER = 'WORKER-TRANSFER'
|
|
14
|
+
|
|
15
|
+
interface WorkerMessenger {
|
|
16
|
+
postMessage(message: any, transferables?: any[]): void
|
|
17
|
+
addEventListener(key: 'message', callback: (event: MessageEvent) => void): void
|
|
18
|
+
start?(): void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type Messenger = Window | WorkerMessenger
|
|
22
|
+
|
|
23
|
+
/**********************************************************************************/
|
|
24
|
+
/* */
|
|
25
|
+
/* Utils */
|
|
26
|
+
/* */
|
|
27
|
+
/**********************************************************************************/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Checks whether the given target is a `Window` object (WindowProxy).
|
|
31
|
+
*/
|
|
32
|
+
function isWindowProxy(target: any): target is Window {
|
|
33
|
+
return (
|
|
34
|
+
typeof target === 'object' &&
|
|
35
|
+
typeof target?.postMessage === 'function' &&
|
|
36
|
+
typeof target?.closed === 'boolean'
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Returns a `postMessage` function compatible with both Window and Worker contexts.
|
|
42
|
+
*/
|
|
43
|
+
function usePostMessage(messenger: Messenger) {
|
|
44
|
+
if (isWindowProxy(messenger)) {
|
|
45
|
+
return (message: any, transferables?: any[]) =>
|
|
46
|
+
messenger.postMessage(message, '*', transferables)
|
|
47
|
+
} else {
|
|
48
|
+
return (message: any, transferables?: any[]) => messenger.postMessage(message, transferables)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**********************************************************************************/
|
|
53
|
+
/* */
|
|
54
|
+
/* Requester / Responder */
|
|
55
|
+
/* */
|
|
56
|
+
/**********************************************************************************/
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Sets up a requester that sends messages and returns promises resolving when a response is received.
|
|
60
|
+
*
|
|
61
|
+
* @param messenger - The target Messenger to send messages to
|
|
62
|
+
* @param options - Optional abort signal
|
|
63
|
+
* @returns A function to send payloads and await responses
|
|
64
|
+
*/
|
|
65
|
+
function createRequester(messenger: Messenger, options: { signal?: AbortSignal } = {}) {
|
|
66
|
+
const promiseRegistry = createIdRegistry<{
|
|
67
|
+
resolve(value: any): void
|
|
68
|
+
reject(value: unknown): void
|
|
69
|
+
}>()
|
|
70
|
+
const postMessage = usePostMessage(messenger)
|
|
71
|
+
|
|
72
|
+
messenger.addEventListener(
|
|
73
|
+
'message',
|
|
74
|
+
event => {
|
|
75
|
+
const data = (event as MessageEvent<unknown>).data
|
|
76
|
+
if (ErrorShape.validate(data)) {
|
|
77
|
+
promiseRegistry.free(data[$MESSENGER_ERROR])?.reject(data.error)
|
|
78
|
+
} else if (ResponseShape.validate(data)) {
|
|
79
|
+
promiseRegistry.free(data[$MESSENGER_RESPONSE])?.resolve(data.payload)
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
options,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
if ('start' in messenger) {
|
|
86
|
+
messenger.start?.()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return (payload: any, transferables?: any[]) => {
|
|
90
|
+
const { promise, resolve, reject } = defer()
|
|
91
|
+
const id = promiseRegistry.register({ resolve, reject })
|
|
92
|
+
postMessage(RequestShape.create(id, payload), transferables)
|
|
93
|
+
return promise
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Sets up a responder that listens for requests and responds with the result of the callback.
|
|
99
|
+
*
|
|
100
|
+
* @param messenger - The Messenger to receive messages from
|
|
101
|
+
* @param callback - A function called with the validated request event
|
|
102
|
+
* @param options - Optional abort signal
|
|
103
|
+
*/
|
|
104
|
+
export function createResponder(
|
|
105
|
+
messenger: Messenger,
|
|
106
|
+
callback: (data: RequestData) => any,
|
|
107
|
+
options: { signal?: AbortSignal } = {},
|
|
108
|
+
) {
|
|
109
|
+
const postMessage = usePostMessage(messenger)
|
|
110
|
+
|
|
111
|
+
messenger.addEventListener(
|
|
112
|
+
'message',
|
|
113
|
+
event => {
|
|
114
|
+
const data = (event as MessageEvent).data
|
|
115
|
+
if (RequestShape.validate(data)) {
|
|
116
|
+
try {
|
|
117
|
+
postMessage(ResponseShape.create(data, callback(data)))
|
|
118
|
+
} catch (error) {
|
|
119
|
+
postMessage(ErrorShape.create(data, error))
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
options,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
if ('start' in messenger) {
|
|
127
|
+
messenger.start?.()
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**********************************************************************************/
|
|
132
|
+
/* */
|
|
133
|
+
/* Expose / Rpc */
|
|
134
|
+
/* */
|
|
135
|
+
/**********************************************************************************/
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Exposes a set of methods as an RPC endpoint over the given messenger.
|
|
139
|
+
*
|
|
140
|
+
* @param methods - An object containing functions to expose
|
|
141
|
+
* @param options - Optional target Messenger and abort signal
|
|
142
|
+
*/
|
|
143
|
+
export function expose<T extends object>(
|
|
144
|
+
methods: T,
|
|
145
|
+
{ to = self, signal }: { to?: Messenger; signal?: AbortSignal } = {},
|
|
146
|
+
) {
|
|
147
|
+
createResponder(
|
|
148
|
+
to,
|
|
149
|
+
data => {
|
|
150
|
+
if (RPCPayloadShape.validate(data.payload)) {
|
|
151
|
+
try {
|
|
152
|
+
const { topics, args } = data.payload
|
|
153
|
+
return callMethod(methods, topics, args)
|
|
154
|
+
} catch (error) {
|
|
155
|
+
console.error('Error while processing rpc request:', error, data.payload, methods)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
{ signal },
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Creates an RPC proxy for calling remote methods on the given Messenger.
|
|
165
|
+
*
|
|
166
|
+
* @param messenger - The Messenger to communicate with (e.g. Worker or Window)
|
|
167
|
+
* @param options - Optional abort signal
|
|
168
|
+
* @returns A proxy object that lets you call methods remotely
|
|
169
|
+
*/
|
|
170
|
+
export function rpc<T extends object>(
|
|
171
|
+
messenger: Messenger,
|
|
172
|
+
options?: { signal?: AbortSignal },
|
|
173
|
+
): RPC<T> {
|
|
174
|
+
const request = createRequester(messenger, options)
|
|
175
|
+
return createCommander<RPC<T>>((topics, args) => request(RPCPayloadShape.create(topics, args)))
|
|
176
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { $MESSENGER_REQUEST, RequestShape, RPCPayloadShape } from '../message-protocol'
|
|
2
|
+
import * as v from 'valibot'
|
|
3
|
+
import { RPC } from '../types'
|
|
4
|
+
import { callMethod, createCommander, createIdRegistry, createShape, defer } from '../utils'
|
|
5
|
+
|
|
6
|
+
const $SSE_RESPONSE_HEADER = 'RPC_SSE_RESPONSE'
|
|
7
|
+
|
|
8
|
+
export const isSSEResponse = (event: { request: Request }) =>
|
|
9
|
+
event.request.headers.has($SSE_RESPONSE_HEADER)
|
|
10
|
+
|
|
11
|
+
export const Payload = createShape(
|
|
12
|
+
v.object({ args: v.array(v.any()), topics: v.array(v.string()) }),
|
|
13
|
+
(topics: Array<string>, args: Array<any>) => ({ topics, args }),
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
/**********************************************************************************/
|
|
17
|
+
/* */
|
|
18
|
+
/* Expose / Rpc */
|
|
19
|
+
/* */
|
|
20
|
+
/**********************************************************************************/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Exposes a set of methods as an RPC endpoint over the given messenger.
|
|
24
|
+
*
|
|
25
|
+
* @param methods - An object containing functions to expose
|
|
26
|
+
* @param options - Optional target Messenger and abort signal
|
|
27
|
+
*/
|
|
28
|
+
export function expose<T extends object>(url: string, methods: T) {
|
|
29
|
+
const sse = new EventSource(url)
|
|
30
|
+
sse.addEventListener('message', async event => {
|
|
31
|
+
try {
|
|
32
|
+
const data = JSON.parse(event.data)
|
|
33
|
+
|
|
34
|
+
if (RequestShape.validate(data) && RPCPayloadShape.validate(data.payload)) {
|
|
35
|
+
const {
|
|
36
|
+
[$MESSENGER_REQUEST]: id,
|
|
37
|
+
payload: { args, topics },
|
|
38
|
+
} = data
|
|
39
|
+
|
|
40
|
+
fetch(url, {
|
|
41
|
+
method: 'POST',
|
|
42
|
+
headers: {
|
|
43
|
+
[$SSE_RESPONSE_HEADER]: id.toString(),
|
|
44
|
+
},
|
|
45
|
+
body: JSON.stringify({ payload: await callMethod(methods, topics, args) }),
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.error('ERROR', error, event)
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Creates an RPC proxy for calling remote methods on the given Messenger.
|
|
56
|
+
*
|
|
57
|
+
* @param messenger - The Messenger to communicate with (e.g. Worker or Window)
|
|
58
|
+
* @param options - Optional abort signal
|
|
59
|
+
* @returns A proxy object that lets you call methods remotely
|
|
60
|
+
*/
|
|
61
|
+
export function rpc<T extends object>() {
|
|
62
|
+
const promiseRegistry = createIdRegistry<{
|
|
63
|
+
resolve(value: any): void
|
|
64
|
+
reject(value: unknown): void
|
|
65
|
+
}>()
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
create() {
|
|
69
|
+
const encoder = new TextEncoder()
|
|
70
|
+
const closeHandlers = new Set<() => void>()
|
|
71
|
+
let controller: ReadableStreamDefaultController
|
|
72
|
+
let closed = false
|
|
73
|
+
|
|
74
|
+
function send(value: any) {
|
|
75
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(value)}\n\n`))
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const stream = new ReadableStream({
|
|
79
|
+
start(_controller) {
|
|
80
|
+
controller = _controller
|
|
81
|
+
},
|
|
82
|
+
cancel() {
|
|
83
|
+
closeHandlers.forEach(handler => handler())
|
|
84
|
+
closed = true
|
|
85
|
+
},
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
return [
|
|
89
|
+
createCommander<RPC<T>>(async (topics, args) => {
|
|
90
|
+
if (closed) {
|
|
91
|
+
throw new Error(`[rpc/sse] Stream is closed.`)
|
|
92
|
+
}
|
|
93
|
+
const { promise, resolve, reject } = defer()
|
|
94
|
+
const id = promiseRegistry.register({ resolve, reject })
|
|
95
|
+
send(RequestShape.create(id, RPCPayloadShape.create(topics, args)))
|
|
96
|
+
return promise
|
|
97
|
+
}),
|
|
98
|
+
{
|
|
99
|
+
get closed() {
|
|
100
|
+
return closed
|
|
101
|
+
},
|
|
102
|
+
onClose(callback: () => void) {
|
|
103
|
+
closeHandlers.add(callback)
|
|
104
|
+
return () => closeHandlers.delete(callback)
|
|
105
|
+
},
|
|
106
|
+
response: new Response(stream, {
|
|
107
|
+
headers: {
|
|
108
|
+
'Content-Type': 'text/event-stream',
|
|
109
|
+
'Cache-Control': 'no-cache',
|
|
110
|
+
Connection: 'keep-alive',
|
|
111
|
+
},
|
|
112
|
+
}),
|
|
113
|
+
},
|
|
114
|
+
] as const
|
|
115
|
+
},
|
|
116
|
+
handleAnswer: async (event: { request: Request }) => {
|
|
117
|
+
const id = event.request.headers.get($SSE_RESPONSE_HEADER)
|
|
118
|
+
if (id !== null) {
|
|
119
|
+
try {
|
|
120
|
+
const { payload } = await event.request.json()
|
|
121
|
+
promiseRegistry.free(+id)?.resolve(payload)
|
|
122
|
+
} catch (error) {
|
|
123
|
+
console.error('ERROR', error, event)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return new Response()
|
|
127
|
+
},
|
|
128
|
+
}
|
|
129
|
+
}
|