From b9d84101769184defa64fed596ded74918e42a35 Mon Sep 17 00:00:00 2001 From: Jack Works Date: Wed, 26 Apr 2023 22:51:41 +0800 Subject: [PATCH] feat: allow channel to be async --- .changeset/light-ladybugs-love.md | 5 ++ __tests__/async-init.ts | 2 +- __tests__/bad-data.ts | 2 +- __tests__/basic.ts | 6 +- __tests__/non-strict.ts | 4 +- __tests__/options.ts | 2 +- api/base.api.md | 2 +- api/full.api.md | 2 +- ...async-call-rpc.asynccalloptions.channel.md | 2 +- docs/async-call-rpc.asynccalloptions.md | 2 +- src/Async-Call.ts | 61 +++++++++++-------- src/types.ts | 2 +- 12 files changed, 55 insertions(+), 37 deletions(-) create mode 100644 .changeset/light-ladybugs-love.md diff --git a/.changeset/light-ladybugs-love.md b/.changeset/light-ladybugs-love.md new file mode 100644 index 0000000..fa28929 --- /dev/null +++ b/.changeset/light-ladybugs-love.md @@ -0,0 +1,5 @@ +--- +'async-call-rpc': minor +--- + +allow channel to be async diff --git a/__tests__/async-init.ts b/__tests__/async-init.ts index ef689ee..e706c8a 100644 --- a/__tests__/async-init.ts +++ b/__tests__/async-init.ts @@ -13,7 +13,7 @@ it( 'launches with rejected implementation', withSnapshotDefault('async-call-impl-rejected', async (f) => { const server = f({ impl: Promise.reject(new TypeError('Import failed')) }) - await expect((server as any).add(1, 2)).rejects.toThrowErrorMatchingInlineSnapshot(`"Import failed"`) + await expect((server as any).add(1, 2)).rejects.toThrowErrorMatchingInlineSnapshot('"Import failed"') }), ) diff --git a/__tests__/bad-data.ts b/__tests__/bad-data.ts index 231430e..15bfcb8 100644 --- a/__tests__/bad-data.ts +++ b/__tests__/bad-data.ts @@ -96,7 +96,7 @@ it( const server = _() raw.client.send(Request('a', 'rpc.async-iterator.next', ['b', undefined])) const iter = (server as any).not_found() - expect(iter.next()).rejects.toThrowErrorMatchingInlineSnapshot(`"not_found is not a function"`) + expect(iter.next()).rejects.toThrowErrorMatchingInlineSnapshot('"not_found is not a function"') await delay(100) }), ) diff --git a/__tests__/basic.ts b/__tests__/basic.ts index b7c04ef..43157f3 100644 --- a/__tests__/basic.ts +++ b/__tests__/basic.ts @@ -11,10 +11,10 @@ it( await Promise.all([ expect(server.add(1, 2)).resolves.toMatchInlineSnapshot(`3`), expect(server.echo('string')).resolves.toMatchInlineSnapshot(`"string"`), - expect(server.throws()).rejects.toThrowErrorMatchingInlineSnapshot(`"impl error"`), - expect(server.throwEcho('1')).rejects.toThrowErrorMatchingInlineSnapshot(`"1"`), + expect(server.throws()).rejects.toThrowErrorMatchingInlineSnapshot('"impl error"'), + expect(server.throwEcho('1')).rejects.toThrowErrorMatchingInlineSnapshot('"1"'), // Unknown methods - expect((server as any).not_found()).rejects.toThrowErrorMatchingInlineSnapshot(`"Method not found"`), + expect((server as any).not_found()).rejects.toThrowErrorMatchingInlineSnapshot('"Method not found"'), // Keep this reference expect(server.withThisRef()).resolves.toMatchInlineSnapshot(`3`), ]) diff --git a/__tests__/non-strict.ts b/__tests__/non-strict.ts index 455b807..085deb1 100644 --- a/__tests__/non-strict.ts +++ b/__tests__/non-strict.ts @@ -24,7 +24,7 @@ it( withSnapshotDefault('async-call-strict', async (f, _, _log, raw) => { const server: any = f() raw.client.send('unknown message') - await expect(server.not_defined_method()).rejects.toThrowErrorMatchingInlineSnapshot(`"Method not found"`) + await expect(server.not_defined_method()).rejects.toThrowErrorMatchingInlineSnapshot('"Method not found"') }), ) @@ -33,6 +33,6 @@ it( withSnapshotDefault('async-call-strict-partial', async (f, _, _log, raw) => { const server: any = f({ opts: { strict: { methodNotFound: true } } }) raw.client.send('unknown message') - await expect(server.not_defined_method()).rejects.toThrowErrorMatchingInlineSnapshot(`"Method not found"`) + await expect(server.not_defined_method()).rejects.toThrowErrorMatchingInlineSnapshot('"Method not found"') }), ) diff --git a/__tests__/options.ts b/__tests__/options.ts index 0bd528b..3f42b49 100644 --- a/__tests__/options.ts +++ b/__tests__/options.ts @@ -95,7 +95,7 @@ it( const server = f({ opts: { mapError: () => ({ code: -233, message: 'Oh my message', data: { custom_data: true } }) }, }) - await expect(server.throws()).rejects.toThrowErrorMatchingInlineSnapshot(`"Oh my message"`) + await expect(server.throws()).rejects.toThrowErrorMatchingInlineSnapshot('"Oh my message"') }), ) diff --git a/api/base.api.md b/api/base.api.md index d92ab3a..611dafb 100644 --- a/api/base.api.md +++ b/api/base.api.md @@ -19,7 +19,7 @@ export interface AsyncCallLogLevel { // @public export interface AsyncCallOptions { - channel: CallbackBasedChannel | EventBasedChannel; + channel: CallbackBasedChannel | EventBasedChannel | Promise; idGenerator?(): string | number; key?: string; log?: AsyncCallLogLevel | boolean | 'all'; diff --git a/api/full.api.md b/api/full.api.md index a99f3d0..84e2101 100644 --- a/api/full.api.md +++ b/api/full.api.md @@ -19,7 +19,7 @@ export interface AsyncCallLogLevel { // @public export interface AsyncCallOptions { - channel: CallbackBasedChannel | EventBasedChannel; + channel: CallbackBasedChannel | EventBasedChannel | Promise; idGenerator?(): string | number; key?: string; log?: AsyncCallLogLevel | boolean | 'all'; diff --git a/docs/async-call-rpc.asynccalloptions.channel.md b/docs/async-call-rpc.asynccalloptions.channel.md index 6083741..c8c0f04 100644 --- a/docs/async-call-rpc.asynccalloptions.channel.md +++ b/docs/async-call-rpc.asynccalloptions.channel.md @@ -9,7 +9,7 @@ The message channel to exchange messages between server and client **Signature:** ```typescript -channel: CallbackBasedChannel | EventBasedChannel; +channel: CallbackBasedChannel | EventBasedChannel | Promise; ``` ## Example diff --git a/docs/async-call-rpc.asynccalloptions.md b/docs/async-call-rpc.asynccalloptions.md index 61f267e..65501ec 100644 --- a/docs/async-call-rpc.asynccalloptions.md +++ b/docs/async-call-rpc.asynccalloptions.md @@ -16,7 +16,7 @@ export interface AsyncCallOptions | Property | Modifiers | Type | Description | | --- | --- | --- | --- | -| [channel](./async-call-rpc.asynccalloptions.channel.md) | | [CallbackBasedChannel](./async-call-rpc.callbackbasedchannel.md) \| [EventBasedChannel](./async-call-rpc.eventbasedchannel.md) | The message channel to exchange messages between server and client | +| [channel](./async-call-rpc.asynccalloptions.channel.md) | | [CallbackBasedChannel](./async-call-rpc.callbackbasedchannel.md) \| [EventBasedChannel](./async-call-rpc.eventbasedchannel.md) \| Promise<[CallbackBasedChannel](./async-call-rpc.callbackbasedchannel.md) \| [EventBasedChannel](./async-call-rpc.eventbasedchannel.md)> | The message channel to exchange messages between server and client | | [key?](./async-call-rpc.asynccalloptions.key.md) | | string | _(Optional)_ This option is used for better log print. | | [log?](./async-call-rpc.asynccalloptions.log.md) | | [AsyncCallLogLevel](./async-call-rpc.asynccallloglevel.md) \| boolean \| 'all' | _(Optional)_ Choose log level. | | [logger?](./async-call-rpc.asynccalloptions.logger.md) | | [ConsoleInterface](./async-call-rpc.consoleinterface.md) | _(Optional)_ Provide the logger of AsyncCall | diff --git a/src/Async-Call.ts b/src/Async-Call.ts index 0945573..9bbcc31 100644 --- a/src/Async-Call.ts +++ b/src/Async-Call.ts @@ -60,8 +60,11 @@ export function AsyncCall( options: AsyncCallOptions, ): AsyncVersionOf { let isThisSideImplementationPending = true - let resolvedThisSideImplementationValue: unknown = undefined - let rejectedThisSideImplementation: unknown = undefined + let resolvedThisSideImplementationValue: unknown + let rejectedThisSideImplementation: unknown + + let resolvedChannel: EventBasedChannel | CallbackBasedChannel | undefined + let channelPromise: Promise | undefined // This promise should never fail const awaitThisSideImplementation = async () => { try { @@ -73,6 +76,29 @@ export function AsyncCall( isThisSideImplementationPending = false } } + const onChannelResolved = (channel: EventBasedChannel | CallbackBasedChannel) => { + resolvedChannel = channel + if (isCallbackBasedChannel(channel)) { + channel.setup( + (data) => rawMessageReceiver(data).then(rawMessageSender), + (data) => { + const _ = deserialization(data) + if (isJSONRPCObject(_)) return true + return Promise_resolve(_).then(isJSONRPCObject) + }, + ) + } + if (isEventBasedChannel(channel)) { + const m = channel + m.on && + m.on((_) => + rawMessageReceiver(_) + .then(rawMessageSender) + .then((x) => x && m.send!(x)), + ) + } + return channel + } const { serializer, @@ -243,28 +269,10 @@ export function AsyncCall( } const serialization = serializer ? (x: unknown) => serializer.serialization(x) : Object const deserialization = serializer ? (x: unknown) => serializer.deserialization(x) : Object - const isEventBasedChannel = (x: typeof channel): x is EventBasedChannel => 'send' in x && isFunction(x.send) - const isCallbackBasedChannel = (x: typeof channel): x is CallbackBasedChannel => 'setup' in x && isFunction(x.setup) - if (isCallbackBasedChannel(channel)) { - channel.setup( - (data) => rawMessageReceiver(data).then(rawMessageSender), - (data) => { - const _ = deserialization(data) - if (isJSONRPCObject(_)) return true - return Promise_resolve(_).then(isJSONRPCObject) - }, - ) - } - if (isEventBasedChannel(channel)) { - const m = channel as EventBasedChannel | CallbackBasedChannel - m.on && - m.on((_) => - rawMessageReceiver(_) - .then(rawMessageSender) - .then((x) => x && m.send!(x)), - ) - } + if (channel instanceof Promise) channelPromise = channel.then(onChannelResolved) + else onChannelResolved(channel) + const makeErrorObject = (e: any, frameworkStack: string, data: Request) => { if (isObject(e) && 'stack' in e) e.stack = frameworkStack @@ -277,7 +285,7 @@ export function AsyncCall( const sendPayload = async (payload: unknown, removeQueueR = false) => { if (removeQueueR) payload = [...(payload as BatchQueue)] const data = await serialization(payload) - return channel.send!(data) + return (resolvedChannel || (await channelPromise))!.send!(data) } const rejectsQueue = (queue: BatchQueue, error: unknown) => { for (const x of queue) { @@ -379,3 +387,8 @@ export function AsyncCall( } // Assume a console object in global if there is no custom logger provided declare const console: ConsoleInterface + +const isEventBasedChannel = (x: EventBasedChannel | CallbackBasedChannel): x is EventBasedChannel => + 'send' in x && isFunction(x.send) +const isCallbackBasedChannel = (x: EventBasedChannel | CallbackBasedChannel): x is CallbackBasedChannel => + 'setup' in x && isFunction(x.setup) diff --git a/src/types.ts b/src/types.ts index 8cafaf3..7f03ba2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -146,7 +146,7 @@ export interface AsyncCallOptions { * @example * {@link https://github.com/Jack-Works/async-call-rpc/blob/main/utils-src/web/websocket.client.ts | Example for CallbackBasedChannel} or {@link https://github.com/Jack-Works/async-call-rpc/blob/main/utils-src/node/websocket.server.ts | Example for EventBasedChannel} */ - channel: CallbackBasedChannel | EventBasedChannel + channel: CallbackBasedChannel | EventBasedChannel | Promise /** * Choose log level. * @remarks