@client-compose/client 0.5.1 → 0.7.0
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/README.md +8 -1
- package/dist/index.d.ts +39 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +181 -21
- package/dist/index.js.map +1 -1
- package/dist/proxy.d.ts +9 -1
- package/dist/proxy.d.ts.map +1 -1
- package/dist/proxy.js +87 -21
- package/dist/proxy.js.map +1 -1
- package/dist/testing.d.ts +19 -2
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +118 -20
- package/dist/testing.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +218 -20
- package/src/proxy.ts +107 -25
- package/src/testing.ts +147 -20
package/src/index.ts
CHANGED
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
type DeclaredError,
|
|
11
11
|
type IslandDescriptor,
|
|
12
12
|
type IslandEntry,
|
|
13
|
+
type IslandRequest,
|
|
14
|
+
type IslandResponse,
|
|
13
15
|
PROTOCOL_VERSION,
|
|
14
16
|
type ScreenRequest,
|
|
15
17
|
type ScreenResponse,
|
|
@@ -31,13 +33,25 @@ export class IslandRegistry<R> {
|
|
|
31
33
|
versionOrRender: number | IslandRenderer<R, P>,
|
|
32
34
|
maybeRender?: IslandRenderer<R, P>,
|
|
33
35
|
): this {
|
|
34
|
-
|
|
35
|
-
typeof
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
if (typeof islandOrType !== "string") {
|
|
37
|
+
if (typeof versionOrRender === "number") {
|
|
38
|
+
throw new Error("register(descriptor, render): pass the renderer, not a version");
|
|
39
|
+
}
|
|
40
|
+
return this.add(islandOrType.type, islandOrType.version, versionOrRender);
|
|
41
|
+
}
|
|
42
|
+
if (typeof versionOrRender !== "number" || !maybeRender) {
|
|
43
|
+
throw new Error("register(type, version, render): pass a version and a renderer");
|
|
44
|
+
}
|
|
45
|
+
return this.add(islandOrType, versionOrRender, maybeRender);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private add<P>(type: string, version: number, render: IslandRenderer<R, P>): this {
|
|
49
|
+
const versions = this.renderers.get(type) ?? new Map<number, IslandRenderer<R>>();
|
|
50
|
+
// The registry hands each renderer the props of its own island type and version, so the
|
|
51
|
+
// renderer's props type holds. TypeScript can't see that through one map of every island.
|
|
52
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
53
|
+
versions.set(version, render as IslandRenderer<R>);
|
|
54
|
+
this.renderers.set(type, versions);
|
|
41
55
|
return this;
|
|
42
56
|
}
|
|
43
57
|
|
|
@@ -61,7 +75,13 @@ export function buildRequest(
|
|
|
61
75
|
client: ClientContext,
|
|
62
76
|
params?: Record<string, string>,
|
|
63
77
|
): ScreenRequest {
|
|
64
|
-
return {
|
|
78
|
+
return {
|
|
79
|
+
protocol: PROTOCOL_VERSION,
|
|
80
|
+
screen,
|
|
81
|
+
params,
|
|
82
|
+
client,
|
|
83
|
+
islands: registry.capabilities(),
|
|
84
|
+
};
|
|
65
85
|
}
|
|
66
86
|
|
|
67
87
|
export interface PlannedIsland<R> {
|
|
@@ -88,7 +108,9 @@ export function plan<R>(
|
|
|
88
108
|
for (const entry of response.islands) {
|
|
89
109
|
if (entry.status === "error") {
|
|
90
110
|
const fallback = options.onError?.(entry);
|
|
91
|
-
if (fallback !== undefined)
|
|
111
|
+
if (fallback !== undefined) {
|
|
112
|
+
out.push({ key: entry.key, render: () => fallback });
|
|
113
|
+
}
|
|
92
114
|
continue;
|
|
93
115
|
}
|
|
94
116
|
if (entry.status !== "ok") {
|
|
@@ -105,6 +127,41 @@ export function plan<R>(
|
|
|
105
127
|
return out;
|
|
106
128
|
}
|
|
107
129
|
|
|
130
|
+
// ---- Reading what the composer answered ----
|
|
131
|
+
|
|
132
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
133
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** The body as JSON, or undefined if it isn't JSON. */
|
|
137
|
+
function parseJson(text: string): unknown {
|
|
138
|
+
try {
|
|
139
|
+
return JSON.parse(text);
|
|
140
|
+
} catch {
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function isIslandEntry(value: unknown): value is IslandEntry {
|
|
146
|
+
if (!isRecord(value)) {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
const named = typeof value.key === "string" && typeof value.type === "string";
|
|
150
|
+
return named && typeof value.version === "number" && typeof value.status === "string";
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function isScreenResponse(value: unknown): value is ScreenResponse {
|
|
154
|
+
if (!isRecord(value) || typeof value.screen !== "string") {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
return Array.isArray(value.islands) && value.islands.every(isIslandEntry);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** An action's outcome: what a client needs of one is its status (the client fixtures, spec §8.2). */
|
|
161
|
+
function isActionResponse(value: unknown): value is ActionResponse {
|
|
162
|
+
return isRecord(value) && (value.status === "ok" || value.status === "error");
|
|
163
|
+
}
|
|
164
|
+
|
|
108
165
|
export class ScreenFetchError extends Error {
|
|
109
166
|
readonly status: number;
|
|
110
167
|
readonly body: string;
|
|
@@ -121,13 +178,27 @@ export class ScreenFetchError extends Error {
|
|
|
121
178
|
|
|
122
179
|
function errorCode(body: string): string | undefined {
|
|
123
180
|
try {
|
|
124
|
-
const
|
|
181
|
+
const parsed: unknown = JSON.parse(body);
|
|
182
|
+
const error = isRecord(parsed) ? parsed.error : undefined;
|
|
183
|
+
const code = isRecord(error) ? error.code : undefined;
|
|
125
184
|
return typeof code === "string" ? code : undefined;
|
|
126
185
|
} catch {
|
|
127
186
|
return undefined;
|
|
128
187
|
}
|
|
129
188
|
}
|
|
130
189
|
|
|
190
|
+
/**
|
|
191
|
+
* The caller's headers, given any way fetch takes them, sent as JSON unless they say otherwise.
|
|
192
|
+
* (Spreading a `Headers` object would drop them all.)
|
|
193
|
+
*/
|
|
194
|
+
function jsonHeaders(init: HeadersInit | undefined): Headers {
|
|
195
|
+
const headers = new Headers(init);
|
|
196
|
+
if (!headers.has("content-type")) {
|
|
197
|
+
headers.set("content-type", "application/json");
|
|
198
|
+
}
|
|
199
|
+
return headers;
|
|
200
|
+
}
|
|
201
|
+
|
|
131
202
|
export async function fetchScreen(
|
|
132
203
|
baseUrl: string,
|
|
133
204
|
request: ScreenRequest,
|
|
@@ -137,11 +208,99 @@ export async function fetchScreen(
|
|
|
137
208
|
const res = await fetchImpl(`${baseUrl}/screens/${encodeURIComponent(request.screen)}`, {
|
|
138
209
|
...init,
|
|
139
210
|
method: "POST",
|
|
140
|
-
headers:
|
|
211
|
+
headers: jsonHeaders(init.headers),
|
|
212
|
+
body: JSON.stringify(request),
|
|
213
|
+
});
|
|
214
|
+
const text = await res.text();
|
|
215
|
+
if (!res.ok) {
|
|
216
|
+
throw new ScreenFetchError(res.status, text);
|
|
217
|
+
}
|
|
218
|
+
const response = parseJson(text);
|
|
219
|
+
if (!isScreenResponse(response)) {
|
|
220
|
+
throw new ScreenFetchError(res.status, text);
|
|
221
|
+
}
|
|
222
|
+
return response;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ---- Island requests (spec §4.5) ----
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* A request for one island of a screen: the page `cursor` asks for (an entry's `next`), or the
|
|
229
|
+
* island from its first page without one. `params` are the screen's own.
|
|
230
|
+
*/
|
|
231
|
+
export function buildIslandRequest(
|
|
232
|
+
screen: string,
|
|
233
|
+
params: Record<string, string> | undefined,
|
|
234
|
+
entry: { key: string; version: number },
|
|
235
|
+
client: ClientContext,
|
|
236
|
+
cursor?: string,
|
|
237
|
+
): IslandRequest {
|
|
238
|
+
return {
|
|
239
|
+
protocol: PROTOCOL_VERSION,
|
|
240
|
+
screen,
|
|
241
|
+
key: entry.key,
|
|
242
|
+
version: entry.version,
|
|
243
|
+
params,
|
|
244
|
+
client,
|
|
245
|
+
...(cursor !== undefined && { cursor }),
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* POST an island request. Resolves with the island's entry: its props (and `next`, if it has more
|
|
251
|
+
* pages) or the error it failed with. Throws IslandFetchError for a non-2xx response (e.g.
|
|
252
|
+
* `invalid_cursor`, `unknown_island`, `unauthenticated`), or an answer that isn't this island.
|
|
253
|
+
*/
|
|
254
|
+
export async function fetchIsland(
|
|
255
|
+
baseUrl: string,
|
|
256
|
+
request: IslandRequest,
|
|
257
|
+
init: RequestInit = {},
|
|
258
|
+
fetchImpl: typeof fetch = fetch,
|
|
259
|
+
): Promise<IslandEntry> {
|
|
260
|
+
const path = `${encodeURIComponent(request.screen)}/islands/${encodeURIComponent(request.key)}`;
|
|
261
|
+
const res = await fetchImpl(`${baseUrl}/screens/${path}`, {
|
|
262
|
+
...init,
|
|
263
|
+
method: "POST",
|
|
264
|
+
headers: jsonHeaders(init.headers),
|
|
141
265
|
body: JSON.stringify(request),
|
|
142
266
|
});
|
|
143
|
-
|
|
144
|
-
|
|
267
|
+
const text = await res.text();
|
|
268
|
+
if (!res.ok) {
|
|
269
|
+
throw new IslandFetchError(res.status, text);
|
|
270
|
+
}
|
|
271
|
+
const response = parseJson(text);
|
|
272
|
+
if (!isIslandResponse(response) || !isIslandAsked(response.island, request)) {
|
|
273
|
+
throw new IslandFetchError(res.status, text);
|
|
274
|
+
}
|
|
275
|
+
return response.island;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* An island request failed: a non-2xx response such as `invalid_cursor`, `unknown_island` or
|
|
280
|
+
* `unauthenticated`, or an answer that isn't the island asked for. An island that failed on the
|
|
281
|
+
* composer isn't thrown: it's an entry with `status: "error"`.
|
|
282
|
+
*/
|
|
283
|
+
export class IslandFetchError extends Error {
|
|
284
|
+
readonly status: number;
|
|
285
|
+
readonly body: string;
|
|
286
|
+
/** The protocol error code from the response body (e.g. "invalid_cursor"), if there was one. */
|
|
287
|
+
readonly code: string | undefined;
|
|
288
|
+
|
|
289
|
+
constructor(status: number, body: string) {
|
|
290
|
+
super(`Island request failed with status ${status}`);
|
|
291
|
+
this.status = status;
|
|
292
|
+
this.body = body;
|
|
293
|
+
this.code = errorCode(body);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function isIslandResponse(value: unknown): value is IslandResponse {
|
|
298
|
+
return isRecord(value) && typeof value.screen === "string" && isIslandEntry(value.island);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Whether the answer is the island the request asked for, at the version it asked for. */
|
|
302
|
+
function isIslandAsked(island: IslandEntry, request: IslandRequest): boolean {
|
|
303
|
+
return island.key === request.key && island.version === request.version;
|
|
145
304
|
}
|
|
146
305
|
|
|
147
306
|
// ---- Actions (spec §5) ----
|
|
@@ -166,8 +325,11 @@ export class ActionFetchError extends Error {
|
|
|
166
325
|
|
|
167
326
|
/** A fresh idempotency key: one per user intent, reused when retrying it (spec §5.2). */
|
|
168
327
|
export function newIdempotencyKey(): string {
|
|
169
|
-
|
|
170
|
-
|
|
328
|
+
// React Native's Hermes may have no crypto, or crypto without randomUUID.
|
|
329
|
+
const c: Partial<Pick<Crypto, "randomUUID">> | undefined = globalThis.crypto;
|
|
330
|
+
if (c?.randomUUID) {
|
|
331
|
+
return c.randomUUID();
|
|
332
|
+
}
|
|
171
333
|
// React Native's Hermes has no crypto.randomUUID. Uniqueness, not secrecy, is what matters here.
|
|
172
334
|
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (ch) => {
|
|
173
335
|
const r = (Math.random() * 16) | 0;
|
|
@@ -191,7 +353,11 @@ export function buildActionRequest<I>(
|
|
|
191
353
|
};
|
|
192
354
|
}
|
|
193
355
|
|
|
194
|
-
/**
|
|
356
|
+
/**
|
|
357
|
+
* POST an action request. Resolves with the outcome the composer sent (ok or an error); throws
|
|
358
|
+
* ActionFetchError for a non-2xx response or a body that isn't an outcome. Check the outcome against
|
|
359
|
+
* the action with `checkActionResponse` (callAction does).
|
|
360
|
+
*/
|
|
195
361
|
export async function fetchAction(
|
|
196
362
|
baseUrl: string,
|
|
197
363
|
request: ActionRequest,
|
|
@@ -201,11 +367,40 @@ export async function fetchAction(
|
|
|
201
367
|
const res = await fetchImpl(`${baseUrl}/actions/${encodeURIComponent(request.action)}`, {
|
|
202
368
|
...init,
|
|
203
369
|
method: "POST",
|
|
204
|
-
headers:
|
|
370
|
+
headers: jsonHeaders(init.headers),
|
|
205
371
|
body: JSON.stringify(request),
|
|
206
372
|
});
|
|
207
|
-
|
|
208
|
-
|
|
373
|
+
const text = await res.text();
|
|
374
|
+
if (!res.ok) {
|
|
375
|
+
throw new ActionFetchError(res.status, text);
|
|
376
|
+
}
|
|
377
|
+
const response = parseJson(text);
|
|
378
|
+
if (!isActionResponse(response)) {
|
|
379
|
+
throw new ActionFetchError(res.status, text);
|
|
380
|
+
}
|
|
381
|
+
return response;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* The response, if it's a result or one of this action's own declared errors. Anything else throws
|
|
386
|
+
* ActionFetchError (status 200, with the code if there is one): an outcome the client can't read,
|
|
387
|
+
* or a code the action doesn't declare, such as one added after this app was built (spec §5.1).
|
|
388
|
+
*/
|
|
389
|
+
export function checkActionResponse<I, R, E extends DeclaredError>(
|
|
390
|
+
action: ActionDescriptor<I, R, E>,
|
|
391
|
+
response: unknown,
|
|
392
|
+
): ActionResponse<R, E> {
|
|
393
|
+
const error = isRecord(response) ? response.error : undefined;
|
|
394
|
+
const code = isRecord(error) ? error.code : undefined;
|
|
395
|
+
const status = isRecord(response) ? response.status : undefined;
|
|
396
|
+
const declared = status === "error" && typeof code === "string" && action.errors.includes(code);
|
|
397
|
+
if (isActionResponse(response) && (status === "ok" || declared)) {
|
|
398
|
+
// The result's and error's types are the action's: generated from its manifest, which the
|
|
399
|
+
// composer checks its outcomes against. They exist only at the type level.
|
|
400
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
401
|
+
return response as ActionResponse<R, E>;
|
|
402
|
+
}
|
|
403
|
+
throw new ActionFetchError(200, JSON.stringify(response));
|
|
209
404
|
}
|
|
210
405
|
|
|
211
406
|
export interface CallActionOptions {
|
|
@@ -236,5 +431,8 @@ export async function callAction<I, R, E extends DeclaredError>(
|
|
|
236
431
|
options.client,
|
|
237
432
|
options.idempotencyKey ?? newIdempotencyKey(),
|
|
238
433
|
);
|
|
239
|
-
return (
|
|
434
|
+
return checkActionResponse(
|
|
435
|
+
action,
|
|
436
|
+
await fetchAction(baseUrl, request, options.init, options.fetch),
|
|
437
|
+
);
|
|
240
438
|
}
|
package/src/proxy.ts
CHANGED
|
@@ -18,15 +18,26 @@ import {
|
|
|
18
18
|
type ActionResponse,
|
|
19
19
|
type Capabilities,
|
|
20
20
|
type ClientContext,
|
|
21
|
+
type IslandEntry,
|
|
22
|
+
type IslandRequest,
|
|
21
23
|
PROTOCOL_VERSION,
|
|
22
24
|
type ScreenRequest,
|
|
23
25
|
type ScreenResponse,
|
|
24
26
|
} from "@client-compose/protocol";
|
|
25
27
|
|
|
26
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
ActionFetchError,
|
|
30
|
+
fetchAction,
|
|
31
|
+
fetchIsland,
|
|
32
|
+
fetchScreen,
|
|
33
|
+
IslandFetchError,
|
|
34
|
+
ScreenFetchError,
|
|
35
|
+
} from "./index.ts";
|
|
27
36
|
|
|
28
37
|
/** A screen request from the browser: the server sets the client context. */
|
|
29
38
|
export type ProxiedScreenRequest = Omit<ScreenRequest, "client">;
|
|
39
|
+
/** An island request from the browser: the server sets the client context. */
|
|
40
|
+
export type ProxiedIslandRequest = Omit<IslandRequest, "client">;
|
|
30
41
|
/** An action request from the browser: the server sets the client context. */
|
|
31
42
|
export type ProxiedActionRequest = Omit<ActionRequest, "client">;
|
|
32
43
|
|
|
@@ -60,6 +71,11 @@ export interface ComposerProxy {
|
|
|
60
71
|
performAction(body: unknown): Promise<ProxyResult<ActionResponse>>;
|
|
61
72
|
/** Checks a browser's screen request, then loads it with the server's context and headers. */
|
|
62
73
|
loadScreen(body: unknown): Promise<ProxyResult<ScreenResponse>>;
|
|
74
|
+
/**
|
|
75
|
+
* Checks a browser's island request (a page of an island, spec §4.5), then loads it with the
|
|
76
|
+
* server's context and headers. Resolves with the island's entry.
|
|
77
|
+
*/
|
|
78
|
+
loadIsland(body: unknown): Promise<ProxyResult<IslandEntry>>;
|
|
63
79
|
}
|
|
64
80
|
|
|
65
81
|
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
@@ -78,25 +94,48 @@ function isStringRecord(value: unknown): value is Record<string, string> {
|
|
|
78
94
|
return isRecord(value) && Object.values(value).every((v) => typeof v === "string");
|
|
79
95
|
}
|
|
80
96
|
|
|
97
|
+
// Each guard returns at the first thing a request gets wrong, so it checks no more than it must.
|
|
98
|
+
|
|
81
99
|
export function isProxiedScreenRequest(value: unknown): value is ProxiedScreenRequest {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
)
|
|
100
|
+
if (!isRecord(value) || value.protocol !== PROTOCOL_VERSION) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
if (typeof value.screen !== "string") {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
if (value.params !== undefined && !isStringRecord(value.params)) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
return isCapabilities(value.islands);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function isProxiedIslandRequest(value: unknown): value is ProxiedIslandRequest {
|
|
113
|
+
if (!isRecord(value) || value.protocol !== PROTOCOL_VERSION) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
if (typeof value.screen !== "string" || typeof value.key !== "string") {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
if (typeof value.version !== "number" || !Number.isInteger(value.version)) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
if (value.params !== undefined && !isStringRecord(value.params)) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
return value.cursor === undefined || typeof value.cursor === "string";
|
|
89
126
|
}
|
|
90
127
|
|
|
91
128
|
export function isProxiedActionRequest(value: unknown): value is ProxiedActionRequest {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
129
|
+
if (!isRecord(value) || value.protocol !== PROTOCOL_VERSION) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
if (typeof value.action !== "string" || typeof value.version !== "number") {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
if (!("input" in value)) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
return value.idempotencyKey === undefined || typeof value.idempotencyKey === "string";
|
|
100
139
|
}
|
|
101
140
|
|
|
102
141
|
/** What the composer itself answers to a malformed request. */
|
|
@@ -108,12 +147,17 @@ const BAD_REQUEST = {
|
|
|
108
147
|
|
|
109
148
|
export function createComposerProxy(options: ComposerProxyOptions): ComposerProxy {
|
|
110
149
|
async function init(): Promise<RequestInit> {
|
|
111
|
-
const headers = (
|
|
112
|
-
|
|
150
|
+
const headers = new Headers(options.requestInit?.headers);
|
|
151
|
+
for (const [name, value] of Object.entries((await options.headers?.()) ?? {})) {
|
|
152
|
+
headers.set(name, value);
|
|
153
|
+
}
|
|
154
|
+
return { ...options.requestInit, headers };
|
|
113
155
|
}
|
|
114
156
|
|
|
115
157
|
async function performAction(body: unknown): Promise<ProxyResult<ActionResponse>> {
|
|
116
|
-
if (!isProxiedActionRequest(body))
|
|
158
|
+
if (!isProxiedActionRequest(body)) {
|
|
159
|
+
return BAD_REQUEST;
|
|
160
|
+
}
|
|
117
161
|
const request: ActionRequest = { ...body, client: await options.client() };
|
|
118
162
|
try {
|
|
119
163
|
const response = await fetchAction(options.baseUrl, request, await init(), options.fetch);
|
|
@@ -122,14 +166,21 @@ export function createComposerProxy(options: ComposerProxyOptions): ComposerProx
|
|
|
122
166
|
response: (await options.onActionResponse?.(request, response)) ?? response,
|
|
123
167
|
};
|
|
124
168
|
} catch (error) {
|
|
125
|
-
if (error instanceof ActionFetchError)
|
|
126
|
-
return {
|
|
169
|
+
if (error instanceof ActionFetchError) {
|
|
170
|
+
return {
|
|
171
|
+
ok: false,
|
|
172
|
+
status: error.status,
|
|
173
|
+
body: error.body,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
127
176
|
throw error;
|
|
128
177
|
}
|
|
129
178
|
}
|
|
130
179
|
|
|
131
180
|
async function loadScreen(body: unknown): Promise<ProxyResult<ScreenResponse>> {
|
|
132
|
-
if (!isProxiedScreenRequest(body))
|
|
181
|
+
if (!isProxiedScreenRequest(body)) {
|
|
182
|
+
return BAD_REQUEST;
|
|
183
|
+
}
|
|
133
184
|
const request: ScreenRequest = { ...body, client: await options.client() };
|
|
134
185
|
try {
|
|
135
186
|
return {
|
|
@@ -137,11 +188,42 @@ export function createComposerProxy(options: ComposerProxyOptions): ComposerProx
|
|
|
137
188
|
response: await fetchScreen(options.baseUrl, request, await init(), options.fetch),
|
|
138
189
|
};
|
|
139
190
|
} catch (error) {
|
|
140
|
-
if (error instanceof ScreenFetchError)
|
|
141
|
-
return {
|
|
191
|
+
if (error instanceof ScreenFetchError) {
|
|
192
|
+
return {
|
|
193
|
+
ok: false,
|
|
194
|
+
status: error.status,
|
|
195
|
+
body: error.body,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function loadIsland(body: unknown): Promise<ProxyResult<IslandEntry>> {
|
|
203
|
+
if (!isProxiedIslandRequest(body)) {
|
|
204
|
+
return BAD_REQUEST;
|
|
205
|
+
}
|
|
206
|
+
const request: IslandRequest = { ...body, client: await options.client() };
|
|
207
|
+
try {
|
|
208
|
+
return {
|
|
209
|
+
ok: true,
|
|
210
|
+
response: await fetchIsland(options.baseUrl, request, await init(), options.fetch),
|
|
211
|
+
};
|
|
212
|
+
} catch (error) {
|
|
213
|
+
if (error instanceof IslandFetchError) {
|
|
214
|
+
return {
|
|
215
|
+
ok: false,
|
|
216
|
+
status: error.status,
|
|
217
|
+
body: error.body,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
142
220
|
throw error;
|
|
143
221
|
}
|
|
144
222
|
}
|
|
145
223
|
|
|
146
|
-
return {
|
|
224
|
+
return {
|
|
225
|
+
performAction,
|
|
226
|
+
loadScreen,
|
|
227
|
+
loadIsland,
|
|
228
|
+
};
|
|
147
229
|
}
|