-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
/
tinyrpc.ts
107 lines (96 loc) · 3 KB
/
tinyrpc.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
/**
* @see https://trpc.io/blog/tinyrpc-client
*/
import type {
AnyMutationProcedure,
AnyProcedure,
AnyQueryProcedure,
AnyRouter,
inferProcedureInput,
inferProcedureOutput,
ProcedureRouterRecord,
} from '@trpc/server';
import type { TRPCResponse } from '@trpc/server/rpc';
interface ProxyCallbackOptions {
path: string[];
args: unknown[];
}
type ProxyCallback = (opts: ProxyCallbackOptions) => unknown;
function createRecursiveProxy(callback: ProxyCallback, path: string[]) {
const proxy: unknown = new Proxy(
() => {
// dummy no-op function since we don't have any
// client-side target we want to remap to
},
{
get(_obj, key) {
if (typeof key !== 'string') return undefined;
// Recursively compose the full path until a function is invoked
return createRecursiveProxy(callback, [...path, key]);
},
apply(_1, _2, args) {
// Call the callback function with the entire path we
// recursively created and forward the arguments
return callback({
path,
args,
});
},
},
);
return proxy;
}
type Resolver<TProcedure extends AnyProcedure> = (
input: inferProcedureInput<TProcedure>,
) => Promise<inferProcedureOutput<TProcedure>>;
type DecorateProcedure<TProcedure extends AnyProcedure> =
TProcedure extends AnyQueryProcedure
? {
query: Resolver<TProcedure>;
}
: TProcedure extends AnyMutationProcedure
? {
mutate: Resolver<TProcedure>;
}
: never;
/**
* @internal
*/
type DecoratedProcedureRecord<TProcedures extends ProcedureRouterRecord> = {
[TKey in keyof TProcedures]: TProcedures[TKey] extends AnyRouter
? DecoratedProcedureRecord<TProcedures[TKey]['_def']['record']>
: TProcedures[TKey] extends AnyProcedure
? DecorateProcedure<TProcedures[TKey]>
: never;
};
export const createTinyRPCClient = <TRouter extends AnyRouter>(
baseUrl: string,
) =>
createRecursiveProxy(async (opts) => {
const path = [...opts.path]; // e.g. ["post", "byId", "query"]
const method = path.pop()! as 'mutate' | 'query';
const dotPath = path.join('.'); // "post.byId" - this is the path procedures have on the backend
let uri = `${baseUrl}/${dotPath}`;
const [input] = opts.args;
const stringifiedInput = input !== undefined && JSON.stringify(input);
let body: string | undefined = undefined;
if (stringifiedInput !== false) {
if (method === 'query') {
uri += `?input=${encodeURIComponent(stringifiedInput)}`;
} else {
body = stringifiedInput;
}
}
const json: TRPCResponse = await fetch(uri, {
method: method === 'query' ? 'GET' : 'POST',
headers: {
'Content-Type': 'application/json',
},
body,
}).then((res) => res.json());
if ('error' in json) {
throw new Error(`Error: ${json.error.message}`);
}
// No error - all good. Return the data.
return json.result.data;
}, []) as DecoratedProcedureRecord<TRouter['_def']['record']>;