@travetto/web-rpc 8.0.0-alpha.14 → 8.0.0-alpha.16
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/package.json +4 -4
- package/src/controller.ts +18 -3
- package/support/client/rpc.ts +69 -35
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/web-rpc",
|
|
3
|
-
"version": "8.0.0-alpha.
|
|
3
|
+
"version": "8.0.0-alpha.16",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "RPC support for a Web Application",
|
|
6
6
|
"keywords": [
|
|
@@ -27,9 +27,9 @@
|
|
|
27
27
|
"directory": "module/web-rpc"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@travetto/config": "^8.0.0-alpha.
|
|
31
|
-
"@travetto/schema": "^8.0.0-alpha.
|
|
32
|
-
"@travetto/web": "^8.0.0-alpha.
|
|
30
|
+
"@travetto/config": "^8.0.0-alpha.14",
|
|
31
|
+
"@travetto/schema": "^8.0.0-alpha.14",
|
|
32
|
+
"@travetto/web": "^8.0.0-alpha.15"
|
|
33
33
|
},
|
|
34
34
|
"travetto": {
|
|
35
35
|
"displayName": "Web RPC Support"
|
package/src/controller.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { IsPrivate } from '@travetto/schema';
|
|
|
4
4
|
import {
|
|
5
5
|
HeaderParam, Controller, ExcludeInterceptors, ControllerRegistryIndex,
|
|
6
6
|
type WebAsyncContext, Body, EndpointUtil, BodyInterceptor, Post, WebCommonUtil,
|
|
7
|
-
RespondInterceptor, DecompressInterceptor, Get
|
|
7
|
+
RespondInterceptor, DecompressInterceptor, Get, QueryParam, Delete, Put, Patch
|
|
8
8
|
} from '@travetto/web';
|
|
9
9
|
|
|
10
10
|
@Controller('/rpc')
|
|
@@ -21,13 +21,28 @@ export class WebRpcController {
|
|
|
21
21
|
ctx: WebAsyncContext;
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
-
* Allow for
|
|
24
|
+
* Allow for extra method-based requests
|
|
25
25
|
*/
|
|
26
26
|
@Get('/:target')
|
|
27
|
-
|
|
27
|
+
onGetRequest(target: string, @QueryParam('TRV_RPC_INPUTS') paramInput?: string): Promise<unknown> {
|
|
28
28
|
return this.onRequest(target, paramInput);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
@Delete('/:target')
|
|
32
|
+
onDeleteRequest(target: string, @QueryParam('TRV_RPC_INPUTS') paramInput?: string): Promise<unknown> {
|
|
33
|
+
return this.onRequest(target, paramInput);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@Put('/:target')
|
|
37
|
+
onPutRequest(target: string, @HeaderParam('X-TRV-RPC-INPUTS') paramInput?: string, @Body() body?: Any): Promise<unknown> {
|
|
38
|
+
return this.onRequest(target, paramInput, body);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@Patch('/:target')
|
|
42
|
+
onPatchRequest(target: string, @HeaderParam('X-TRV-RPC-INPUTS') paramInput?: string, @Body() body?: Any): Promise<unknown> {
|
|
43
|
+
return this.onRequest(target, paramInput, body);
|
|
44
|
+
}
|
|
45
|
+
|
|
31
46
|
/**
|
|
32
47
|
* RPC main entrypoint
|
|
33
48
|
*/
|
package/support/client/rpc.ts
CHANGED
|
@@ -3,13 +3,21 @@ type MethodKeys<C extends {}> = {
|
|
|
3
3
|
}[keyof C];
|
|
4
4
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5
5
|
type PromiseFn = (...args: any) => Promise<unknown>;
|
|
6
|
-
type
|
|
6
|
+
type PromiseResult<V extends PromiseFn> = Awaited<ReturnType<V>>;
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
const isBlobMap = (value: unknown): value is Record<string, Blob> => {
|
|
9
|
+
if (typeof value !== 'object' || value === null) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
for (const [key, item] of Object.entries(value)) {
|
|
13
|
+
if (!(item instanceof Blob) || typeof key !== 'string') {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return true;
|
|
18
|
+
};
|
|
10
19
|
|
|
11
|
-
|
|
12
|
-
const isBlobLike = (value: any): value is Record<string, Blob> | Blob => value instanceof Blob || isBlobMap(value);
|
|
20
|
+
const isBlobLike = (value: unknown): value is Record<string, Blob> | Blob => value instanceof Blob || isBlobMap(value);
|
|
13
21
|
|
|
14
22
|
const extendHeaders = (base: RequestInit['headers'], toAdd: Record<string, string>): Headers => {
|
|
15
23
|
const headers = new Headers(base);
|
|
@@ -18,10 +26,10 @@ const extendHeaders = (base: RequestInit['headers'], toAdd: Record<string, strin
|
|
|
18
26
|
};
|
|
19
27
|
|
|
20
28
|
const jsonToString = (input: unknown): string =>
|
|
21
|
-
JSON.stringify(input, (
|
|
29
|
+
JSON.stringify(input, (_, value): unknown => typeof value === 'bigint' ? `${value.toString()}n` : value);
|
|
22
30
|
|
|
23
31
|
const stringToJson = <T = unknown>(input: string): T =>
|
|
24
|
-
JSON.parse(input, (
|
|
32
|
+
JSON.parse(input, (_, value): unknown => {
|
|
25
33
|
if (typeof value === 'string') {
|
|
26
34
|
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[.]\d{3}Z$/.test(value)) {
|
|
27
35
|
return new Date(value);
|
|
@@ -33,16 +41,17 @@ const stringToJson = <T = unknown>(input: string): T =>
|
|
|
33
41
|
});
|
|
34
42
|
|
|
35
43
|
|
|
36
|
-
export type PreRequestHandler = (item: RequestInit) => Promise<
|
|
44
|
+
export type PreRequestHandler = (item: RequestInit) => Promise<RpcRequest['core'] | undefined | void>;
|
|
37
45
|
export type PostResponseHandler = (item: Response) => Promise<Response | undefined | void>;
|
|
38
46
|
|
|
39
47
|
export type RpcRequest = {
|
|
40
|
-
core
|
|
48
|
+
core: Omit<Partial<RequestInit>, 'method'> & {
|
|
41
49
|
timeout?: number;
|
|
42
50
|
retriesOnConnectFailure?: number;
|
|
43
|
-
path
|
|
44
|
-
controller
|
|
45
|
-
endpoint
|
|
51
|
+
path: string;
|
|
52
|
+
controller: string;
|
|
53
|
+
endpoint: string;
|
|
54
|
+
method: string;
|
|
46
55
|
};
|
|
47
56
|
url: URL | string;
|
|
48
57
|
consumeJSON?: <T>(text?: unknown) => (T | Promise<T>);
|
|
@@ -57,7 +66,7 @@ export type RpcClient<T extends Record<string, {}>, E extends Record<string, Fun
|
|
|
57
66
|
|
|
58
67
|
export type RpcClientFactory<T extends Record<string, {}>> =
|
|
59
68
|
<R extends Record<string, Function>>(
|
|
60
|
-
baseOpts: RpcRequest,
|
|
69
|
+
baseOpts: Omit<Partial<RpcRequest>, 'url'> & { url: RpcRequest['url'] },
|
|
61
70
|
decorate?: (request: RpcRequest) => R
|
|
62
71
|
) => RpcClient<T, R>;
|
|
63
72
|
|
|
@@ -87,28 +96,49 @@ function registerTimeout<T>(
|
|
|
87
96
|
}
|
|
88
97
|
|
|
89
98
|
function buildRequest<T extends RequestInit>(base: T, controller: string, endpoint: string): T {
|
|
99
|
+
let verb: string;
|
|
100
|
+
switch (endpoint.split(/[A-Z]/)[0]) {
|
|
101
|
+
case 'get': verb = 'GET'; break;
|
|
102
|
+
case 'update': verb = 'PUT'; break;
|
|
103
|
+
case 'delete': verb = 'DELETE'; break;
|
|
104
|
+
case 'create': default: verb = 'POST'; break;
|
|
105
|
+
}
|
|
90
106
|
return {
|
|
91
107
|
...base,
|
|
92
|
-
method:
|
|
108
|
+
method: verb,
|
|
93
109
|
path: `${controller}:${endpoint}`,
|
|
94
110
|
controller,
|
|
95
111
|
endpoint
|
|
96
112
|
};
|
|
97
113
|
}
|
|
98
114
|
|
|
99
|
-
export function getBody(inputs: unknown[],
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
115
|
+
export function getBody(inputs: unknown[], method: string): {
|
|
116
|
+
body: FormData | string | undefined;
|
|
117
|
+
query: Record<string, string> | undefined;
|
|
118
|
+
method: string;
|
|
119
|
+
headers: Record<string, string>;
|
|
120
|
+
} {
|
|
121
|
+
let outMethod = method;
|
|
122
|
+
if (method.toLowerCase() === 'get' || method.toLowerCase() === 'delete') {
|
|
123
|
+
const inputComputed = btoa(encodeURIComponent(jsonToString(inputs)));
|
|
124
|
+
if (inputComputed.length < 1000) {
|
|
125
|
+
return {
|
|
126
|
+
body: undefined,
|
|
127
|
+
method: outMethod,
|
|
128
|
+
query: { TRV_RPC_INPUTS: inputComputed },
|
|
129
|
+
headers: {}
|
|
130
|
+
};
|
|
131
|
+
} else {
|
|
132
|
+
outMethod = 'POST';
|
|
133
|
+
}
|
|
107
134
|
}
|
|
135
|
+
|
|
108
136
|
// If we do not have a blob, simple output
|
|
109
137
|
if (!inputs.some(isBlobLike)) {
|
|
110
138
|
return {
|
|
111
139
|
body: jsonToString(inputs),
|
|
140
|
+
query: undefined,
|
|
141
|
+
method: outMethod,
|
|
112
142
|
headers: {
|
|
113
143
|
'Content-Type': 'application/json'
|
|
114
144
|
}
|
|
@@ -130,6 +160,8 @@ export function getBody(inputs: unknown[], isBodyRequest: boolean): { body: Form
|
|
|
130
160
|
|
|
131
161
|
return {
|
|
132
162
|
body: form,
|
|
163
|
+
query: undefined,
|
|
164
|
+
method: outMethod,
|
|
133
165
|
headers: {
|
|
134
166
|
'X-TRV-RPC-INPUTS': btoa(encodeURIComponent(jsonToString(plainInputs)))
|
|
135
167
|
}
|
|
@@ -169,20 +201,16 @@ export async function consumeError(error: unknown): Promise<Error> {
|
|
|
169
201
|
}
|
|
170
202
|
|
|
171
203
|
export async function invokeFetch<T>(request: RpcRequest, ...params: unknown[]): Promise<T> {
|
|
172
|
-
let core = request.core
|
|
204
|
+
let core = { ...request.core };
|
|
173
205
|
|
|
174
206
|
try {
|
|
175
|
-
const { body, headers } = getBody(params,
|
|
176
|
-
if (body) {
|
|
177
|
-
|
|
178
|
-
}
|
|
207
|
+
const { method, body, headers, query } = getBody(params, request.core.method);
|
|
208
|
+
if (body) { core.body = body; }
|
|
209
|
+
core.method = method;
|
|
179
210
|
core.headers = extendHeaders(core.headers, headers);
|
|
180
211
|
|
|
181
212
|
for (const fn of request.preRequestHandlers ?? []) {
|
|
182
|
-
|
|
183
|
-
if (computed) {
|
|
184
|
-
core = computed;
|
|
185
|
-
}
|
|
213
|
+
core = (await fn(core)) ?? core;
|
|
186
214
|
}
|
|
187
215
|
|
|
188
216
|
const signals = [];
|
|
@@ -202,8 +230,9 @@ export async function invokeFetch<T>(request: RpcRequest, ...params: unknown[]):
|
|
|
202
230
|
}
|
|
203
231
|
|
|
204
232
|
const url = typeof request.url === 'string' ? new URL(request.url) : request.url;
|
|
205
|
-
|
|
206
|
-
|
|
233
|
+
url.pathname = `${url.pathname}/${request.core.path || '/'}`.replaceAll('//', '/');
|
|
234
|
+
for (const [key, value] of Object.entries(query ?? {})) {
|
|
235
|
+
url.searchParams.append(key, value);
|
|
207
236
|
}
|
|
208
237
|
|
|
209
238
|
let resolved: Response | undefined;
|
|
@@ -265,7 +294,12 @@ export function clientFactory<T extends Record<string, {}>>(): RpcClientFactory<
|
|
|
265
294
|
consumeJSON,
|
|
266
295
|
consumeError,
|
|
267
296
|
...request,
|
|
268
|
-
core: {
|
|
297
|
+
core: {
|
|
298
|
+
method: 'POST',
|
|
299
|
+
path: undefined!, controller: undefined!, endpoint: undefined!,
|
|
300
|
+
timeout: 0, credentials: 'include', mode: 'cors',
|
|
301
|
+
...request.core
|
|
302
|
+
},
|
|
269
303
|
};
|
|
270
304
|
const cache: Record<string, unknown> = {};
|
|
271
305
|
// @ts-ignore
|
|
@@ -292,7 +326,7 @@ export function clientFactory<T extends Record<string, {}>>(): RpcClientFactory<
|
|
|
292
326
|
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
|
293
327
|
export function withConfigFactoryDecorator(request: RpcRequest) {
|
|
294
328
|
return {
|
|
295
|
-
withConfig<V extends PromiseFn>(this: V, extra: Partial<RpcRequest['core']>, ...params: Parameters<V>): Promise<
|
|
329
|
+
withConfig<V extends PromiseFn>(this: V, extra: Partial<RpcRequest['core']>, ...params: Parameters<V>): Promise<PromiseResult<V>> {
|
|
296
330
|
return invokeFetch({ ...request, core: { ...request.core, ...extra } }, ...params);
|
|
297
331
|
}
|
|
298
332
|
};
|