@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 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
@@ -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;gBAEV,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM;CAKzC;AAED,wBAAsB,WAAW,CAC/B,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,aAAa,EACtB,IAAI,GAAE,WAAgB,EACtB,SAAS,GAAE,OAAO,KAAa,GAC9B,OAAO,CAAC,cAAc,CAAC,CASzB"}
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":"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"}
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.1.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.1.0"
39
+ "@client-compose/protocol": "0.3.0"
40
40
  },
41
41
  "devDependencies": {
42
- "@client-compose/conformance": "0.1.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
+ }