Skip to content

Commit

Permalink
Client now supports optional "OverrideModule" type parameter to suppo…
Browse files Browse the repository at this point in the history
…rt partial modules

This is useful when part of a module is available client-side and a call can be made synchronously.
Now, type definitions can optionally reflect when that module is available and avoid wrapping the return result
in a Promise. The default remains the same, and the Module type parameter goes through transformations to reflect
the transport of function data from a remote location (wrapping it in a Promise and adding support for HTML forms).

Currently, the client needs this type parameter passed explicitly (passing partial module to `.module` option alone
isn't enough but I'm unsure how to work around that).
  • Loading branch information
doseofted committed Mar 23, 2024
1 parent cfc6eb0 commit 558a5ee
Show file tree
Hide file tree
Showing 3 changed files with 65 additions and 28 deletions.
26 changes: 15 additions & 11 deletions libs/rpc/src/client/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import { expect, test } from "vitest"
import { createPrimClient } from "./index"

type ExampleModule = { hello: (name?: string) => string; goodbye: () => string; anError: () => void }
type ExampleModule = { hello: (name?: string) => Promise<string>; goodbye: () => string; anError: () => void }
const exampleModule = {
hello(name?: string) {
return ["Hello", name].filter(given => given).join(" ")
},
anError() {
throw "Uh-oh"
},
}

test("static import", async () => {
const client = createPrimClient<() => Promise<ExampleModule>>({
module: {
hello(name: string) {
return ["Hello", name].filter(given => given).join(" ")
},
anError() {
throw "Uh-oh"
},
},
const client = createPrimClient<Promise<ExampleModule>, typeof exampleModule>({
module: exampleModule,
})
// not async since module was provided locally
expect(client.hello()).toBe("Hello")
Expand All @@ -23,12 +24,15 @@ test("static import", async () => {

test("dynamic import", async () => {
const client = createPrimClient<ExampleModule>({
module: new Promise<Partial<ExampleModule>>(r =>
module: new Promise<typeof exampleModule>(r =>
r({
// goodbye() { return "Bye!" },
hello(name: string) {
return ["Hello", name].filter(given => given).join(" ")
},
anError() {
throw "Uh-oh"
},
})
),
})
Expand Down
15 changes: 8 additions & 7 deletions libs/rpc/src/client/index.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import type { JsonHandler, PrimOptions, PromisifiedModule } from "../interfaces"
import type { PrimOptions, PromisifiedModule } from "../interfaces"
import { createPrimOptions } from "../options"
import { isDefined } from "emery"
import { createMethodCatcher } from "./proxy"
import { handlePotentialPromise } from "./wrapper"
import { getUnfulfilledModule, handleLocalModule } from "./local"

export function createPrimClient<
ModuleType extends PrimOptions["module"] = object,
JsonHandlerType extends PrimOptions["jsonHandler"] = JsonHandler,
>(options?: PrimOptions<ModuleType, JsonHandlerType>) {
options = createPrimOptions<PrimOptions<ModuleType, JsonHandlerType>>(options)
ModuleType extends GivenOptions["module"],
OverrideModule extends GivenOptions["module"] = never,
GivenOptions extends PrimOptions = PrimOptions,
>(options?: GivenOptions) {
options = createPrimOptions<GivenOptions>(options)
const providedModule = getUnfulfilledModule(options.module)
const providedMethodPlugin = isDefined(options.methodPlugin)
const providedCallbackPlugin = isDefined(options.callbackPlugin)
// the returned client will catch all method calls given on it
return createMethodCatcher<PromisifiedModule<ModuleType>>({
// the returned client will catch all method calls given on it recursively
return createMethodCatcher<PromisifiedModule<ModuleType, OverrideModule>>({
onMethod(rpc, next) {
// if module method was provided (and is not dynamic import), intercept call and return synchronously
if (providedModule instanceof Promise) return next
Expand Down
52 changes: 42 additions & 10 deletions libs/rpc/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,21 +125,47 @@ type FunctionAndForm<Args extends any[], Result> = {
(formLike: SubmitEvent | FormData | HTMLFormElement): Result
}

// type PromisifiedModuleDirectWithoutOverride<
// ModuleGiven extends object,
// Recursive extends true | false = true,
// Keys extends keyof ModuleGiven = Extract<keyof ModuleGiven, string>,
// > = ConditionalExcept<
// {
// [Key in Keys]: ModuleGiven[Key] extends ((...args: infer A) => infer R) & object
// ? FunctionAndForm<A, Promise<Awaited<R>>> & PromisifiedModuleDirect<ModuleGiven[Key], false>
// : ModuleGiven[Key] extends object
// ? Recursive extends true
// ? PromisifiedModuleDirect<ModuleGiven[Key], true>
// : never
// : never
// },
// never
// >

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type AnyFunction = (...args: any[]) => any
// NOTE: consider condition of checking `.rpc` property on function (but also remember that it may be in allow list)
type PromisifiedModuleDirect<
ModuleGiven extends object,
Recursive extends true | false = true,
ModuleOverride extends object = never,
Keys extends keyof ModuleGiven = Extract<keyof ModuleGiven, string>,
> = ConditionalExcept<
{
[Key in Keys]: ModuleGiven[Key] extends ((...args: infer A) => infer R) & object
? FunctionAndForm<A, Promise<Awaited<R>>> & PromisifiedModuleDirect<ModuleGiven[Key], false>
[Key in Keys]: ModuleGiven[Key] extends (...args: infer A) => infer R
? Key extends keyof ModuleOverride
? ModuleOverride[Key] extends (...args: infer A2) => infer R2
? FunctionAndForm<A2, R2> & PromisifiedModuleDirect<ModuleGiven[Key], false, ModuleOverride[Key]>
: FunctionAndForm<A, Promise<Awaited<R>>> & PromisifiedModuleDirect<ModuleGiven[Key], false>
: FunctionAndForm<A, Promise<Awaited<R>>> & PromisifiedModuleDirect<ModuleGiven[Key], false>
: ModuleGiven[Key] extends object
? Recursive extends true
? PromisifiedModuleDirect<ModuleGiven[Key], true>
: never
? Key extends keyof ModuleOverride
? ModuleOverride[Key] extends object
? PromisifiedModuleDirect<ModuleGiven[Key], true, ModuleOverride[Key]>
: PromisifiedModuleDirect<ModuleGiven[Key], true>
: PromisifiedModuleDirect<ModuleGiven[Key], true>
: PromisifiedModuleDirect<ModuleGiven[Key], false>
: never
},
never
Expand Down Expand Up @@ -179,24 +205,30 @@ type PromisifiedModuleDirect<
// void promTest.abc("what")

// NOTE: this is a non-recursive version of default `Awaited` type that comes with TypeScript
type PromisifiedModuleDynamicImport<ModuleGiven extends object> = ModuleGiven extends object & {
type PromisifiedModuleDynamicImport<
ModuleGiven extends object,
ModuleOverride extends object = never,
> = ModuleGiven extends object & {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
then: (onfulfilled: infer F, ...args: infer _) => any
}
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
F extends (value: infer V, ...args: infer _) => any
? V extends object
? PromisifiedModuleDirect<V>
? PromisifiedModuleDirect<V, true, ModuleOverride>
: never
: never
: PromisifiedModuleDirect<ModuleGiven>
: PromisifiedModuleDirect<ModuleGiven, true, ModuleOverride>

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type AnyFunctionReturnsPromise = (...args: any[]) => PromiseLike<any>
// If given a function that returns a promise, get the returned promise (pattern used often with dynamic imports)
export type PromisifiedModule<Given extends object> = Given extends AnyFunctionReturnsPromise
? PromisifiedModuleDynamicImport<ReturnType<Given>>
: PromisifiedModuleDynamicImport<Given>
export type PromisifiedModule<
Given extends object,
ModuleOverride extends object = never,
> = Given extends AnyFunctionReturnsPromise
? PromisifiedModuleDynamicImport<ReturnType<Given>, ModuleOverride>
: PromisifiedModuleDynamicImport<Given, ModuleOverride>

export type RemoveFunctionWrapper<Given extends object> = Given extends AnyFunctionReturnsPromise
? ReturnType<Given>
Expand Down

0 comments on commit 558a5ee

Please sign in to comment.