1 | export * from './types'
|
2 | export type { _IgnoreResponse } from './core/notify'
|
3 | export { JSONSerialization, NoSerialization } from './utils/serialization'
|
4 | export { notify } from './core/notify'
|
5 | export { batch } from './core/batch'
|
6 |
|
7 | import { NoSerialization } from './utils/serialization'
|
8 | import {
|
9 | Request,
|
10 | Response,
|
11 | ErrorResponseMapped,
|
12 | SuccessResponse,
|
13 | hasKey,
|
14 | isJSONRPCObject,
|
15 | isObject,
|
16 | ErrorResponse,
|
17 | defaultErrorMapper,
|
18 | ErrorResponseMethodNotFound,
|
19 | ErrorResponseInvalidRequest,
|
20 | ErrorResponseParseError,
|
21 | } from './utils/jsonrpc'
|
22 | import {
|
23 | removeStackHeader,
|
24 | RecoverError,
|
25 | makeHostedMessage,
|
26 | Err_Cannot_call_method_starts_with_rpc_dot_directly,
|
27 | Err_Then_is_accessed_on_local_implementation_Please_explicitly_mark_if_it_is_thenable_in_the_options,
|
28 | } from './utils/error'
|
29 | import { generateRandomID } from './utils/generateRandomID'
|
30 | import { normalizeStrictOptions, normalizeLogOptions } from './utils/normalizeOptions'
|
31 | import { AsyncCallIgnoreResponse, AsyncCallNotify, AsyncCallBatch } from './utils/internalSymbol'
|
32 | import type { BatchQueue } from './core/batch'
|
33 | import type { CallbackBasedChannel, EventBasedChannel, AsyncCallOptions, ConsoleInterface, _AsyncVersionOf } from './types'
|
34 | import {
|
35 | ERROR,
|
36 | isArray,
|
37 | isFunction,
|
38 | isString,
|
39 | Promise_reject,
|
40 | Promise_resolve,
|
41 | replayFunction,
|
42 | undefined,
|
43 | } from './utils/constants'
|
44 |
|
45 |
|
46 |
|
47 |
|
48 |
|
49 |
|
50 |
|
51 |
|
52 |
|
53 |
|
54 |
|
55 |
|
56 |
|
57 |
|
58 |
|
59 |
|
60 |
|
61 |
|
62 |
|
63 | export function AsyncCall<OtherSideImplementedFunctions = {}>(
|
64 | thisSideImplementation: null | undefined | object | Promise<object>,
|
65 | options: AsyncCallOptions,
|
66 | ): _AsyncVersionOf<OtherSideImplementedFunctions> {
|
67 | let isThisSideImplementationPending = true
|
68 | let resolvedThisSideImplementationValue: unknown = undefined
|
69 | let rejectedThisSideImplementation: unknown = undefined
|
70 |
|
71 | const awaitThisSideImplementation = async () => {
|
72 | try {
|
73 | resolvedThisSideImplementationValue = await thisSideImplementation
|
74 | } catch (e) {
|
75 | rejectedThisSideImplementation = e
|
76 | console_error('AsyncCall failed to start', e)
|
77 | } finally {
|
78 | isThisSideImplementationPending = false
|
79 | }
|
80 | }
|
81 |
|
82 | const {
|
83 | serializer = NoSerialization,
|
84 | key: logKey = 'rpc',
|
85 | strict = true,
|
86 | log = true,
|
87 | parameterStructures = 'by-position',
|
88 | preferLocalImplementation = false,
|
89 | idGenerator = generateRandomID,
|
90 | mapError,
|
91 | logger,
|
92 | channel,
|
93 | thenable,
|
94 | } = options
|
95 |
|
96 | if (thisSideImplementation instanceof Promise) awaitThisSideImplementation()
|
97 | else {
|
98 | resolvedThisSideImplementationValue = thisSideImplementation
|
99 | isThisSideImplementationPending = false
|
100 | }
|
101 |
|
102 | const [banMethodNotFound, banUnknownMessage] = normalizeStrictOptions(strict)
|
103 | const [
|
104 | log_beCalled,
|
105 | log_localError,
|
106 | log_remoteError,
|
107 | log_pretty,
|
108 | log_requestReplay,
|
109 | log_sendLocalStack,
|
110 | ] = normalizeLogOptions(log)
|
111 | const {
|
112 | log: console_log,
|
113 | error: console_error = console_log,
|
114 | debug: console_debug = console_log,
|
115 | groupCollapsed: console_groupCollapsed = console_log,
|
116 | groupEnd: console_groupEnd = console_log,
|
117 | warn: console_warn = console_log,
|
118 | } = (logger || console) as ConsoleInterface
|
119 | type PromiseParam = [resolve: (value?: any) => void, reject: (reason?: any) => void]
|
120 | const requestContext = new Map<string | number, { f: PromiseParam; stack: string }>()
|
121 | const onRequest = async (data: Request): Promise<Response | undefined> => {
|
122 | if (isThisSideImplementationPending) await awaitThisSideImplementation()
|
123 | else {
|
124 |
|
125 | if (rejectedThisSideImplementation) return makeErrorObject(rejectedThisSideImplementation, '', data)
|
126 | }
|
127 | let frameworkStack: string = ''
|
128 | try {
|
129 | const { params, method, id: req_id, remoteStack } = data
|
130 |
|
131 | const key = (method.startsWith('rpc.') ? Symbol.for(method) : method) as keyof object
|
132 | const executor: unknown =
|
133 | resolvedThisSideImplementationValue && (resolvedThisSideImplementationValue as any)[key]
|
134 | if (!isFunction(executor)) {
|
135 | if (!banMethodNotFound) {
|
136 | if (log_localError) console_debug('Missing method', key, data)
|
137 | return
|
138 | } else return ErrorResponseMethodNotFound(req_id)
|
139 | }
|
140 | const args = isArray(params) ? params : [params]
|
141 | frameworkStack = removeStackHeader(new Error().stack)
|
142 | const promise = new Promise((resolve) => resolve(executor.apply(resolvedThisSideImplementationValue, args)))
|
143 | if (log_beCalled) {
|
144 | if (log_pretty) {
|
145 | const logArgs: unknown[] = [
|
146 | `${logKey}.%c${method}%c(${args.map(() => '%o').join(', ')}%c)\n%o %c@${req_id}`,
|
147 | 'color: #d2c057',
|
148 | '',
|
149 | ...args,
|
150 | '',
|
151 | promise,
|
152 | 'color: gray; font-style: italic;',
|
153 | ]
|
154 | if (log_requestReplay) {
|
155 |
|
156 |
|
157 | const replay = () => { debugger; return executor.apply(resolvedThisSideImplementationValue, args) }
|
158 | replay.toString = replayFunction
|
159 | logArgs.push(replay)
|
160 | }
|
161 | if (remoteStack) {
|
162 | console_groupCollapsed(...logArgs)
|
163 | console_log(remoteStack)
|
164 | console_groupEnd()
|
165 | } else console_log(...logArgs)
|
166 | } else console_log(`${logKey}.${method}(${[...args].toString()}) @${req_id}`)
|
167 | }
|
168 | const result = await promise
|
169 | if (result === AsyncCallIgnoreResponse) return
|
170 | return SuccessResponse(req_id, await promise)
|
171 | } catch (e) {
|
172 | return makeErrorObject(e, frameworkStack, data)
|
173 | }
|
174 | }
|
175 | const onResponse = async (data: Response): Promise<void> => {
|
176 | let errorMessage = '',
|
177 | remoteErrorStack = '',
|
178 | errorCode = 0,
|
179 | errorType = ERROR
|
180 | if (hasKey(data, 'error')) {
|
181 | const e = data.error
|
182 | errorMessage = e.message
|
183 | errorCode = e.code
|
184 | const detail = e.data
|
185 |
|
186 | if (isObject(detail) && hasKey(detail, 'stack') && isString(detail.stack)) remoteErrorStack = detail.stack
|
187 | else remoteErrorStack = '<remote stack not available>'
|
188 |
|
189 | if (isObject(detail) && hasKey(detail, 'type') && isString(detail.type)) errorType = detail.type
|
190 | else errorType = ERROR
|
191 |
|
192 | if (log_remoteError)
|
193 | log_pretty
|
194 | ? console_error(
|
195 | `${errorType}: ${errorMessage}(${errorCode}) %c@${data.id}\n%c${remoteErrorStack}`,
|
196 | 'color: gray',
|
197 | '',
|
198 | )
|
199 | : console_error(`${errorType}: ${errorMessage}(${errorCode}) @${data.id}\n${remoteErrorStack}`)
|
200 | }
|
201 | if (data.id === null || data.id === undefined) return
|
202 | const { f: [resolve, reject] = [null, null], stack: localErrorStack = '' } = requestContext.get(data.id) || {}
|
203 | if (!resolve || !reject) return
|
204 | requestContext.delete(data.id)
|
205 | if (hasKey(data, 'error')) {
|
206 | reject(
|
207 | RecoverError(
|
208 | errorType,
|
209 | errorMessage,
|
210 | errorCode,
|
211 |
|
212 | remoteErrorStack + '\n \u0430t AsyncCall (rpc) \n' + localErrorStack,
|
213 | ),
|
214 | )
|
215 | } else {
|
216 | resolve(data.result)
|
217 | }
|
218 | return
|
219 | }
|
220 | const rawMessageReceiver = async (_: unknown): Promise<undefined | Response | (Response | undefined)[]> => {
|
221 | let data: unknown
|
222 | let result: Response | undefined = undefined
|
223 | try {
|
224 | data = await deserialization(_)
|
225 | if (isJSONRPCObject(data)) {
|
226 | return (result = await handleSingleMessage(data))
|
227 | } else if (isArray(data) && data.every(isJSONRPCObject) && data.length !== 0) {
|
228 | return Promise.all(data.map(handleSingleMessage))
|
229 | } else {
|
230 | if (banUnknownMessage) {
|
231 | let id = (data as any).id
|
232 | if (id === undefined) id = null
|
233 | return ErrorResponseInvalidRequest(id)
|
234 | } else {
|
235 |
|
236 | return undefined
|
237 | }
|
238 | }
|
239 | } catch (e) {
|
240 | if (log_localError) console_error(e, data, result)
|
241 | return ErrorResponseParseError(e, mapError || defaultErrorMapper(e && e.stack))
|
242 | }
|
243 | }
|
244 | const rawMessageSender = async (res: undefined | Response | (Response | undefined)[]) => {
|
245 | if (!res) return
|
246 | if (isArray(res)) {
|
247 | const reply = res.filter((x) => x && hasKey(x, 'id'))
|
248 | if (reply.length === 0) return
|
249 | return serialization(reply)
|
250 | } else {
|
251 | return serialization(res)
|
252 | }
|
253 | }
|
254 | const serialization = (x: unknown) => serializer.serialization(x)
|
255 | const deserialization = (x: unknown) => serializer.deserialization(x)
|
256 | const isEventBasedChannel = (x: typeof channel): x is EventBasedChannel => hasKey(x, 'send') && isFunction(x.send)
|
257 | const isCallbackBasedChannel = (x: typeof channel): x is CallbackBasedChannel =>
|
258 | hasKey(x, 'setup') && isFunction(x.setup)
|
259 |
|
260 | if (isCallbackBasedChannel(channel)) {
|
261 | channel.setup(
|
262 | (data) => rawMessageReceiver(data).then(rawMessageSender),
|
263 | (data) => {
|
264 | const _ = deserialization(data)
|
265 | if (isJSONRPCObject(_)) return true
|
266 | return Promise_resolve(_).then(isJSONRPCObject)
|
267 | },
|
268 | )
|
269 | }
|
270 | if (isEventBasedChannel(channel)) {
|
271 | const m = channel as EventBasedChannel | CallbackBasedChannel
|
272 | m.on &&
|
273 | m.on((_) =>
|
274 | rawMessageReceiver(_)
|
275 | .then(rawMessageSender)
|
276 | .then((x) => x && m.send!(x)),
|
277 | )
|
278 | }
|
279 | function makeErrorObject(e: any, frameworkStack: string, data: Request) {
|
280 | if (isObject(e) && hasKey(e, 'stack'))
|
281 | e.stack = frameworkStack
|
282 | .split('\n')
|
283 | .reduce((stack, fstack) => stack.replace(fstack + '\n', ''), '' + e.stack)
|
284 | if (log_localError) console_error(e)
|
285 | return ErrorResponseMapped(data, e, mapError || defaultErrorMapper(log_sendLocalStack ? e.stack : undefined))
|
286 | }
|
287 |
|
288 | async function sendPayload(payload: unknown, removeQueueR = false) {
|
289 | if (removeQueueR) payload = [...(payload as BatchQueue)]
|
290 | const data = await serialization(payload)
|
291 | return channel.send!(data)
|
292 | }
|
293 | function rejectsQueue(queue: BatchQueue, error: unknown) {
|
294 | for (const x of queue) {
|
295 | if (hasKey(x, 'id')) {
|
296 | const ctx = requestContext.get(x.id!)
|
297 | ctx && ctx.f[1](error)
|
298 | }
|
299 | }
|
300 | }
|
301 | const handleSingleMessage = async (
|
302 | data: SuccessResponse | ErrorResponse | Request,
|
303 | ): Promise<SuccessResponse | ErrorResponse | undefined> => {
|
304 | if (hasKey(data, 'method')) {
|
305 | const r = onRequest(data)
|
306 | if (hasKey(data, 'id')) return r
|
307 | try {
|
308 | await r
|
309 | } catch {}
|
310 | return undefined
|
311 | }
|
312 | return onResponse(data) as Promise<undefined>
|
313 | }
|
314 | return new Proxy({ __proto__: null } as any, {
|
315 | get(cache, method: string | symbol) {
|
316 | if (method === 'then') {
|
317 | if (thenable === undefined) {
|
318 | console_warn(
|
319 | makeHostedMessage(
|
320 | Err_Then_is_accessed_on_local_implementation_Please_explicitly_mark_if_it_is_thenable_in_the_options,
|
321 | new TypeError('RPC used as Promise: '),
|
322 | ),
|
323 | )
|
324 | }
|
325 | if (thenable !== true) return undefined
|
326 | }
|
327 | if (isString(method) && cache[method]) return cache[method]
|
328 | const factory = (notify: boolean) => (...params: unknown[]) => {
|
329 | let stack = removeStackHeader(new Error().stack)
|
330 | let queue: BatchQueue | undefined = undefined
|
331 | if (method === AsyncCallBatch) {
|
332 | queue = params.shift() as any
|
333 | method = params.shift() as any
|
334 | }
|
335 | if (typeof method === 'symbol') {
|
336 | const RPCInternalMethod = Symbol.keyFor(method) || (method as any).description
|
337 | if (RPCInternalMethod) {
|
338 | if (RPCInternalMethod.startsWith('rpc.')) method = RPCInternalMethod
|
339 | else return Promise_reject(new TypeError('Not start with rpc.'))
|
340 | }
|
341 | } else if (method.startsWith('rpc.'))
|
342 | return Promise_reject(
|
343 | makeHostedMessage(Err_Cannot_call_method_starts_with_rpc_dot_directly, new TypeError()),
|
344 | )
|
345 | return new Promise<void>((resolve, reject) => {
|
346 | if (preferLocalImplementation && !isThisSideImplementationPending && isString(method)) {
|
347 | const localImpl: unknown =
|
348 | resolvedThisSideImplementationValue && (resolvedThisSideImplementationValue as any)[method]
|
349 | if (isFunction(localImpl)) return resolve(localImpl(...params))
|
350 | }
|
351 | const id = idGenerator()
|
352 | const [param0] = params
|
353 | const sendingStack = log_sendLocalStack ? stack : ''
|
354 | const param =
|
355 | parameterStructures === 'by-name' && params.length === 1 && isObject(param0) ? param0 : params
|
356 | const request = Request(notify ? undefined : id, method as string, param, sendingStack)
|
357 | if (queue) {
|
358 | queue.push(request)
|
359 | if (!queue.r) queue.r = [() => sendPayload(queue, true), (e) => rejectsQueue(queue!, e)]
|
360 | } else sendPayload(request).catch(reject)
|
361 | if (notify) return resolve()
|
362 | requestContext.set(id, {
|
363 | f: [resolve, reject],
|
364 | stack,
|
365 | })
|
366 | })
|
367 | }
|
368 | const f = factory(false)
|
369 | // @ts-ignore
|
370 | f[AsyncCallNotify] = factory(true)
|
371 | // @ts-ignore
|
372 | f[AsyncCallNotify][AsyncCallNotify] = f[AsyncCallNotify]
|
373 | isString(method) && Object.defineProperty(cache, method, { value: f, configurable: true })
|
374 | return f
|
375 | },
|
376 | }) as _AsyncVersionOf<OtherSideImplementedFunctions>
|
377 | }
|
378 | // Assume a console object in global if there is no custom logger provided
|
379 | declare const console: ConsoleInterface
|
380 |
|
\ | No newline at end of file |