@client-compose/client 0.6.0 → 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 +28 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +161 -25
- 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 +194 -26
- package/src/proxy.ts +107 -25
- package/src/testing.ts +147 -20
package/dist/testing.d.ts
CHANGED
|
@@ -19,6 +19,15 @@ export interface RecordedScreenRequest extends RecordedRequest {
|
|
|
19
19
|
client: ClientContext | undefined;
|
|
20
20
|
islands: Capabilities;
|
|
21
21
|
}
|
|
22
|
+
/** An island request (spec §4.5): a page of one island, or the island resolved again. */
|
|
23
|
+
export interface RecordedIslandRequest extends RecordedRequest {
|
|
24
|
+
screen: string;
|
|
25
|
+
key: string;
|
|
26
|
+
version: number;
|
|
27
|
+
params: Record<string, string>;
|
|
28
|
+
cursor: string | undefined;
|
|
29
|
+
client: ClientContext | undefined;
|
|
30
|
+
}
|
|
22
31
|
export interface RecordedActionRequest extends RecordedRequest {
|
|
23
32
|
action: string;
|
|
24
33
|
version: number;
|
|
@@ -31,6 +40,8 @@ export interface FakeGatewayHandlers {
|
|
|
31
40
|
screens?: Record<string, (request: RecordedScreenRequest) => FakeResponse>;
|
|
32
41
|
/** By action name. */
|
|
33
42
|
actions?: Record<string, (request: RecordedActionRequest) => FakeResponse>;
|
|
43
|
+
/** Island requests, by `"{screen}/{key}"`. */
|
|
44
|
+
islands?: Record<string, (request: RecordedIslandRequest) => FakeResponse>;
|
|
34
45
|
/** Anything else, e.g. uploads to storage: return undefined to leave a request to the others. */
|
|
35
46
|
other?: (request: RecordedRequest) => FakeResponse | undefined;
|
|
36
47
|
}
|
|
@@ -40,6 +51,7 @@ export interface FakeGateway {
|
|
|
40
51
|
/** Every request received, in order. */
|
|
41
52
|
requests: RecordedRequest[];
|
|
42
53
|
screenRequests(): RecordedScreenRequest[];
|
|
54
|
+
islandRequests(): RecordedIslandRequest[];
|
|
43
55
|
actionRequests(): RecordedActionRequest[];
|
|
44
56
|
}
|
|
45
57
|
/** A composer whose screens and actions are the handlers given. */
|
|
@@ -53,12 +65,17 @@ export interface InstalledFakeGateway extends FakeGateway {
|
|
|
53
65
|
* app rendered in a test). Call `restore()` after the test.
|
|
54
66
|
*/
|
|
55
67
|
export declare function installFakeGateway(handlers?: FakeGatewayHandlers): InstalledFakeGateway;
|
|
56
|
-
/**
|
|
57
|
-
|
|
68
|
+
/**
|
|
69
|
+
* An island in a screen or island response, typed by its generated descriptor. Give `next` for an
|
|
70
|
+
* island with more pages (spec §4.5).
|
|
71
|
+
*/
|
|
72
|
+
export declare function island<P>(key: string, descriptor: IslandDescriptor<P>, props: P, next?: string): IslandEntry<P>;
|
|
58
73
|
/** An island that failed on the composer (spec §3.4). */
|
|
59
74
|
export declare function islandError(key: string, descriptor: IslandDescriptor<unknown>, error: IslandError): IslandEntry;
|
|
60
75
|
/** A screen response. */
|
|
61
76
|
export declare function screen(id: string, islands: IslandEntry<unknown>[], meta?: ScreenMeta): FakeResponse;
|
|
77
|
+
/** An island response (spec §4.5): a page of one island, or the island resolved again. */
|
|
78
|
+
export declare function islandResponse(screen: string, entry: IslandEntry<unknown>): FakeResponse;
|
|
62
79
|
/** A screen request that fails, e.g. `screenError(404, "not_found")` or `screenError(401, "unauthenticated")`. */
|
|
63
80
|
export declare function screenError(status: number, code: string): FakeResponse;
|
|
64
81
|
/** An action that succeeded, with its result typed by the descriptor. */
|
package/dist/testing.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"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"}
|
package/dist/testing.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// const gateway = createFakeGateway({
|
|
6
6
|
// screens: { profile: () => screen("profile", [island("header", ProfileHeaderV1, { name: "Ada" })]) },
|
|
7
7
|
// actions: { follow: () => actionOk(FollowUserV1, { followers: 8 }) },
|
|
8
|
+
// islands: { "profile/posts": (r) => islandResponse("profile", island("posts", PostsV1, …, "page:3")) },
|
|
8
9
|
// });
|
|
9
10
|
// // pass gateway.fetch as the client's fetch, then assert on gateway.actionRequests()
|
|
10
11
|
//
|
|
@@ -12,13 +13,15 @@
|
|
|
12
13
|
import { PROTOCOL_VERSION, } from "@client-compose/protocol";
|
|
13
14
|
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14
15
|
function strings(value) {
|
|
15
|
-
if (!isRecord(value))
|
|
16
|
+
if (!isRecord(value)) {
|
|
16
17
|
return {};
|
|
18
|
+
}
|
|
17
19
|
return Object.fromEntries(Object.entries(value).filter((e) => typeof e[1] === "string"));
|
|
18
20
|
}
|
|
19
21
|
function capabilities(value) {
|
|
20
|
-
if (!isRecord(value))
|
|
22
|
+
if (!isRecord(value)) {
|
|
21
23
|
return {};
|
|
24
|
+
}
|
|
22
25
|
return Object.fromEntries(Object.entries(value).map(([type, versions]) => [
|
|
23
26
|
type,
|
|
24
27
|
Array.isArray(versions) ? versions.filter((v) => typeof v === "number") : [],
|
|
@@ -27,13 +30,19 @@ function capabilities(value) {
|
|
|
27
30
|
function clientContext(value) {
|
|
28
31
|
if (!isRecord(value) ||
|
|
29
32
|
typeof value.platform !== "string" ||
|
|
30
|
-
typeof value.appVersion !== "string")
|
|
33
|
+
typeof value.appVersion !== "string") {
|
|
31
34
|
return undefined;
|
|
32
|
-
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
...value,
|
|
38
|
+
platform: value.platform,
|
|
39
|
+
appVersion: value.appVersion,
|
|
40
|
+
};
|
|
33
41
|
}
|
|
34
42
|
function parseBody(body) {
|
|
35
|
-
if (typeof body !== "string")
|
|
43
|
+
if (typeof body !== "string") {
|
|
36
44
|
return body;
|
|
45
|
+
}
|
|
37
46
|
try {
|
|
38
47
|
return JSON.parse(body);
|
|
39
48
|
}
|
|
@@ -42,8 +51,9 @@ function parseBody(body) {
|
|
|
42
51
|
}
|
|
43
52
|
}
|
|
44
53
|
function urlOf(input) {
|
|
45
|
-
if (typeof input === "string")
|
|
54
|
+
if (typeof input === "string") {
|
|
46
55
|
return input;
|
|
56
|
+
}
|
|
47
57
|
return input instanceof URL ? input.href : input.url;
|
|
48
58
|
}
|
|
49
59
|
function headersOf(init) {
|
|
@@ -51,10 +61,15 @@ function headersOf(init) {
|
|
|
51
61
|
new Headers(init).forEach((value, name) => (out[name] = value));
|
|
52
62
|
return out;
|
|
53
63
|
}
|
|
64
|
+
function isIslandURL(url) {
|
|
65
|
+
return /\/screens\/[^/]+\/islands\/[^/?]+/.test(url);
|
|
66
|
+
}
|
|
54
67
|
function asScreenRequest(request) {
|
|
55
68
|
const { body } = request;
|
|
56
|
-
|
|
69
|
+
const isScreen = request.url.includes("/screens/") && !isIslandURL(request.url);
|
|
70
|
+
if (!isScreen || !isRecord(body) || typeof body.screen !== "string") {
|
|
57
71
|
return undefined;
|
|
72
|
+
}
|
|
58
73
|
return {
|
|
59
74
|
...request,
|
|
60
75
|
screen: body.screen,
|
|
@@ -63,10 +78,29 @@ function asScreenRequest(request) {
|
|
|
63
78
|
islands: capabilities(body.islands),
|
|
64
79
|
};
|
|
65
80
|
}
|
|
81
|
+
function asIslandRequest(request) {
|
|
82
|
+
const { body } = request;
|
|
83
|
+
if (!isIslandURL(request.url) || !isRecord(body)) {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
if (typeof body.screen !== "string" || typeof body.key !== "string") {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
...request,
|
|
91
|
+
screen: body.screen,
|
|
92
|
+
key: body.key,
|
|
93
|
+
version: typeof body.version === "number" ? body.version : 0,
|
|
94
|
+
params: strings(body.params),
|
|
95
|
+
cursor: typeof body.cursor === "string" ? body.cursor : undefined,
|
|
96
|
+
client: clientContext(body.client),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
66
99
|
function asActionRequest(request) {
|
|
67
100
|
const { body } = request;
|
|
68
|
-
if (!request.url.includes("/actions/") || !isRecord(body) || typeof body.action !== "string")
|
|
101
|
+
if (!request.url.includes("/actions/") || !isRecord(body) || typeof body.action !== "string") {
|
|
69
102
|
return undefined;
|
|
103
|
+
}
|
|
70
104
|
return {
|
|
71
105
|
...request,
|
|
72
106
|
action: body.action,
|
|
@@ -94,22 +128,31 @@ export function createFakeGateway(handlers = {}) {
|
|
|
94
128
|
};
|
|
95
129
|
requests.push(request);
|
|
96
130
|
const other = handlers.other?.(request);
|
|
97
|
-
if (other)
|
|
131
|
+
if (other) {
|
|
98
132
|
return respond(other);
|
|
133
|
+
}
|
|
134
|
+
const islandRequest = asIslandRequest(request);
|
|
135
|
+
const islandHandler = islandRequest && handlers.islands?.[`${islandRequest.screen}/${islandRequest.key}`];
|
|
136
|
+
if (islandRequest && islandHandler) {
|
|
137
|
+
return respond(islandHandler(islandRequest));
|
|
138
|
+
}
|
|
99
139
|
const screenRequest = asScreenRequest(request);
|
|
100
140
|
const screenHandler = screenRequest && handlers.screens?.[screenRequest.screen];
|
|
101
|
-
if (screenRequest && screenHandler)
|
|
141
|
+
if (screenRequest && screenHandler) {
|
|
102
142
|
return respond(screenHandler(screenRequest));
|
|
143
|
+
}
|
|
103
144
|
const actionRequest = asActionRequest(request);
|
|
104
145
|
const actionHandler = actionRequest && handlers.actions?.[actionRequest.action];
|
|
105
|
-
if (actionRequest && actionHandler)
|
|
146
|
+
if (actionRequest && actionHandler) {
|
|
106
147
|
return respond(actionHandler(actionRequest));
|
|
148
|
+
}
|
|
107
149
|
throw new Error(`fake gateway: no handler for ${request.method} ${request.url} ${JSON.stringify(request.body)}`);
|
|
108
150
|
}
|
|
109
151
|
return {
|
|
110
152
|
fetch: fakeFetch,
|
|
111
153
|
requests,
|
|
112
154
|
screenRequests: () => requests.flatMap((r) => asScreenRequest(r) ?? []),
|
|
155
|
+
islandRequests: () => requests.flatMap((r) => asIslandRequest(r) ?? []),
|
|
113
156
|
actionRequests: () => requests.flatMap((r) => asActionRequest(r) ?? []),
|
|
114
157
|
};
|
|
115
158
|
}
|
|
@@ -129,19 +172,51 @@ export function installFakeGateway(handlers = {}) {
|
|
|
129
172
|
};
|
|
130
173
|
}
|
|
131
174
|
// ---- Responses ----
|
|
132
|
-
/**
|
|
133
|
-
|
|
134
|
-
|
|
175
|
+
/**
|
|
176
|
+
* An island in a screen or island response, typed by its generated descriptor. Give `next` for an
|
|
177
|
+
* island with more pages (spec §4.5).
|
|
178
|
+
*/
|
|
179
|
+
export function island(key, descriptor, props, next) {
|
|
180
|
+
return {
|
|
181
|
+
key,
|
|
182
|
+
type: descriptor.type,
|
|
183
|
+
version: descriptor.version,
|
|
184
|
+
status: "ok",
|
|
185
|
+
props,
|
|
186
|
+
...(next !== undefined && { next }),
|
|
187
|
+
};
|
|
135
188
|
}
|
|
136
189
|
/** An island that failed on the composer (spec §3.4). */
|
|
137
190
|
export function islandError(key, descriptor, error) {
|
|
138
|
-
return {
|
|
191
|
+
return {
|
|
192
|
+
key,
|
|
193
|
+
type: descriptor.type,
|
|
194
|
+
version: descriptor.version,
|
|
195
|
+
status: "error",
|
|
196
|
+
error,
|
|
197
|
+
};
|
|
139
198
|
}
|
|
140
199
|
/** A screen response. */
|
|
141
200
|
export function screen(id, islands, meta) {
|
|
142
201
|
return {
|
|
143
202
|
status: 200,
|
|
144
|
-
body: {
|
|
203
|
+
body: {
|
|
204
|
+
protocol: PROTOCOL_VERSION,
|
|
205
|
+
screen: id,
|
|
206
|
+
islands,
|
|
207
|
+
...(meta && { meta }),
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
/** An island response (spec §4.5): a page of one island, or the island resolved again. */
|
|
212
|
+
export function islandResponse(screen, entry) {
|
|
213
|
+
return {
|
|
214
|
+
status: 200,
|
|
215
|
+
body: {
|
|
216
|
+
protocol: PROTOCOL_VERSION,
|
|
217
|
+
screen,
|
|
218
|
+
island: entry,
|
|
219
|
+
},
|
|
145
220
|
};
|
|
146
221
|
}
|
|
147
222
|
/** A screen request that fails, e.g. `screenError(404, "not_found")` or `screenError(401, "unauthenticated")`. */
|
|
@@ -150,13 +225,36 @@ export function screenError(status, code) {
|
|
|
150
225
|
}
|
|
151
226
|
/** An action that succeeded, with its result typed by the descriptor. */
|
|
152
227
|
export function actionOk(action, result, effects) {
|
|
153
|
-
const base = {
|
|
154
|
-
|
|
228
|
+
const base = {
|
|
229
|
+
protocol: PROTOCOL_VERSION,
|
|
230
|
+
action: action.action,
|
|
231
|
+
version: action.version,
|
|
232
|
+
};
|
|
233
|
+
return {
|
|
234
|
+
status: 200,
|
|
235
|
+
body: {
|
|
236
|
+
...base,
|
|
237
|
+
status: "ok",
|
|
238
|
+
result,
|
|
239
|
+
...(effects && { effects }),
|
|
240
|
+
},
|
|
241
|
+
};
|
|
155
242
|
}
|
|
156
243
|
/** An action that ended with one of its declared errors. */
|
|
157
244
|
export function actionError(action, error) {
|
|
158
|
-
const base = {
|
|
159
|
-
|
|
245
|
+
const base = {
|
|
246
|
+
protocol: PROTOCOL_VERSION,
|
|
247
|
+
action: action.action,
|
|
248
|
+
version: action.version,
|
|
249
|
+
};
|
|
250
|
+
return {
|
|
251
|
+
status: 200,
|
|
252
|
+
body: {
|
|
253
|
+
...base,
|
|
254
|
+
status: "error",
|
|
255
|
+
error,
|
|
256
|
+
},
|
|
257
|
+
};
|
|
160
258
|
}
|
|
161
259
|
/** An action the composer didn't perform, e.g. `actionFailure(401, "unauthenticated")` (spec §5.4). */
|
|
162
260
|
export function actionFailure(status, code) {
|
package/dist/testing.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testing.js","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAAA,gGAAgG;AAChG,8FAA8F;AAC9F,yFAAyF;AACzF,EAAE;AACF,0CAA0C;AAC1C,6GAA6G;AAC7G,6EAA6E;AAC7E,UAAU;AACV,2FAA2F;AAC3F,EAAE;AACF,0FAA0F;AAE1F,OAAO,EASL,gBAAgB,GAEjB,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"testing.js","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@client-compose/client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Framework-agnostic Client Compose client: island registry, screen requests and render planning.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"client-compose",
|
|
@@ -44,10 +44,10 @@
|
|
|
44
44
|
"LICENSE"
|
|
45
45
|
],
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@client-compose/protocol": "0.
|
|
47
|
+
"@client-compose/protocol": "0.4.0"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
|
-
"@client-compose/conformance": "0.
|
|
50
|
+
"@client-compose/conformance": "0.7.0"
|
|
51
51
|
},
|
|
52
52
|
"publishConfig": {
|
|
53
53
|
"access": "public"
|
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),
|
|
141
212
|
body: JSON.stringify(request),
|
|
142
213
|
});
|
|
143
|
-
|
|
144
|
-
|
|
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),
|
|
265
|
+
body: JSON.stringify(request),
|
|
266
|
+
});
|
|
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;
|
|
@@ -205,16 +367,18 @@ export async function fetchAction(
|
|
|
205
367
|
const res = await fetchImpl(`${baseUrl}/actions/${encodeURIComponent(request.action)}`, {
|
|
206
368
|
...init,
|
|
207
369
|
method: "POST",
|
|
208
|
-
headers:
|
|
370
|
+
headers: jsonHeaders(init.headers),
|
|
209
371
|
body: JSON.stringify(request),
|
|
210
372
|
});
|
|
211
373
|
const text = await res.text();
|
|
212
|
-
if (!res.ok)
|
|
213
|
-
try {
|
|
214
|
-
return JSON.parse(text) as ActionResponse;
|
|
215
|
-
} catch {
|
|
374
|
+
if (!res.ok) {
|
|
216
375
|
throw new ActionFetchError(res.status, text);
|
|
217
376
|
}
|
|
377
|
+
const response = parseJson(text);
|
|
378
|
+
if (!isActionResponse(response)) {
|
|
379
|
+
throw new ActionFetchError(res.status, text);
|
|
380
|
+
}
|
|
381
|
+
return response;
|
|
218
382
|
}
|
|
219
383
|
|
|
220
384
|
/**
|
|
@@ -226,12 +390,16 @@ export function checkActionResponse<I, R, E extends DeclaredError>(
|
|
|
226
390
|
action: ActionDescriptor<I, R, E>,
|
|
227
391
|
response: unknown,
|
|
228
392
|
): ActionResponse<R, E> {
|
|
229
|
-
const
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
+
}
|
|
235
403
|
throw new ActionFetchError(200, JSON.stringify(response));
|
|
236
404
|
}
|
|
237
405
|
|