@travetto/web-rpc 7.0.0-rc.2 → 7.0.0-rc.3
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 +7 -7
- package/src/service.ts +9 -9
- package/support/cli.web_rpc-client.ts +3 -4
- package/support/client/rpc-node.ts +4 -4
- package/support/client/rpc.ts +39 -39
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/web-rpc",
|
|
3
|
-
"version": "7.0.0-rc.
|
|
3
|
+
"version": "7.0.0-rc.3",
|
|
4
4
|
"description": "RPC support for a Web Application",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"web",
|
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
"directory": "module/web-rpc"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@travetto/config": "^7.0.0-rc.
|
|
30
|
-
"@travetto/schema": "^7.0.0-rc.
|
|
31
|
-
"@travetto/web": "^7.0.0-rc.
|
|
29
|
+
"@travetto/config": "^7.0.0-rc.2",
|
|
30
|
+
"@travetto/schema": "^7.0.0-rc.2",
|
|
31
|
+
"@travetto/web": "^7.0.0-rc.3"
|
|
32
32
|
},
|
|
33
33
|
"travetto": {
|
|
34
34
|
"displayName": "Web RPC Support"
|
package/src/controller.ts
CHANGED
|
@@ -8,11 +8,11 @@ import {
|
|
|
8
8
|
} from '@travetto/web';
|
|
9
9
|
|
|
10
10
|
@Controller('/rpc')
|
|
11
|
-
@ExcludeInterceptors(
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
@ExcludeInterceptors(value => !(
|
|
12
|
+
value instanceof DecompressInterceptor ||
|
|
13
|
+
value instanceof BodyInterceptor ||
|
|
14
|
+
value instanceof RespondInterceptor ||
|
|
15
|
+
value.category === 'global'
|
|
16
16
|
))
|
|
17
17
|
@IsPrivate()
|
|
18
18
|
export class WebRpcController {
|
|
@@ -49,7 +49,7 @@ export class WebRpcController {
|
|
|
49
49
|
} else if (Array.isArray(body)) { // Params passed via body
|
|
50
50
|
params = body;
|
|
51
51
|
|
|
52
|
-
const bodyParamIdx = endpoint.parameters.findIndex((
|
|
52
|
+
const bodyParamIdx = endpoint.parameters.findIndex((config) => config.location === 'body');
|
|
53
53
|
if (bodyParamIdx >= 0) { // Re-assign body
|
|
54
54
|
request.body = params[bodyParamIdx];
|
|
55
55
|
}
|
|
@@ -59,7 +59,7 @@ export class WebRpcController {
|
|
|
59
59
|
params = [];
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
const final = endpoint.parameters.map((
|
|
62
|
+
const final = endpoint.parameters.map((config, i) => (config.location === 'body' && paramInput) ? EndpointUtil.MissingParamSymbol : params[i]);
|
|
63
63
|
WebCommonUtil.setRequestParams(request, final);
|
|
64
64
|
|
|
65
65
|
// Dispatch
|
package/src/service.ts
CHANGED
|
@@ -28,16 +28,16 @@ export class WebRpcClientGeneratorService {
|
|
|
28
28
|
|
|
29
29
|
async #getClasses(relativeTo: string): Promise<{ name: string, import: string }[]> {
|
|
30
30
|
return ControllerRegistryIndex.getClasses()
|
|
31
|
-
.filter(
|
|
32
|
-
const entry = RuntimeIndex.getEntry(Runtime.getSourceFile(
|
|
31
|
+
.filter(cls => {
|
|
32
|
+
const entry = RuntimeIndex.getEntry(Runtime.getSourceFile(cls));
|
|
33
33
|
return entry && entry.role === 'std';
|
|
34
34
|
})
|
|
35
|
-
.filter(
|
|
36
|
-
.map(
|
|
37
|
-
const imp = ManifestModuleUtil.withOutputExtension(Runtime.getImport(
|
|
35
|
+
.filter(cls => SchemaRegistryIndex.getConfig(cls).private !== true)
|
|
36
|
+
.map(config => {
|
|
37
|
+
const imp = ManifestModuleUtil.withOutputExtension(Runtime.getImport(config));
|
|
38
38
|
const base = Runtime.workspaceRelative(RuntimeIndex.manifest.build.typesFolder);
|
|
39
39
|
return {
|
|
40
|
-
name:
|
|
40
|
+
name: config.name,
|
|
41
41
|
import: path.relative(relativeTo, `${base}/node_modules/${imp}`)
|
|
42
42
|
};
|
|
43
43
|
});
|
|
@@ -56,15 +56,15 @@ export class WebRpcClientGeneratorService {
|
|
|
56
56
|
const flavorOutputFile = path.resolve(config.output, path.basename(flavorSourceFile));
|
|
57
57
|
const flavorSourceContents = (await fs.readFile(flavorSourceFile, 'utf8').catch(() => ''))
|
|
58
58
|
.replaceAll(/^\s*\/\/\s*@ts-ignore[^\n]*\n/gsm, '')
|
|
59
|
-
.replaceAll(/^\/\/\s*#UNCOMMENT (.*)/gm, (_,
|
|
59
|
+
.replaceAll(/^\/\/\s*#UNCOMMENT (.*)/gm, (_, line) => line);
|
|
60
60
|
|
|
61
61
|
const factoryOutputFile = path.resolve(config.output, 'factory.ts');
|
|
62
62
|
const factorySourceContents = [
|
|
63
63
|
`import { ${clientFactory.name} } from './rpc';`,
|
|
64
|
-
...classes.map((
|
|
64
|
+
...classes.map((cls) => `import type { ${cls.name} } from '${cls.import}';`),
|
|
65
65
|
'',
|
|
66
66
|
`export const factory = ${clientFactory.name}<{`,
|
|
67
|
-
...classes.map(
|
|
67
|
+
...classes.map(cls => ` ${cls.name}: ${cls.name};`),
|
|
68
68
|
'}>();',
|
|
69
69
|
].join('\n');
|
|
70
70
|
|
|
@@ -28,18 +28,17 @@ export class CliWebRpcCommand implements CliCommandShape {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
async main(type: WebRpcClient['type'] | 'config', output?: string): Promise<void> {
|
|
31
|
+
const service = await this.#service;
|
|
31
32
|
if (type === 'config') {
|
|
32
|
-
|
|
33
|
-
await svc.render();
|
|
33
|
+
await service.render();
|
|
34
34
|
} else {
|
|
35
35
|
if (!output) {
|
|
36
36
|
throw new CliValidationResultError(this, [
|
|
37
37
|
{ message: 'output is required when type is not `config`', source: 'arg' }
|
|
38
38
|
]);
|
|
39
39
|
}
|
|
40
|
-
const svc = await this.#service;
|
|
41
40
|
output = path.resolve(output);
|
|
42
|
-
return
|
|
41
|
+
return service.renderProvider({ type, output });
|
|
43
42
|
}
|
|
44
43
|
}
|
|
45
44
|
}
|
|
@@ -2,11 +2,11 @@ import { consumeError } from './rpc.ts';
|
|
|
2
2
|
|
|
3
3
|
export async function toNodeError(payload: unknown): Promise<Error> {
|
|
4
4
|
try {
|
|
5
|
-
let
|
|
5
|
+
let result = undefined;
|
|
6
6
|
const { AppError } = await import('@travetto/runtime');
|
|
7
|
-
|
|
8
|
-
if (
|
|
9
|
-
return
|
|
7
|
+
result = AppError.fromJSON(payload);
|
|
8
|
+
if (result) {
|
|
9
|
+
return result;
|
|
10
10
|
}
|
|
11
11
|
} catch { }
|
|
12
12
|
return consumeError(payload);
|
package/support/client/rpc.ts
CHANGED
|
@@ -6,14 +6,14 @@ type PromiseFn = (...args: any) => Promise<unknown>;
|
|
|
6
6
|
type PromiseRes<V extends PromiseFn> = Awaited<ReturnType<V>>;
|
|
7
7
|
|
|
8
8
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
9
|
-
const isBlobMap = (
|
|
9
|
+
const isBlobMap = (value: any): value is Record<string, Blob> => value && typeof value === 'object' && value[Object.keys(value)[0]] instanceof Blob;
|
|
10
10
|
|
|
11
11
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
12
|
-
const isBlobLike = (
|
|
12
|
+
const isBlobLike = (value: any): value is Record<string, Blob> | Blob => value instanceof Blob || isBlobMap(value);
|
|
13
13
|
|
|
14
14
|
const extendHeaders = (base: RequestInit['headers'], toAdd: Record<string, string>): Headers => {
|
|
15
15
|
const headers = new Headers(base);
|
|
16
|
-
for (const [
|
|
16
|
+
for (const [key, value] of Object.entries(toAdd)) { headers.set(key, value); }
|
|
17
17
|
return headers;
|
|
18
18
|
};
|
|
19
19
|
|
|
@@ -42,26 +42,26 @@ export type RpcClient<T extends Record<string, {}>, E extends Record<string, Fun
|
|
|
42
42
|
export type RpcClientFactory<T extends Record<string, {}>> =
|
|
43
43
|
<R extends Record<string, Function>>(
|
|
44
44
|
baseOpts: RpcRequest,
|
|
45
|
-
decorate?: (
|
|
45
|
+
decorate?: (request: RpcRequest) => R
|
|
46
46
|
) => RpcClient<T, R>;
|
|
47
47
|
|
|
48
|
-
function isResponse(
|
|
49
|
-
return !!
|
|
48
|
+
function isResponse(value: unknown): value is Response {
|
|
49
|
+
return !!value && typeof value === 'object' && 'status' in value && !!value.status && 'headers' in value && !!value.headers;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
function isPlainObject(
|
|
53
|
-
return typeof
|
|
54
|
-
&&
|
|
55
|
-
&&
|
|
56
|
-
&&
|
|
57
|
-
&& Object.prototype.toString.call(
|
|
52
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
53
|
+
return typeof value === 'object' // separate from primitives
|
|
54
|
+
&& value !== undefined
|
|
55
|
+
&& value !== null // is obvious
|
|
56
|
+
&& value.constructor === Object // separate instances (Array, DOM, ...)
|
|
57
|
+
&& Object.prototype.toString.call(value) === '[object Object]'; // separate build-in like Math
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
function registerTimeout<T extends (number | string | { unref(): unknown })>(
|
|
61
61
|
controller: AbortController,
|
|
62
62
|
timeout: number,
|
|
63
63
|
start: (fn: (...args: unknown[]) => unknown, delay: number) => T,
|
|
64
|
-
stop: (
|
|
64
|
+
stop: (value: T) => void
|
|
65
65
|
): void {
|
|
66
66
|
const timer = start(() => controller.abort(), timeout);
|
|
67
67
|
if (!(typeof timer === 'number' || typeof timer === 'string')) {
|
|
@@ -99,14 +99,14 @@ export function getBody(inputs: unknown[], isBodyRequest: boolean): { body: Form
|
|
|
99
99
|
};
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
const plainInputs = inputs.map(
|
|
102
|
+
const plainInputs = inputs.map(value => isBlobLike(value) ? null : value);
|
|
103
103
|
const form = new FormData();
|
|
104
104
|
|
|
105
|
-
for (const
|
|
106
|
-
if (
|
|
107
|
-
form.append('file',
|
|
105
|
+
for (const input of inputs.filter(isBlobLike)) {
|
|
106
|
+
if (input instanceof Blob) {
|
|
107
|
+
form.append('file', input, (input instanceof File) ? input.name : undefined);
|
|
108
108
|
} else {
|
|
109
|
-
for (const [name, blob] of Object.entries(
|
|
109
|
+
for (const [name, blob] of Object.entries(input)) {
|
|
110
110
|
form.append(name, blob, (blob instanceof File) ? blob.name : undefined);
|
|
111
111
|
}
|
|
112
112
|
}
|
|
@@ -134,21 +134,21 @@ export function consumeJSON<T>(text: string | unknown): T {
|
|
|
134
134
|
return value;
|
|
135
135
|
}
|
|
136
136
|
});
|
|
137
|
-
} catch (
|
|
138
|
-
throw new Error(`Unable to parse response: ${text}, Unknown error: ${
|
|
137
|
+
} catch (error) {
|
|
138
|
+
throw new Error(`Unable to parse response: ${text}, Unknown error: ${error}`);
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
export async function consumeError(
|
|
143
|
-
if (
|
|
144
|
-
return
|
|
145
|
-
} else if (isResponse(
|
|
146
|
-
const out = new Error(
|
|
147
|
-
Object.assign(out, { status:
|
|
142
|
+
export async function consumeError(error: unknown): Promise<Error> {
|
|
143
|
+
if (error instanceof Error) {
|
|
144
|
+
return error;
|
|
145
|
+
} else if (isResponse(error)) {
|
|
146
|
+
const out = new Error(error.statusText);
|
|
147
|
+
Object.assign(out, { status: error.status });
|
|
148
148
|
return consumeError(out);
|
|
149
|
-
} else if (isPlainObject(
|
|
149
|
+
} else if (isPlainObject(error)) {
|
|
150
150
|
const out = new Error();
|
|
151
|
-
Object.assign(out,
|
|
151
|
+
Object.assign(out, error);
|
|
152
152
|
return consumeError(out);
|
|
153
153
|
} else {
|
|
154
154
|
return new Error('Unknown error');
|
|
@@ -198,12 +198,12 @@ export async function invokeFetch<T>(request: RpcRequest, ...params: unknown[]):
|
|
|
198
198
|
try {
|
|
199
199
|
resolved = await fetch(url, core);
|
|
200
200
|
break;
|
|
201
|
-
} catch (
|
|
201
|
+
} catch (error) {
|
|
202
202
|
if (i < (core.retriesOnConnectFailure ?? 0)) {
|
|
203
|
-
await new Promise(
|
|
203
|
+
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait 1s
|
|
204
204
|
continue;
|
|
205
205
|
} else {
|
|
206
|
-
throw
|
|
206
|
+
throw error;
|
|
207
207
|
}
|
|
208
208
|
}
|
|
209
209
|
}
|
|
@@ -240,19 +240,19 @@ export async function invokeFetch<T>(request: RpcRequest, ...params: unknown[]):
|
|
|
240
240
|
}
|
|
241
241
|
throw responseObject;
|
|
242
242
|
}
|
|
243
|
-
} catch (
|
|
244
|
-
throw await request.consumeError!(
|
|
243
|
+
} catch (error) {
|
|
244
|
+
throw await request.consumeError!(error);
|
|
245
245
|
}
|
|
246
246
|
}
|
|
247
247
|
|
|
248
248
|
export function clientFactory<T extends Record<string, {}>>(): RpcClientFactory<T> {
|
|
249
249
|
// @ts-ignore
|
|
250
|
-
return function (
|
|
250
|
+
return function (request, decorate) {
|
|
251
251
|
const client: RpcRequest = {
|
|
252
252
|
consumeJSON,
|
|
253
253
|
consumeError,
|
|
254
|
-
...
|
|
255
|
-
core: { timeout: 0, credentials: 'include', mode: 'cors', ...
|
|
254
|
+
...request,
|
|
255
|
+
core: { timeout: 0, credentials: 'include', mode: 'cors', ...request.core },
|
|
256
256
|
};
|
|
257
257
|
const cache: Record<string, unknown> = {};
|
|
258
258
|
// @ts-ignore
|
|
@@ -267,7 +267,7 @@ export function clientFactory<T extends Record<string, {}>>(): RpcClientFactory<
|
|
|
267
267
|
return cache[`${controller}/${endpoint}`] ??= Object.defineProperties(
|
|
268
268
|
invokeFetch.bind(null, final),
|
|
269
269
|
Object.fromEntries(
|
|
270
|
-
Object.entries(decorate?.(final) ?? {}).map(([
|
|
270
|
+
Object.entries(decorate?.(final) ?? {}).map(([key, value]) => [key, { value }])
|
|
271
271
|
)
|
|
272
272
|
);
|
|
273
273
|
}
|
|
@@ -277,10 +277,10 @@ export function clientFactory<T extends Record<string, {}>>(): RpcClientFactory<
|
|
|
277
277
|
}
|
|
278
278
|
|
|
279
279
|
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
|
280
|
-
export function withConfigFactoryDecorator(
|
|
280
|
+
export function withConfigFactoryDecorator(request: RpcRequest) {
|
|
281
281
|
return {
|
|
282
282
|
withConfig<V extends PromiseFn>(this: V, extra: Partial<RpcRequest['core']>, ...params: Parameters<V>): Promise<PromiseRes<V>> {
|
|
283
|
-
return invokeFetch({ ...
|
|
283
|
+
return invokeFetch({ ...request, core: { ...request.core, ...extra } }, ...params);
|
|
284
284
|
}
|
|
285
285
|
};
|
|
286
286
|
}
|