@client-compose/client 0.1.0 → 0.3.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 +20 -0
- package/dist/index.d.ts +35 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +74 -0
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +106 -0
package/README.md
CHANGED
|
@@ -42,6 +42,26 @@ container.replaceChildren(...islands.map((island) => island.render()));
|
|
|
42
42
|
- **`fetchScreen`** throws `ScreenFetchError` (with `status` and `body`) for non-2xx responses, and
|
|
43
43
|
accepts a custom `fetch` as its fourth argument.
|
|
44
44
|
|
|
45
|
+
## Actions
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import { ActionFetchError, callAction } from "@client-compose/client";
|
|
49
|
+
import { FollowUserV1 } from "./actions.generated"; // from @client-compose/codegen
|
|
50
|
+
|
|
51
|
+
const res = await callAction("https://api.example.com/compose", FollowUserV1, { userId }, {
|
|
52
|
+
client: { platform: "web", appVersion: "1.4.0" },
|
|
53
|
+
init: { headers: { Authorization: `Bearer ${token}` } },
|
|
54
|
+
});
|
|
55
|
+
if (res.status === "ok") showFollowers(res.result.followers); // and reload res.effects?.invalidate
|
|
56
|
+
else if (res.error.code === "blocked") showBlocked();
|
|
57
|
+
else showGenericError(); // codes added after this app was built
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
- Declared errors are outcomes (`status: "error"`), typed per action. Anything else, such as
|
|
61
|
+
`invalid_input`, `unauthenticated` or `action_failed`, throws `ActionFetchError` with `status`
|
|
62
|
+
and `code`.
|
|
63
|
+
- Each call sends a new idempotency key. Pass `idempotencyKey` yourself to retry the same intent.
|
|
64
|
+
|
|
45
65
|
Props aren't validated at runtime; generate types with
|
|
46
66
|
[`@client-compose/codegen`](https://www.npmjs.com/package/@client-compose/codegen) to keep them in
|
|
47
67
|
sync with the composer.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Capabilities, type ClientContext, type IslandDescriptor, type IslandEntry, type ScreenRequest, type ScreenResponse } from "@client-compose/protocol";
|
|
1
|
+
import { type ActionDescriptor, type ActionRequest, type ActionResponse, type Capabilities, type ClientContext, type DeclaredError, type IslandDescriptor, type IslandEntry, type ScreenRequest, type ScreenResponse } from "@client-compose/protocol";
|
|
2
2
|
type OkEntry<P = any> = IslandEntry<P> & {
|
|
3
3
|
status: "ok";
|
|
4
4
|
};
|
|
@@ -31,8 +31,42 @@ export declare function plan<R>(registry: IslandRegistry<R>, response: ScreenRes
|
|
|
31
31
|
export declare class ScreenFetchError extends Error {
|
|
32
32
|
readonly status: number;
|
|
33
33
|
readonly body: string;
|
|
34
|
+
/** The protocol error code from the response body (e.g. "not_found"), if there was one. */
|
|
35
|
+
readonly code: string | undefined;
|
|
34
36
|
constructor(status: number, body: string);
|
|
35
37
|
}
|
|
36
38
|
export declare function fetchScreen(baseUrl: string, request: ScreenRequest, init?: RequestInit, fetchImpl?: typeof fetch): Promise<ScreenResponse>;
|
|
39
|
+
/**
|
|
40
|
+
* The composer didn't perform the action: a non-2xx response such as `invalid_input`,
|
|
41
|
+
* `unauthenticated` or `action_failed`. Declared errors are not thrown; they're outcomes.
|
|
42
|
+
*/
|
|
43
|
+
export declare class ActionFetchError extends Error {
|
|
44
|
+
readonly status: number;
|
|
45
|
+
readonly body: string;
|
|
46
|
+
/** The protocol error code from the response body (e.g. "unauthenticated"), if there was one. */
|
|
47
|
+
readonly code: string | undefined;
|
|
48
|
+
constructor(status: number, body: string);
|
|
49
|
+
}
|
|
50
|
+
/** A fresh idempotency key: one per user intent, reused when retrying it (spec §5.2). */
|
|
51
|
+
export declare function newIdempotencyKey(): string;
|
|
52
|
+
export declare function buildActionRequest<I>(action: ActionDescriptor<I, unknown, DeclaredError>, input: I, client: ClientContext, idempotencyKey?: string): ActionRequest<I>;
|
|
53
|
+
/** POST an action request. Resolves with the outcome (ok or a declared error); throws ActionFetchError otherwise. */
|
|
54
|
+
export declare function fetchAction(baseUrl: string, request: ActionRequest, init?: RequestInit, fetchImpl?: typeof fetch): Promise<ActionResponse>;
|
|
55
|
+
export interface CallActionOptions {
|
|
56
|
+
client: ClientContext;
|
|
57
|
+
/** Default: a new key. Pass the same key again to retry the same user intent. */
|
|
58
|
+
idempotencyKey?: string;
|
|
59
|
+
init?: RequestInit;
|
|
60
|
+
fetch?: typeof fetch;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Perform an action with its generated descriptor:
|
|
64
|
+
*
|
|
65
|
+
* const res = await callAction(baseUrl, FollowUserV1, { userId }, { client });
|
|
66
|
+
* if (res.status === "ok") show(res.result.followers);
|
|
67
|
+
* else if (res.error.code === "blocked") showBlocked();
|
|
68
|
+
* else showGenericError(); // codes added after this app was built
|
|
69
|
+
*/
|
|
70
|
+
export declare function callAction<I, R, E extends DeclaredError>(baseUrl: string, action: ActionDescriptor<I, R, E>, input: I, options: CallActionOptions): Promise<ActionResponse<R, E>>;
|
|
37
71
|
export {};
|
|
38
72
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACrB,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,cAAc,EACpB,MAAM,0BAA0B,CAAC;AAElC,KAAK,OAAO,CAAC,CAAC,GAAG,GAAG,IAAI,WAAW,CAAC,CAAC,CAAC,GAAG;IAAE,MAAM,EAAE,IAAI,CAAA;CAAE,CAAC;AAC1D,KAAK,UAAU,GAAG,WAAW,GAAG;IAAE,MAAM,EAAE,OAAO,CAAA;CAAE,CAAC;AAEpD,MAAM,MAAM,cAAc,CAAC,CAAC,EAAE,CAAC,GAAG,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;AAE5E,qBAAa,cAAc,CAAC,CAAC;IAC3B,OAAO,CAAC,SAAS,CAAqD;IAEtE,wGAAwG;IACxG,QAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,cAAc,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,IAAI;IAC5E,QAAQ,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,IAAI;IAe9E,YAAY,IAAI,YAAY;IAM5B,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,cAAc,CAAC,CAAC,CAAC,GAAG,SAAS;CAGlE;AAED,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,cAAc,CAAC,OAAO,CAAC,EACjC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,aAAa,EACrB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC9B,aAAa,CAEf;AAED,MAAM,WAAW,aAAa,CAAC,CAAC;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC,CAAC;CACjB;AAED,MAAM,MAAM,UAAU,GAAG,gBAAgB,GAAG,gBAAgB,CAAC;AAE7D,MAAM,WAAW,WAAW,CAAC,CAAC;IAC5B,4EAA4E;IAC5E,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,CAAC,GAAG,SAAS,CAAC;IAC/C,oFAAoF;IACpF,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,UAAU,KAAK,IAAI,CAAC;CAC9D;AAED,kEAAkE;AAClE,wBAAgB,IAAI,CAAC,CAAC,EACpB,QAAQ,EAAE,cAAc,CAAC,CAAC,CAAC,EAC3B,QAAQ,EAAE,cAAc,EACxB,OAAO,GAAE,WAAW,CAAC,CAAC,CAAM,GAC3B,aAAa,CAAC,CAAC,CAAC,EAAE,CAoBpB;AAED,qBAAa,gBAAiB,SAAQ,KAAK;IACzC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -48,10 +48,22 @@ export function plan(registry, response, options = {}) {
|
|
|
48
48
|
export class ScreenFetchError extends Error {
|
|
49
49
|
status;
|
|
50
50
|
body;
|
|
51
|
+
/** The protocol error code from the response body (e.g. "not_found"), if there was one. */
|
|
52
|
+
code;
|
|
51
53
|
constructor(status, body) {
|
|
52
54
|
super(`Screen request failed with status ${status}`);
|
|
53
55
|
this.status = status;
|
|
54
56
|
this.body = body;
|
|
57
|
+
this.code = errorCode(body);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function errorCode(body) {
|
|
61
|
+
try {
|
|
62
|
+
const code = JSON.parse(body).error?.code;
|
|
63
|
+
return typeof code === "string" ? code : undefined;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return undefined;
|
|
55
67
|
}
|
|
56
68
|
}
|
|
57
69
|
export async function fetchScreen(baseUrl, request, init = {}, fetchImpl = fetch) {
|
|
@@ -65,4 +77,66 @@ export async function fetchScreen(baseUrl, request, init = {}, fetchImpl = fetch
|
|
|
65
77
|
throw new ScreenFetchError(res.status, await res.text());
|
|
66
78
|
return (await res.json());
|
|
67
79
|
}
|
|
80
|
+
// ---- Actions (spec §5) ----
|
|
81
|
+
/**
|
|
82
|
+
* The composer didn't perform the action: a non-2xx response such as `invalid_input`,
|
|
83
|
+
* `unauthenticated` or `action_failed`. Declared errors are not thrown; they're outcomes.
|
|
84
|
+
*/
|
|
85
|
+
export class ActionFetchError extends Error {
|
|
86
|
+
status;
|
|
87
|
+
body;
|
|
88
|
+
/** The protocol error code from the response body (e.g. "unauthenticated"), if there was one. */
|
|
89
|
+
code;
|
|
90
|
+
constructor(status, body) {
|
|
91
|
+
super(`Action request failed with status ${status}`);
|
|
92
|
+
this.status = status;
|
|
93
|
+
this.body = body;
|
|
94
|
+
this.code = errorCode(body);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/** A fresh idempotency key: one per user intent, reused when retrying it (spec §5.2). */
|
|
98
|
+
export function newIdempotencyKey() {
|
|
99
|
+
const c = globalThis.crypto;
|
|
100
|
+
if (c?.randomUUID)
|
|
101
|
+
return c.randomUUID();
|
|
102
|
+
// React Native's Hermes has no crypto.randomUUID. Uniqueness, not secrecy, is what matters here.
|
|
103
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (ch) => {
|
|
104
|
+
const r = (Math.random() * 16) | 0;
|
|
105
|
+
return (ch === "x" ? r : (r & 0x3) | 0x8).toString(16);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
export function buildActionRequest(action, input, client, idempotencyKey) {
|
|
109
|
+
return {
|
|
110
|
+
protocol: PROTOCOL_VERSION,
|
|
111
|
+
action: action.action,
|
|
112
|
+
version: action.version,
|
|
113
|
+
input,
|
|
114
|
+
client,
|
|
115
|
+
...(idempotencyKey !== undefined && { idempotencyKey }),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/** POST an action request. Resolves with the outcome (ok or a declared error); throws ActionFetchError otherwise. */
|
|
119
|
+
export async function fetchAction(baseUrl, request, init = {}, fetchImpl = fetch) {
|
|
120
|
+
const res = await fetchImpl(`${baseUrl}/actions/${encodeURIComponent(request.action)}`, {
|
|
121
|
+
...init,
|
|
122
|
+
method: "POST",
|
|
123
|
+
headers: { "content-type": "application/json", ...init.headers },
|
|
124
|
+
body: JSON.stringify(request),
|
|
125
|
+
});
|
|
126
|
+
if (!res.ok)
|
|
127
|
+
throw new ActionFetchError(res.status, await res.text());
|
|
128
|
+
return (await res.json());
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Perform an action with its generated descriptor:
|
|
132
|
+
*
|
|
133
|
+
* const res = await callAction(baseUrl, FollowUserV1, { userId }, { client });
|
|
134
|
+
* if (res.status === "ok") show(res.result.followers);
|
|
135
|
+
* else if (res.error.code === "blocked") showBlocked();
|
|
136
|
+
* else showGenericError(); // codes added after this app was built
|
|
137
|
+
*/
|
|
138
|
+
export async function callAction(baseUrl, action, input, options) {
|
|
139
|
+
const request = buildActionRequest(action, input, options.client, options.idempotencyKey ?? newIdempotencyKey());
|
|
140
|
+
return (await fetchAction(baseUrl, request, options.init, options.fetch));
|
|
141
|
+
}
|
|
68
142
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,kFAAkF;AAElF,OAAO,EACL,gBAAgB,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.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.3.0",
|
|
4
4
|
"description": "Framework-agnostic Client Compose client: island registry, screen requests and render planning.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"client-compose",
|
|
@@ -36,10 +36,10 @@
|
|
|
36
36
|
"LICENSE"
|
|
37
37
|
],
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@client-compose/protocol": "0.
|
|
39
|
+
"@client-compose/protocol": "0.3.0"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@client-compose/conformance": "0.
|
|
42
|
+
"@client-compose/conformance": "0.4.0"
|
|
43
43
|
},
|
|
44
44
|
"publishConfig": {
|
|
45
45
|
"access": "public"
|
package/src/index.ts
CHANGED
|
@@ -3,8 +3,12 @@
|
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
5
|
PROTOCOL_VERSION,
|
|
6
|
+
type ActionDescriptor,
|
|
7
|
+
type ActionRequest,
|
|
8
|
+
type ActionResponse,
|
|
6
9
|
type Capabilities,
|
|
7
10
|
type ClientContext,
|
|
11
|
+
type DeclaredError,
|
|
8
12
|
type IslandDescriptor,
|
|
9
13
|
type IslandEntry,
|
|
10
14
|
type ScreenRequest,
|
|
@@ -100,11 +104,23 @@ export function plan<R>(
|
|
|
100
104
|
export class ScreenFetchError extends Error {
|
|
101
105
|
readonly status: number;
|
|
102
106
|
readonly body: string;
|
|
107
|
+
/** The protocol error code from the response body (e.g. "not_found"), if there was one. */
|
|
108
|
+
readonly code: string | undefined;
|
|
103
109
|
|
|
104
110
|
constructor(status: number, body: string) {
|
|
105
111
|
super(`Screen request failed with status ${status}`);
|
|
106
112
|
this.status = status;
|
|
107
113
|
this.body = body;
|
|
114
|
+
this.code = errorCode(body);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function errorCode(body: string): string | undefined {
|
|
119
|
+
try {
|
|
120
|
+
const code = (JSON.parse(body) as { error?: { code?: unknown } }).error?.code;
|
|
121
|
+
return typeof code === "string" ? code : undefined;
|
|
122
|
+
} catch {
|
|
123
|
+
return undefined;
|
|
108
124
|
}
|
|
109
125
|
}
|
|
110
126
|
|
|
@@ -123,3 +139,93 @@ export async function fetchScreen(
|
|
|
123
139
|
if (!res.ok) throw new ScreenFetchError(res.status, await res.text());
|
|
124
140
|
return (await res.json()) as ScreenResponse;
|
|
125
141
|
}
|
|
142
|
+
|
|
143
|
+
// ---- Actions (spec §5) ----
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* The composer didn't perform the action: a non-2xx response such as `invalid_input`,
|
|
147
|
+
* `unauthenticated` or `action_failed`. Declared errors are not thrown; they're outcomes.
|
|
148
|
+
*/
|
|
149
|
+
export class ActionFetchError extends Error {
|
|
150
|
+
readonly status: number;
|
|
151
|
+
readonly body: string;
|
|
152
|
+
/** The protocol error code from the response body (e.g. "unauthenticated"), if there was one. */
|
|
153
|
+
readonly code: string | undefined;
|
|
154
|
+
|
|
155
|
+
constructor(status: number, body: string) {
|
|
156
|
+
super(`Action request failed with status ${status}`);
|
|
157
|
+
this.status = status;
|
|
158
|
+
this.body = body;
|
|
159
|
+
this.code = errorCode(body);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** A fresh idempotency key: one per user intent, reused when retrying it (spec §5.2). */
|
|
164
|
+
export function newIdempotencyKey(): string {
|
|
165
|
+
const c = (globalThis as { crypto?: { randomUUID?: () => string } }).crypto;
|
|
166
|
+
if (c?.randomUUID) return c.randomUUID();
|
|
167
|
+
// React Native's Hermes has no crypto.randomUUID. Uniqueness, not secrecy, is what matters here.
|
|
168
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (ch) => {
|
|
169
|
+
const r = (Math.random() * 16) | 0;
|
|
170
|
+
return (ch === "x" ? r : (r & 0x3) | 0x8).toString(16);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function buildActionRequest<I>(
|
|
175
|
+
action: ActionDescriptor<I, unknown, DeclaredError>,
|
|
176
|
+
input: I,
|
|
177
|
+
client: ClientContext,
|
|
178
|
+
idempotencyKey?: string,
|
|
179
|
+
): ActionRequest<I> {
|
|
180
|
+
return {
|
|
181
|
+
protocol: PROTOCOL_VERSION,
|
|
182
|
+
action: action.action,
|
|
183
|
+
version: action.version,
|
|
184
|
+
input,
|
|
185
|
+
client,
|
|
186
|
+
...(idempotencyKey !== undefined && { idempotencyKey }),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** POST an action request. Resolves with the outcome (ok or a declared error); throws ActionFetchError otherwise. */
|
|
191
|
+
export async function fetchAction(
|
|
192
|
+
baseUrl: string,
|
|
193
|
+
request: ActionRequest,
|
|
194
|
+
init: RequestInit = {},
|
|
195
|
+
fetchImpl: typeof fetch = fetch,
|
|
196
|
+
): Promise<ActionResponse> {
|
|
197
|
+
const res = await fetchImpl(`${baseUrl}/actions/${encodeURIComponent(request.action)}`, {
|
|
198
|
+
...init,
|
|
199
|
+
method: "POST",
|
|
200
|
+
headers: { "content-type": "application/json", ...init.headers },
|
|
201
|
+
body: JSON.stringify(request),
|
|
202
|
+
});
|
|
203
|
+
if (!res.ok) throw new ActionFetchError(res.status, await res.text());
|
|
204
|
+
return (await res.json()) as ActionResponse;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export interface CallActionOptions {
|
|
208
|
+
client: ClientContext;
|
|
209
|
+
/** Default: a new key. Pass the same key again to retry the same user intent. */
|
|
210
|
+
idempotencyKey?: string;
|
|
211
|
+
init?: RequestInit;
|
|
212
|
+
fetch?: typeof fetch;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Perform an action with its generated descriptor:
|
|
217
|
+
*
|
|
218
|
+
* const res = await callAction(baseUrl, FollowUserV1, { userId }, { client });
|
|
219
|
+
* if (res.status === "ok") show(res.result.followers);
|
|
220
|
+
* else if (res.error.code === "blocked") showBlocked();
|
|
221
|
+
* else showGenericError(); // codes added after this app was built
|
|
222
|
+
*/
|
|
223
|
+
export async function callAction<I, R, E extends DeclaredError>(
|
|
224
|
+
baseUrl: string,
|
|
225
|
+
action: ActionDescriptor<I, R, E>,
|
|
226
|
+
input: I,
|
|
227
|
+
options: CallActionOptions,
|
|
228
|
+
): Promise<ActionResponse<R, E>> {
|
|
229
|
+
const request = buildActionRequest(action, input, options.client, options.idempotencyKey ?? newIdempotencyKey());
|
|
230
|
+
return (await fetchAction(baseUrl, request, options.init, options.fetch)) as ActionResponse<R, E>;
|
|
231
|
+
}
|