@gusnips/react 0.6.2 → 0.8.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/dist/api-client.d.ts +31 -2
- package/dist/api-client.d.ts.map +1 -1
- package/dist/api-client.js +2 -1
- package/dist/api-client.js.map +1 -1
- package/package.json +1 -1
- package/src/api-client.test.ts +62 -0
- package/src/api-client.ts +34 -3
package/dist/api-client.d.ts
CHANGED
|
@@ -80,8 +80,20 @@ export interface ApiClientOptions {
|
|
|
80
80
|
*
|
|
81
81
|
* Observation only: the error is thrown either way, and throwing from here would replace a
|
|
82
82
|
* real API failure with whatever the listener hit.
|
|
83
|
+
*
|
|
84
|
+
* **The response comes with it, because a refusal's headers are otherwise unreachable.**
|
|
85
|
+
* `request` throws before the `Response` escapes, so an adopter whose API states something on
|
|
86
|
+
* the error path had no way to read it at all — one reports what every call cost, refusals
|
|
87
|
+
* included, which is how "a blocked page costs nothing" becomes a thing a customer can check
|
|
88
|
+
* rather than a thing we assert. That is the distinction worth keeping: field-error flattening
|
|
89
|
+
* and a per-call meter callback both stayed in their products, because a product can write
|
|
90
|
+
* those itself. This one it cannot, at any price, and a capability the kit makes unreachable
|
|
91
|
+
* is a regression the kit has to undo.
|
|
92
|
+
*
|
|
93
|
+
* The BODY has already been read by the time this runs — the envelope was needed to build the
|
|
94
|
+
* error — so `response.json()` here throws. The headers are what is left, and what this is for.
|
|
83
95
|
*/
|
|
84
|
-
onError?: (error: ApiError) => void;
|
|
96
|
+
onError?: (error: ApiError, response: Response) => void;
|
|
85
97
|
/**
|
|
86
98
|
* Abort a request that has not answered. Default 30s.
|
|
87
99
|
*
|
|
@@ -132,8 +144,25 @@ export interface ApiClient {
|
|
|
132
144
|
del(path: string, options?: RequestOptions): Promise<void>;
|
|
133
145
|
/** The DELETE that answers with something worth reading — a queue entry it handed back. */
|
|
134
146
|
delJson<T>(path: string, options?: RequestOptions): Promise<T>;
|
|
135
|
-
/**
|
|
147
|
+
/**
|
|
148
|
+
* The whole envelope rather than just `data`, for a route whose `meta` is half the answer —
|
|
149
|
+
* a list's `total`, or a metered call's receipt.
|
|
150
|
+
*/
|
|
136
151
|
page<T, M>(path: string, options?: RequestOptions): Promise<ApiSuccess<T, M>>;
|
|
152
|
+
/**
|
|
153
|
+
* The same, off a POST.
|
|
154
|
+
*
|
|
155
|
+
* Two adopters needed this and both wrote the same workaround: `client.request(…)` followed by
|
|
156
|
+
* a hand-rolled `res.json()`, stepping around the client's own envelope reader and its one
|
|
157
|
+
* assertion about the success shape. One of them left the reason sitting in a comment — "the
|
|
158
|
+
* whole envelope is reachable off a GET and not off a POST, so this one reads the body itself"
|
|
159
|
+
* — which is this package describing its own gap in somebody else's file.
|
|
160
|
+
*
|
|
161
|
+
* A metered route takes a body whenever its input is too long or too punctuated to be a path
|
|
162
|
+
* segment (a URL, a search query), and then the receipt has to survive the POST too. When the
|
|
163
|
+
* same guard shows up in every adopter, the thing it guards against is ours.
|
|
164
|
+
*/
|
|
165
|
+
postPage<T, M>(path: string, body?: unknown, options?: RequestOptions): Promise<ApiSuccess<T, M>>;
|
|
137
166
|
}
|
|
138
167
|
export declare function createApiClient({ baseUrl, session, headers: extraHeaders, onSessionDead, onError, timeoutMs, maxRefreshAttempts, refreshRetryDelayMs, signOutTimeoutMs, requestIdHeader, }: ApiClientOptions): ApiClient;
|
|
139
168
|
//# sourceMappingURL=api-client.d.ts.map
|
package/dist/api-client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-client.d.ts","sourceRoot":"","sources":["../src/api-client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,UAAU,EAAE,MAAM,eAAe,CAAC;AAC5D,OAAO,EAAE,QAAQ,EAAmB,MAAM,gBAAgB,CAAC;AAE3D;;;;;;;;;;;;;;;;;;GAkBG;AAEH,+CAA+C;AAC/C,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB;;;;;;;;;OASG;IACH,WAAW,EAAE,OAAO,CAAC;CACtB;AAED;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACnC,OAAO,IAAI,OAAO,CAAC,aAAa,CAAC,CAAC;IAClC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAED,MAAM,WAAW,gBAAgB;IAC/B,6EAA6E;IAC7E,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,cAAc,CAAC;IACxB;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvC;;;;;;;OAOG;IACH,aAAa,EAAE,MAAM,IAAI,CAAC;IAC1B
|
|
1
|
+
{"version":3,"file":"api-client.d.ts","sourceRoot":"","sources":["../src/api-client.ts"],"names":[],"mappings":"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"}
|
package/dist/api-client.js
CHANGED
|
@@ -150,7 +150,7 @@ export function createApiClient({ baseUrl, session, headers: extraHeaders, onSes
|
|
|
150
150
|
// the real news, and swallowing it for a bug in a side effect would send everyone
|
|
151
151
|
// debugging the wrong thing.
|
|
152
152
|
try {
|
|
153
|
-
onError?.(error);
|
|
153
|
+
onError?.(error, res);
|
|
154
154
|
}
|
|
155
155
|
catch {
|
|
156
156
|
// ignored on purpose — see above
|
|
@@ -187,6 +187,7 @@ export function createApiClient({ baseUrl, session, headers: extraHeaders, onSes
|
|
|
187
187
|
},
|
|
188
188
|
delJson: (path, options = {}) => data(path, { ...options, method: "DELETE" }),
|
|
189
189
|
page: async (path, options = {}) => envelope(await request(path, { ...options, method: "GET" })),
|
|
190
|
+
postPage: async (path, body, options = {}) => envelope(await request(path, json(body, { ...options, method: "POST" }))),
|
|
190
191
|
};
|
|
191
192
|
}
|
|
192
193
|
//# sourceMappingURL=api-client.js.map
|
package/dist/api-client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-client.js","sourceRoot":"","sources":["../src/api-client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAmB,MAAM,eAAe,CAAC;AAC5D,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"api-client.js","sourceRoot":"","sources":["../src/api-client.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gusnips/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "The headless runtime under a Vite + React SPA: typed fetch client with single-flight refresh, auth store, query defaults, route guards, error boundary, stale-tab recovery, i18n bootstrap.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/api-client.test.ts
CHANGED
|
@@ -272,6 +272,41 @@ describe("createApiClient", () => {
|
|
|
272
272
|
expect(fetched).toHaveBeenCalledTimes(2);
|
|
273
273
|
});
|
|
274
274
|
|
|
275
|
+
/**
|
|
276
|
+
* The metered POST. `post` keeps only `data`, and on a metered route the `meta` beside it is
|
|
277
|
+
* half the answer — what the call cost, whether a cache hit made it free. Two adopters wanted
|
|
278
|
+
* that off a POST (the input is a URL or a search query, too long and too punctuated for a path
|
|
279
|
+
* segment) and both reached past the client for `res.json()` to get it.
|
|
280
|
+
*/
|
|
281
|
+
it("returns the whole envelope off a POST, with the body serialized", async () => {
|
|
282
|
+
const body = { data: { markdown: "# hi" }, meta: { cost: 0, cached: true } };
|
|
283
|
+
const fetched = stubFetch(
|
|
284
|
+
() =>
|
|
285
|
+
new Response(JSON.stringify(body), {
|
|
286
|
+
status: 200,
|
|
287
|
+
headers: { "Content-Type": "application/json" },
|
|
288
|
+
}),
|
|
289
|
+
);
|
|
290
|
+
const api = createApiClient({
|
|
291
|
+
baseUrl: "https://api.test",
|
|
292
|
+
session: session(),
|
|
293
|
+
onSessionDead: () => {},
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
await expect(
|
|
297
|
+
api.postPage<{ markdown: string }, { cost: number; cached: boolean }>("/v1/scrape", {
|
|
298
|
+
url: "https://example.com",
|
|
299
|
+
}),
|
|
300
|
+
).resolves.toEqual(body);
|
|
301
|
+
|
|
302
|
+
const init = fetched.mock.calls[0]?.[1] as RequestInit;
|
|
303
|
+
expect(init.method).toBe("POST");
|
|
304
|
+
// Serialized and typed by the client, exactly as `post` does it — the half a hand-rolled
|
|
305
|
+
// `request()` call has to remember, and the reason this is not just `page` with a method.
|
|
306
|
+
expect(init.body).toBe(JSON.stringify({ url: "https://example.com" }));
|
|
307
|
+
expect(new Headers(init.headers).get("Content-Type")).toBe("application/json");
|
|
308
|
+
});
|
|
309
|
+
|
|
275
310
|
it("reports a failure to onError once, and still throws it", async () => {
|
|
276
311
|
stubFetch(
|
|
277
312
|
() =>
|
|
@@ -301,6 +336,33 @@ describe("createApiClient", () => {
|
|
|
301
336
|
expect(thrown).toBe(seen[0]);
|
|
302
337
|
});
|
|
303
338
|
|
|
339
|
+
/**
|
|
340
|
+
* A refusal's headers reach nobody otherwise: `request` throws before the `Response` escapes.
|
|
341
|
+
* One adopter's API reports what every call cost on the error path too — `x-request-cost: 0`
|
|
342
|
+
* on a page that was walled — which is the whole promise "a blocked page costs nothing" made
|
|
343
|
+
* checkable. Without the response here, adopting this client would have deleted that screen.
|
|
344
|
+
*/
|
|
345
|
+
it("hands the failed response to onError, headers and all", async () => {
|
|
346
|
+
stubFetch(
|
|
347
|
+
() =>
|
|
348
|
+
new Response(JSON.stringify({ error: { code: "UPSTREAM_BLOCKED", message: "walled" } }), {
|
|
349
|
+
status: 422,
|
|
350
|
+
headers: { "x-request-cost": "0" },
|
|
351
|
+
}),
|
|
352
|
+
);
|
|
353
|
+
const seen: { code: string | undefined; cost: string | null }[] = [];
|
|
354
|
+
const api = createApiClient({
|
|
355
|
+
baseUrl: "https://api.test",
|
|
356
|
+
session: session(),
|
|
357
|
+
onSessionDead: () => {},
|
|
358
|
+
onError: (error, response) =>
|
|
359
|
+
seen.push({ code: error.code, cost: response.headers.get("x-request-cost") }),
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
await api.get("/v1/scrape").catch(() => {});
|
|
363
|
+
expect(seen).toEqual([{ code: "UPSTREAM_BLOCKED", cost: "0" }]);
|
|
364
|
+
});
|
|
365
|
+
|
|
304
366
|
it("does not let a broken listener replace the API failure", async () => {
|
|
305
367
|
stubFetch(
|
|
306
368
|
() =>
|
package/src/api-client.ts
CHANGED
|
@@ -84,8 +84,20 @@ export interface ApiClientOptions {
|
|
|
84
84
|
*
|
|
85
85
|
* Observation only: the error is thrown either way, and throwing from here would replace a
|
|
86
86
|
* real API failure with whatever the listener hit.
|
|
87
|
+
*
|
|
88
|
+
* **The response comes with it, because a refusal's headers are otherwise unreachable.**
|
|
89
|
+
* `request` throws before the `Response` escapes, so an adopter whose API states something on
|
|
90
|
+
* the error path had no way to read it at all — one reports what every call cost, refusals
|
|
91
|
+
* included, which is how "a blocked page costs nothing" becomes a thing a customer can check
|
|
92
|
+
* rather than a thing we assert. That is the distinction worth keeping: field-error flattening
|
|
93
|
+
* and a per-call meter callback both stayed in their products, because a product can write
|
|
94
|
+
* those itself. This one it cannot, at any price, and a capability the kit makes unreachable
|
|
95
|
+
* is a regression the kit has to undo.
|
|
96
|
+
*
|
|
97
|
+
* The BODY has already been read by the time this runs — the envelope was needed to build the
|
|
98
|
+
* error — so `response.json()` here throws. The headers are what is left, and what this is for.
|
|
87
99
|
*/
|
|
88
|
-
onError?: (error: ApiError) => void;
|
|
100
|
+
onError?: (error: ApiError, response: Response) => void;
|
|
89
101
|
/**
|
|
90
102
|
* Abort a request that has not answered. Default 30s.
|
|
91
103
|
*
|
|
@@ -138,8 +150,25 @@ export interface ApiClient {
|
|
|
138
150
|
del(path: string, options?: RequestOptions): Promise<void>;
|
|
139
151
|
/** The DELETE that answers with something worth reading — a queue entry it handed back. */
|
|
140
152
|
delJson<T>(path: string, options?: RequestOptions): Promise<T>;
|
|
141
|
-
/**
|
|
153
|
+
/**
|
|
154
|
+
* The whole envelope rather than just `data`, for a route whose `meta` is half the answer —
|
|
155
|
+
* a list's `total`, or a metered call's receipt.
|
|
156
|
+
*/
|
|
142
157
|
page<T, M>(path: string, options?: RequestOptions): Promise<ApiSuccess<T, M>>;
|
|
158
|
+
/**
|
|
159
|
+
* The same, off a POST.
|
|
160
|
+
*
|
|
161
|
+
* Two adopters needed this and both wrote the same workaround: `client.request(…)` followed by
|
|
162
|
+
* a hand-rolled `res.json()`, stepping around the client's own envelope reader and its one
|
|
163
|
+
* assertion about the success shape. One of them left the reason sitting in a comment — "the
|
|
164
|
+
* whole envelope is reachable off a GET and not off a POST, so this one reads the body itself"
|
|
165
|
+
* — which is this package describing its own gap in somebody else's file.
|
|
166
|
+
*
|
|
167
|
+
* A metered route takes a body whenever its input is too long or too punctuated to be a path
|
|
168
|
+
* segment (a URL, a search query), and then the receipt has to survive the POST too. When the
|
|
169
|
+
* same guard shows up in every adopter, the thing it guards against is ours.
|
|
170
|
+
*/
|
|
171
|
+
postPage<T, M>(path: string, body?: unknown, options?: RequestOptions): Promise<ApiSuccess<T, M>>;
|
|
143
172
|
}
|
|
144
173
|
|
|
145
174
|
const sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));
|
|
@@ -317,7 +346,7 @@ export function createApiClient({
|
|
|
317
346
|
// the real news, and swallowing it for a bug in a side effect would send everyone
|
|
318
347
|
// debugging the wrong thing.
|
|
319
348
|
try {
|
|
320
|
-
onError?.(error);
|
|
349
|
+
onError?.(error, res);
|
|
321
350
|
} catch {
|
|
322
351
|
// ignored on purpose — see above
|
|
323
352
|
}
|
|
@@ -364,5 +393,7 @@ export function createApiClient({
|
|
|
364
393
|
data<T>(path, { ...options, method: "DELETE" }),
|
|
365
394
|
page: async <T, M>(path: string, options: RequestOptions = {}) =>
|
|
366
395
|
envelope<T, M>(await request(path, { ...options, method: "GET" })),
|
|
396
|
+
postPage: async <T, M>(path: string, body?: unknown, options: RequestOptions = {}) =>
|
|
397
|
+
envelope<T, M>(await request(path, json(body, { ...options, method: "POST" }))),
|
|
367
398
|
};
|
|
368
399
|
}
|