@molecule/app-http 1.0.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.
@@ -0,0 +1,298 @@
1
+ /**
2
+ * HTTP client provider implementations.
3
+ *
4
+ * Contains concrete implementations of the HttpClient interface.
5
+ *
6
+ * @module
7
+ */
8
+ import { t } from '@molecule/app-i18n';
9
+ import { getLogger } from '@molecule/app-logger';
10
+ import { HttpError } from './types.js';
11
+ /**
12
+ * Creates a fetch-based HTTP client using the native Fetch API.
13
+ *
14
+ * Supports request/response/error interceptors, automatic JSON
15
+ * serialization, auth token injection, timeout via AbortController,
16
+ * and 401 error handler hooks.
17
+ *
18
+ * @param config - Client configuration including baseURL, default headers, timeout, and credentials.
19
+ * @returns A fully configured `HttpClient` instance.
20
+ */
21
+ export const createFetchClient = (config = {}) => {
22
+ const logger = getLogger('http');
23
+ let authToken = null;
24
+ const requestInterceptors = [];
25
+ const responseInterceptors = [];
26
+ const errorInterceptors = [];
27
+ const authErrorHandlers = new Set();
28
+ // Track whether the document is currently navigating / unloading.
29
+ // Browser tears down in-flight fetch()es on navigation as
30
+ // `TypeError: Failed to fetch`, which is indistinguishable from a
31
+ // real outage at the catch site. The pagehide / beforeunload listeners
32
+ // give us a hint we can use to demote the log severity in that case
33
+ // without hiding genuine network failures.
34
+ let navigationInProgress = false;
35
+ if (typeof window !== 'undefined' && typeof window.addEventListener === 'function') {
36
+ const markNav = () => {
37
+ navigationInProgress = true;
38
+ // Auto-clear shortly after; if the navigation is real, the page
39
+ // will unload and we won't see anything else from this client
40
+ // anyway. If it's a same-page event, this lets us recover.
41
+ setTimeout(() => {
42
+ navigationInProgress = false;
43
+ }, 1000);
44
+ };
45
+ window.addEventListener('beforeunload', markNav);
46
+ window.addEventListener('pagehide', markNav);
47
+ }
48
+ const client = {
49
+ baseURL: config.baseURL || '',
50
+ defaultHeaders: config.defaultHeaders || {},
51
+ async request(requestConfig) {
52
+ // Apply request interceptors
53
+ let finalConfig = { ...requestConfig };
54
+ for (const interceptor of requestInterceptors) {
55
+ finalConfig = await interceptor(finalConfig);
56
+ }
57
+ // Build URL
58
+ let url = finalConfig.url;
59
+ if (client.baseURL && !url.startsWith('http://') && !url.startsWith('https://')) {
60
+ // Guard against double-prefixing: callers (e.g. @molecule/app-billing-react
61
+ // hooks) pass absolute `/api/...` paths whether or not a baseURL is bonded.
62
+ // If the URL already starts with the bonded baseURL, leave it alone.
63
+ const prefix = client.baseURL.endsWith('/') ? client.baseURL : client.baseURL + '/';
64
+ if (!url.startsWith(prefix) && url !== client.baseURL) {
65
+ url = client.baseURL + url;
66
+ }
67
+ }
68
+ // Add query params
69
+ if (finalConfig.params) {
70
+ const params = new URLSearchParams();
71
+ for (const [key, value] of Object.entries(finalConfig.params)) {
72
+ if (value !== undefined) {
73
+ params.append(key, String(value));
74
+ }
75
+ }
76
+ const queryString = params.toString();
77
+ if (queryString) {
78
+ url += (url.includes('?') ? '&' : '?') + queryString;
79
+ }
80
+ }
81
+ // Build headers
82
+ const headers = {
83
+ ...client.defaultHeaders,
84
+ ...finalConfig.headers,
85
+ };
86
+ // Add auth token
87
+ if (authToken) {
88
+ headers['Authorization'] = `Bearer ${authToken}`;
89
+ }
90
+ // Add content-type for JSON
91
+ if (finalConfig.data && !headers['Content-Type']) {
92
+ headers['Content-Type'] = 'application/json';
93
+ }
94
+ // Build fetch options
95
+ const fetchOptions = {
96
+ method: finalConfig.method,
97
+ headers,
98
+ credentials: (finalConfig.withCredentials ?? config.withCredentials) ? 'include' : 'same-origin',
99
+ signal: finalConfig.signal,
100
+ };
101
+ // Add body
102
+ if (finalConfig.data) {
103
+ if (finalConfig.data instanceof FormData || finalConfig.data instanceof Blob) {
104
+ fetchOptions.body = finalConfig.data;
105
+ // Remove Content-Type for FormData (browser sets it with boundary)
106
+ if (finalConfig.data instanceof FormData) {
107
+ delete headers['Content-Type'];
108
+ }
109
+ }
110
+ else {
111
+ fetchOptions.body = JSON.stringify(finalConfig.data);
112
+ }
113
+ }
114
+ // Create abort controller for timeout
115
+ let timeoutId;
116
+ const timeout = finalConfig.timeout ?? config.timeout;
117
+ if (timeout && !finalConfig.signal) {
118
+ const controller = new AbortController();
119
+ fetchOptions.signal = controller.signal;
120
+ timeoutId = setTimeout(() => controller.abort(), timeout);
121
+ }
122
+ try {
123
+ const fetchResponse = await fetch(url, fetchOptions);
124
+ if (timeoutId) {
125
+ clearTimeout(timeoutId);
126
+ }
127
+ // Parse response headers
128
+ const responseHeaders = {};
129
+ fetchResponse.headers.forEach((value, key) => {
130
+ responseHeaders[key] = value;
131
+ });
132
+ // Parse response body
133
+ let data;
134
+ const responseType = finalConfig.responseType || 'json';
135
+ switch (responseType) {
136
+ case 'text':
137
+ data = (await fetchResponse.text());
138
+ break;
139
+ case 'blob':
140
+ data = (await fetchResponse.blob());
141
+ break;
142
+ case 'arraybuffer':
143
+ data = (await fetchResponse.arrayBuffer());
144
+ break;
145
+ case 'json':
146
+ default: {
147
+ const text = await fetchResponse.text();
148
+ try {
149
+ data = text ? JSON.parse(text) : null;
150
+ }
151
+ catch (_error) {
152
+ // Safe to ignore: the raw text is already captured above and used
153
+ // as the fallback value. Non-JSON responses (plain text, HTML errors)
154
+ // are a known valid case for some APIs.
155
+ logger.debug('Response is not JSON, using raw text', url);
156
+ data = text;
157
+ }
158
+ break;
159
+ }
160
+ }
161
+ let response = {
162
+ data,
163
+ status: fetchResponse.status,
164
+ statusText: fetchResponse.statusText,
165
+ headers: responseHeaders,
166
+ config: finalConfig,
167
+ };
168
+ // Handle error status codes
169
+ if (!fetchResponse.ok) {
170
+ logger.warn('HTTP error', finalConfig.method, url, fetchResponse.status);
171
+ const error = new HttpError(t('http.error.requestFailed', { status: fetchResponse.status }, { defaultValue: `Request failed with status ${fetchResponse.status}` }), fetchResponse.status, response, finalConfig, 'http.error.requestFailed');
172
+ // Handle auth errors
173
+ if (fetchResponse.status === 401) {
174
+ authErrorHandlers.forEach((handler) => handler());
175
+ }
176
+ // Apply error interceptors
177
+ let finalError = error;
178
+ for (const interceptor of errorInterceptors) {
179
+ finalError = await interceptor(finalError);
180
+ }
181
+ throw finalError;
182
+ }
183
+ // Apply response interceptors
184
+ for (const interceptor of responseInterceptors) {
185
+ response = (await interceptor(response));
186
+ }
187
+ return response;
188
+ }
189
+ catch (err) {
190
+ if (timeoutId) {
191
+ clearTimeout(timeoutId);
192
+ }
193
+ if (err instanceof HttpError) {
194
+ throw err;
195
+ }
196
+ // Distinguish an explicit abort (AbortController, page unload
197
+ // wired up via signal) and SPA navigation-induced cancellation
198
+ // from a real network outage.
199
+ //
200
+ // - AbortError → user-initiated cancel: silent
201
+ // - "TypeError: Failed to fetch" while the browser tab is
202
+ // hidden / unloading → SPA tore down a fetch as it left the
203
+ // page. Demote to warn so it stays visible in the console
204
+ // for debugging but doesn't fail strict console-error gates.
205
+ // - Anything else (including "Failed to fetch" while the tab
206
+ // is visible) → real outage: logger.error.
207
+ //
208
+ // Components that want zero navigation noise should still pass
209
+ // an AbortSignal from their useEffect cleanup.
210
+ const errMsg = err instanceof Error ? err.message : '';
211
+ const errName = err instanceof Error ? err.name : '';
212
+ const isAbort = errName === 'AbortError' ||
213
+ (typeof DOMException !== 'undefined' &&
214
+ err instanceof DOMException &&
215
+ err.name === 'AbortError') ||
216
+ /the user aborted a request/i.test(errMsg);
217
+ const tabHidden = typeof document !== 'undefined' &&
218
+ (document.visibilityState === 'hidden' || document.hidden === true);
219
+ const isNavCancel = !isAbort &&
220
+ errName === 'TypeError' &&
221
+ /failed to fetch|network ?error|load failed/i.test(errMsg) &&
222
+ (tabHidden || navigationInProgress);
223
+ if (isAbort) {
224
+ // silent — not interesting
225
+ }
226
+ else if (isNavCancel) {
227
+ logger.warn('Network request cancelled by navigation', finalConfig.method, url);
228
+ }
229
+ else {
230
+ logger.error('Network error', finalConfig.method, url, err);
231
+ }
232
+ const error = new HttpError(err instanceof Error
233
+ ? err.message
234
+ : t('http.error.networkError', undefined, { defaultValue: 'Network error' }), 0, undefined, finalConfig, 'http.error.networkError');
235
+ // Apply error interceptors
236
+ let finalError = error;
237
+ for (const interceptor of errorInterceptors) {
238
+ finalError = await interceptor(finalError);
239
+ }
240
+ throw finalError;
241
+ }
242
+ },
243
+ get(url, requestConfig) {
244
+ return client.request({ ...requestConfig, method: 'GET', url });
245
+ },
246
+ post(url, data, requestConfig) {
247
+ return client.request({ ...requestConfig, method: 'POST', url, data });
248
+ },
249
+ put(url, data, requestConfig) {
250
+ return client.request({ ...requestConfig, method: 'PUT', url, data });
251
+ },
252
+ patch(url, data, requestConfig) {
253
+ return client.request({ ...requestConfig, method: 'PATCH', url, data });
254
+ },
255
+ delete(url, requestConfig) {
256
+ return client.request({ ...requestConfig, method: 'DELETE', url });
257
+ },
258
+ addRequestInterceptor(interceptor) {
259
+ requestInterceptors.push(interceptor);
260
+ return () => {
261
+ const index = requestInterceptors.indexOf(interceptor);
262
+ if (index !== -1) {
263
+ requestInterceptors.splice(index, 1);
264
+ }
265
+ };
266
+ },
267
+ addResponseInterceptor(interceptor) {
268
+ responseInterceptors.push(interceptor);
269
+ return () => {
270
+ const index = responseInterceptors.indexOf(interceptor);
271
+ if (index !== -1) {
272
+ responseInterceptors.splice(index, 1);
273
+ }
274
+ };
275
+ },
276
+ addErrorInterceptor(interceptor) {
277
+ errorInterceptors.push(interceptor);
278
+ return () => {
279
+ const index = errorInterceptors.indexOf(interceptor);
280
+ if (index !== -1) {
281
+ errorInterceptors.splice(index, 1);
282
+ }
283
+ };
284
+ },
285
+ setAuthToken(token) {
286
+ authToken = token;
287
+ },
288
+ getAuthToken() {
289
+ return authToken;
290
+ },
291
+ onAuthError(handler) {
292
+ authErrorHandlers.add(handler);
293
+ return () => authErrorHandlers.delete(handler);
294
+ },
295
+ };
296
+ return client;
297
+ };
298
+ //# sourceMappingURL=provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider.js","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,oBAAoB,CAAA;AACtC,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAA;AAKhD,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAEtC;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,SAA2B,EAAE,EAAc,EAAE;IAC7E,MAAM,MAAM,GAAG,SAAS,CAAC,MAAM,CAAC,CAAA;IAChC,IAAI,SAAS,GAAkB,IAAI,CAAA;IACnC,MAAM,mBAAmB,GAAyB,EAAE,CAAA;IACpD,MAAM,oBAAoB,GAA0B,EAAE,CAAA;IACtD,MAAM,iBAAiB,GAAuB,EAAE,CAAA;IAChD,MAAM,iBAAiB,GAAG,IAAI,GAAG,EAAc,CAAA;IAE/C,kEAAkE;IAClE,0DAA0D;IAC1D,kEAAkE;IAClE,uEAAuE;IACvE,oEAAoE;IACpE,2CAA2C;IAC3C,IAAI,oBAAoB,GAAG,KAAK,CAAA;IAChC,IAAI,OAAO,MAAM,KAAK,WAAW,IAAI,OAAO,MAAM,CAAC,gBAAgB,KAAK,UAAU,EAAE,CAAC;QACnF,MAAM,OAAO,GAAG,GAAS,EAAE;YACzB,oBAAoB,GAAG,IAAI,CAAA;YAC3B,gEAAgE;YAChE,8DAA8D;YAC9D,2DAA2D;YAC3D,UAAU,CAAC,GAAG,EAAE;gBACd,oBAAoB,GAAG,KAAK,CAAA;YAC9B,CAAC,EAAE,IAAI,CAAC,CAAA;QACV,CAAC,CAAA;QACD,MAAM,CAAC,gBAAgB,CAAC,cAAc,EAAE,OAAO,CAAC,CAAA;QAChD,MAAM,CAAC,gBAAgB,CAAC,UAAU,EAAE,OAAO,CAAC,CAAA;IAC9C,CAAC;IAED,MAAM,MAAM,GAAe;QACzB,OAAO,EAAE,MAAM,CAAC,OAAO,IAAI,EAAE;QAC7B,cAAc,EAAE,MAAM,CAAC,cAAc,IAAI,EAAE;QAE3C,KAAK,CAAC,OAAO,CAAc,aAAgC;YACzD,6BAA6B;YAC7B,IAAI,WAAW,GAAG,EAAE,GAAG,aAAa,EAAE,CAAA;YACtC,KAAK,MAAM,WAAW,IAAI,mBAAmB,EAAE,CAAC;gBAC9C,WAAW,GAAG,MAAM,WAAW,CAAC,WAAW,CAAC,CAAA;YAC9C,CAAC;YAED,YAAY;YACZ,IAAI,GAAG,GAAG,WAAW,CAAC,GAAG,CAAA;YACzB,IAAI,MAAM,CAAC,OAAO,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;gBAChF,4EAA4E;gBAC5E,4EAA4E;gBAC5E,qEAAqE;gBACrE,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,GAAG,GAAG,CAAA;gBACnF,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,MAAM,CAAC,IAAI,GAAG,KAAK,MAAM,CAAC,OAAO,EAAE,CAAC;oBACtD,GAAG,GAAG,MAAM,CAAC,OAAO,GAAG,GAAG,CAAA;gBAC5B,CAAC;YACH,CAAC;YAED,mBAAmB;YACnB,IAAI,WAAW,CAAC,MAAM,EAAE,CAAC;gBACvB,MAAM,MAAM,GAAG,IAAI,eAAe,EAAE,CAAA;gBACpC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,MAAM,CAAC,EAAE,CAAC;oBAC9D,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;wBACxB,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAA;oBACnC,CAAC;gBACH,CAAC;gBACD,MAAM,WAAW,GAAG,MAAM,CAAC,QAAQ,EAAE,CAAA;gBACrC,IAAI,WAAW,EAAE,CAAC;oBAChB,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,WAAW,CAAA;gBACtD,CAAC;YACH,CAAC;YAED,gBAAgB;YAChB,MAAM,OAAO,GAA2B;gBACtC,GAAG,MAAM,CAAC,cAAc;gBACxB,GAAG,WAAW,CAAC,OAAO;aACvB,CAAA;YAED,iBAAiB;YACjB,IAAI,SAAS,EAAE,CAAC;gBACd,OAAO,CAAC,eAAe,CAAC,GAAG,UAAU,SAAS,EAAE,CAAA;YAClD,CAAC;YAED,4BAA4B;YAC5B,IAAI,WAAW,CAAC,IAAI,IAAI,CAAC,OAAO,CAAC,cAAc,CAAC,EAAE,CAAC;gBACjD,OAAO,CAAC,cAAc,CAAC,GAAG,kBAAkB,CAAA;YAC9C,CAAC;YAED,sBAAsB;YACtB,MAAM,YAAY,GAAgB;gBAChC,MAAM,EAAE,WAAW,CAAC,MAAM;gBAC1B,OAAO;gBACP,WAAW,EACT,CAAC,WAAW,CAAC,eAAe,IAAI,MAAM,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,aAAa;gBACrF,MAAM,EAAE,WAAW,CAAC,MAAM;aAC3B,CAAA;YAED,WAAW;YACX,IAAI,WAAW,CAAC,IAAI,EAAE,CAAC;gBACrB,IAAI,WAAW,CAAC,IAAI,YAAY,QAAQ,IAAI,WAAW,CAAC,IAAI,YAAY,IAAI,EAAE,CAAC;oBAC7E,YAAY,CAAC,IAAI,GAAG,WAAW,CAAC,IAAgB,CAAA;oBAChD,mEAAmE;oBACnE,IAAI,WAAW,CAAC,IAAI,YAAY,QAAQ,EAAE,CAAC;wBACzC,OAAO,OAAO,CAAC,cAAc,CAAC,CAAA;oBAChC,CAAC;gBACH,CAAC;qBAAM,CAAC;oBACN,YAAY,CAAC,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,IAAI,CAAC,CAAA;gBACtD,CAAC;YACH,CAAC;YAED,sCAAsC;YACtC,IAAI,SAAoD,CAAA;YACxD,MAAM,OAAO,GAAG,WAAW,CAAC,OAAO,IAAI,MAAM,CAAC,OAAO,CAAA;YACrD,IAAI,OAAO,IAAI,CAAC,WAAW,CAAC,MAAM,EAAE,CAAC;gBACnC,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAA;gBACxC,YAAY,CAAC,MAAM,GAAG,UAAU,CAAC,MAAM,CAAA;gBACvC,SAAS,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,OAAO,CAAC,CAAA;YAC3D,CAAC;YAED,IAAI,CAAC;gBACH,MAAM,aAAa,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE,YAAY,CAAC,CAAA;gBAEpD,IAAI,SAAS,EAAE,CAAC;oBACd,YAAY,CAAC,SAAS,CAAC,CAAA;gBACzB,CAAC;gBAED,yBAAyB;gBACzB,MAAM,eAAe,GAA2B,EAAE,CAAA;gBAClD,aAAa,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;oBAC3C,eAAe,CAAC,GAAG,CAAC,GAAG,KAAK,CAAA;gBAC9B,CAAC,CAAC,CAAA;gBAEF,sBAAsB;gBACtB,IAAI,IAAO,CAAA;gBACX,MAAM,YAAY,GAAG,WAAW,CAAC,YAAY,IAAI,MAAM,CAAA;gBAEvD,QAAQ,YAAY,EAAE,CAAC;oBACrB,KAAK,MAAM;wBACT,IAAI,GAAG,CAAC,MAAM,aAAa,CAAC,IAAI,EAAE,CAAM,CAAA;wBACxC,MAAK;oBACP,KAAK,MAAM;wBACT,IAAI,GAAG,CAAC,MAAM,aAAa,CAAC,IAAI,EAAE,CAAM,CAAA;wBACxC,MAAK;oBACP,KAAK,aAAa;wBAChB,IAAI,GAAG,CAAC,MAAM,aAAa,CAAC,WAAW,EAAE,CAAM,CAAA;wBAC/C,MAAK;oBACP,KAAK,MAAM,CAAC;oBACZ,OAAO,CAAC,CAAC,CAAC;wBACR,MAAM,IAAI,GAAG,MAAM,aAAa,CAAC,IAAI,EAAE,CAAA;wBACvC,IAAI,CAAC;4BACH,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;wBACvC,CAAC;wBAAC,OAAO,MAAM,EAAE,CAAC;4BAChB,kEAAkE;4BAClE,sEAAsE;4BACtE,wCAAwC;4BACxC,MAAM,CAAC,KAAK,CAAC,sCAAsC,EAAE,GAAG,CAAC,CAAA;4BACzD,IAAI,GAAG,IAAS,CAAA;wBAClB,CAAC;wBACD,MAAK;oBACP,CAAC;gBACH,CAAC;gBAED,IAAI,QAAQ,GAAoB;oBAC9B,IAAI;oBACJ,MAAM,EAAE,aAAa,CAAC,MAAM;oBAC5B,UAAU,EAAE,aAAa,CAAC,UAAU;oBACpC,OAAO,EAAE,eAAe;oBACxB,MAAM,EAAE,WAAW;iBACpB,CAAA;gBAED,4BAA4B;gBAC5B,IAAI,CAAC,aAAa,CAAC,EAAE,EAAE,CAAC;oBACtB,MAAM,CAAC,IAAI,CAAC,YAAY,EAAE,WAAW,CAAC,MAAM,EAAE,GAAG,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;oBACxE,MAAM,KAAK,GAAG,IAAI,SAAS,CACzB,CAAC,CACC,0BAA0B,EAC1B,EAAE,MAAM,EAAE,aAAa,CAAC,MAAM,EAAE,EAChC,EAAE,YAAY,EAAE,8BAA8B,aAAa,CAAC,MAAM,EAAE,EAAE,CACvE,EACD,aAAa,CAAC,MAAM,EACpB,QAAQ,EACR,WAAW,EACX,0BAA0B,CAC3B,CAAA;oBAED,qBAAqB;oBACrB,IAAI,aAAa,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;wBACjC,iBAAiB,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,EAAE,CAAC,CAAA;oBACnD,CAAC;oBAED,2BAA2B;oBAC3B,IAAI,UAAU,GAAG,KAAK,CAAA;oBACtB,KAAK,MAAM,WAAW,IAAI,iBAAiB,EAAE,CAAC;wBAC5C,UAAU,GAAG,MAAM,WAAW,CAAC,UAAU,CAAC,CAAA;oBAC5C,CAAC;oBAED,MAAM,UAAU,CAAA;gBAClB,CAAC;gBAED,8BAA8B;gBAC9B,KAAK,MAAM,WAAW,IAAI,oBAAoB,EAAE,CAAC;oBAC/C,QAAQ,GAAG,CAAC,MAAM,WAAW,CAAC,QAAQ,CAAC,CAAoB,CAAA;gBAC7D,CAAC;gBAED,OAAO,QAAQ,CAAA;YACjB,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,IAAI,SAAS,EAAE,CAAC;oBACd,YAAY,CAAC,SAAS,CAAC,CAAA;gBACzB,CAAC;gBAED,IAAI,GAAG,YAAY,SAAS,EAAE,CAAC;oBAC7B,MAAM,GAAG,CAAA;gBACX,CAAC;gBAED,8DAA8D;gBAC9D,+DAA+D;gBAC/D,8BAA8B;gBAC9B,EAAE;gBACF,gDAAgD;gBAChD,2DAA2D;gBAC3D,+DAA+D;gBAC/D,6DAA6D;gBAC7D,gEAAgE;gBAChE,8DAA8D;gBAC9D,8CAA8C;gBAC9C,EAAE;gBACF,+DAA+D;gBAC/D,+CAA+C;gBAC/C,MAAM,MAAM,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAA;gBACtD,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAA;gBACpD,MAAM,OAAO,GACX,OAAO,KAAK,YAAY;oBACxB,CAAC,OAAO,YAAY,KAAK,WAAW;wBAClC,GAAG,YAAY,YAAY;wBAC3B,GAAG,CAAC,IAAI,KAAK,YAAY,CAAC;oBAC5B,6BAA6B,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;gBAE5C,MAAM,SAAS,GACb,OAAO,QAAQ,KAAK,WAAW;oBAC/B,CAAC,QAAQ,CAAC,eAAe,KAAK,QAAQ,IAAI,QAAQ,CAAC,MAAM,KAAK,IAAI,CAAC,CAAA;gBACrE,MAAM,WAAW,GACf,CAAC,OAAO;oBACR,OAAO,KAAK,WAAW;oBACvB,6CAA6C,CAAC,IAAI,CAAC,MAAM,CAAC;oBAC1D,CAAC,SAAS,IAAI,oBAAoB,CAAC,CAAA;gBAErC,IAAI,OAAO,EAAE,CAAC;oBACZ,2BAA2B;gBAC7B,CAAC;qBAAM,IAAI,WAAW,EAAE,CAAC;oBACvB,MAAM,CAAC,IAAI,CAAC,yCAAyC,EAAE,WAAW,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;gBACjF,CAAC;qBAAM,CAAC;oBACN,MAAM,CAAC,KAAK,CAAC,eAAe,EAAE,WAAW,CAAC,MAAM,EAAE,GAAG,EAAE,GAAG,CAAC,CAAA;gBAC7D,CAAC;gBACD,MAAM,KAAK,GAAG,IAAI,SAAS,CACzB,GAAG,YAAY,KAAK;oBAClB,CAAC,CAAC,GAAG,CAAC,OAAO;oBACb,CAAC,CAAC,CAAC,CAAC,yBAAyB,EAAE,SAAS,EAAE,EAAE,YAAY,EAAE,eAAe,EAAE,CAAC,EAC9E,CAAC,EACD,SAAS,EACT,WAAW,EACX,yBAAyB,CAC1B,CAAA;gBAED,2BAA2B;gBAC3B,IAAI,UAAU,GAAG,KAAK,CAAA;gBACtB,KAAK,MAAM,WAAW,IAAI,iBAAiB,EAAE,CAAC;oBAC5C,UAAU,GAAG,MAAM,WAAW,CAAC,UAAU,CAAC,CAAA;gBAC5C,CAAC;gBAED,MAAM,UAAU,CAAA;YAClB,CAAC;QACH,CAAC;QAED,GAAG,CAAc,GAAW,EAAE,aAA6B;YACzD,OAAO,MAAM,CAAC,OAAO,CAAI,EAAE,GAAG,aAAa,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAA;QACpE,CAAC;QAED,IAAI,CACF,GAAW,EACX,IAAc,EACd,aAA6B;YAE7B,OAAO,MAAM,CAAC,OAAO,CAAI,EAAE,GAAG,aAAa,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,CAAA;QAC3E,CAAC;QAED,GAAG,CACD,GAAW,EACX,IAAc,EACd,aAA6B;YAE7B,OAAO,MAAM,CAAC,OAAO,CAAI,EAAE,GAAG,aAAa,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,CAAA;QAC1E,CAAC;QAED,KAAK,CACH,GAAW,EACX,IAAc,EACd,aAA6B;YAE7B,OAAO,MAAM,CAAC,OAAO,CAAI,EAAE,GAAG,aAAa,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,CAAA;QAC5E,CAAC;QAED,MAAM,CAAc,GAAW,EAAE,aAA6B;YAC5D,OAAO,MAAM,CAAC,OAAO,CAAI,EAAE,GAAG,aAAa,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC,CAAA;QACvE,CAAC;QAED,qBAAqB,CAAC,WAA+B;YACnD,mBAAmB,CAAC,IAAI,CAAC,WAAW,CAAC,CAAA;YACrC,OAAO,GAAG,EAAE;gBACV,MAAM,KAAK,GAAG,mBAAmB,CAAC,OAAO,CAAC,WAAW,CAAC,CAAA;gBACtD,IAAI,KAAK,KAAK,CAAC,CAAC,EAAE,CAAC;oBACjB,mBAAmB,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAA;gBACtC,CAAC;YACH,CAAC,CAAA;QACH,CAAC;QAED,sBAAsB,CAAC,WAAgC;YACrD,oBAAoB,CAAC,IAAI,CAAC,WAAW,CAAC,CAAA;YACtC,OAAO,GAAG,EAAE;gBACV,MAAM,KAAK,GAAG,oBAAoB,CAAC,OAAO,CAAC,WAAW,CAAC,CAAA;gBACvD,IAAI,KAAK,KAAK,CAAC,CAAC,EAAE,CAAC;oBACjB,oBAAoB,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAA;gBACvC,CAAC;YACH,CAAC,CAAA;QACH,CAAC;QAED,mBAAmB,CAAC,WAA6B;YAC/C,iBAAiB,CAAC,IAAI,CAAC,WAAW,CAAC,CAAA;YACnC,OAAO,GAAG,EAAE;gBACV,MAAM,KAAK,GAAG,iBAAiB,CAAC,OAAO,CAAC,WAAW,CAAC,CAAA;gBACpD,IAAI,KAAK,KAAK,CAAC,CAAC,EAAE,CAAC;oBACjB,iBAAiB,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAA;gBACpC,CAAC;YACH,CAAC,CAAA;QACH,CAAC;QAED,YAAY,CAAC,KAAoB;YAC/B,SAAS,GAAG,KAAK,CAAA;QACnB,CAAC;QAED,YAAY;YACV,OAAO,SAAS,CAAA;QAClB,CAAC;QAED,WAAW,CAAC,OAAmB;YAC7B,iBAAiB,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;YAC9B,OAAO,GAAG,EAAE,CAAC,iBAAiB,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;QAChD,CAAC;KACF,CAAA;IAED,OAAO,MAAM,CAAA;AACf,CAAC,CAAA"}
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Core type definitions for the HTTP client.
3
+ *
4
+ * @module
5
+ */
6
+ /**
7
+ * HTTP request method.
8
+ */
9
+ export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';
10
+ /**
11
+ * HTTP request options (headers, query params, timeout, credentials, response type, abort signal).
12
+ */
13
+ export interface RequestConfig {
14
+ /**
15
+ * Request headers.
16
+ */
17
+ headers?: Record<string, string>;
18
+ /**
19
+ * Query parameters.
20
+ */
21
+ params?: Record<string, string | number | boolean | undefined>;
22
+ /**
23
+ * Request timeout in milliseconds.
24
+ */
25
+ timeout?: number;
26
+ /**
27
+ * Whether to include credentials (cookies).
28
+ */
29
+ withCredentials?: boolean;
30
+ /**
31
+ * Response type.
32
+ */
33
+ responseType?: 'json' | 'text' | 'blob' | 'arraybuffer';
34
+ /**
35
+ * Abort signal for cancellation.
36
+ */
37
+ signal?: AbortSignal;
38
+ /**
39
+ * Request body data.
40
+ */
41
+ data?: unknown;
42
+ /**
43
+ * Custom request options (implementation-specific).
44
+ */
45
+ options?: Record<string, unknown>;
46
+ }
47
+ /**
48
+ * Full request configuration including method and URL.
49
+ */
50
+ export interface FullRequestConfig extends RequestConfig {
51
+ /**
52
+ * HTTP method.
53
+ */
54
+ method: HttpMethod;
55
+ /**
56
+ * Request URL (can be relative or absolute).
57
+ */
58
+ url: string;
59
+ /**
60
+ * Request body data.
61
+ */
62
+ data?: unknown;
63
+ }
64
+ /**
65
+ * Parsed HTTP response with status code, headers, and typed body data.
66
+ */
67
+ export interface HttpResponse<T = unknown> {
68
+ /**
69
+ * Response data.
70
+ */
71
+ data: T;
72
+ /**
73
+ * HTTP status code.
74
+ */
75
+ status: number;
76
+ /**
77
+ * HTTP status text.
78
+ */
79
+ statusText: string;
80
+ /**
81
+ * Response headers.
82
+ */
83
+ headers: Record<string, string>;
84
+ /**
85
+ * Original request config.
86
+ */
87
+ config: FullRequestConfig;
88
+ }
89
+ /**
90
+ * HTTP error with response details.
91
+ */
92
+ export declare class HttpError extends Error {
93
+ status: number;
94
+ response?: HttpResponse | undefined;
95
+ config?: FullRequestConfig | undefined;
96
+ errorKey?: string | undefined;
97
+ constructor(message: string, status: number, response?: HttpResponse | undefined, config?: FullRequestConfig | undefined, errorKey?: string | undefined);
98
+ }
99
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,GAAG,SAAS,CAAA;AAEzF;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAEhC;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC,CAAA;IAE9D;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAEhB;;OAEG;IACH,eAAe,CAAC,EAAE,OAAO,CAAA;IAEzB;;OAEG;IACH,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,aAAa,CAAA;IAEvD;;OAEG;IACH,MAAM,CAAC,EAAE,WAAW,CAAA;IAEpB;;OAEG;IACH,IAAI,CAAC,EAAE,OAAO,CAAA;IAEd;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAClC;AAED;;GAEG;AACH,MAAM,WAAW,iBAAkB,SAAQ,aAAa;IACtD;;OAEG;IACH,MAAM,EAAE,UAAU,CAAA;IAElB;;OAEG;IACH,GAAG,EAAE,MAAM,CAAA;IAEX;;OAEG;IACH,IAAI,CAAC,EAAE,OAAO,CAAA;CACf;AAED;;GAEG;AACH,MAAM,WAAW,YAAY,CAAC,CAAC,GAAG,OAAO;IACvC;;OAEG;IACH,IAAI,EAAE,CAAC,CAAA;IAEP;;OAEG;IACH,MAAM,EAAE,MAAM,CAAA;IAEd;;OAEG;IACH,UAAU,EAAE,MAAM,CAAA;IAElB;;OAEG;IACH,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAE/B;;OAEG;IACH,MAAM,EAAE,iBAAiB,CAAA;CAC1B;AAED;;GAEG;AACH,qBAAa,SAAU,SAAQ,KAAK;IAGzB,MAAM,EAAE,MAAM;IACd,QAAQ,CAAC,EAAE,YAAY;IACvB,MAAM,CAAC,EAAE,iBAAiB;IAC1B,QAAQ,CAAC,EAAE,MAAM;gBAJxB,OAAO,EAAE,MAAM,EACR,MAAM,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE,YAAY,YAAA,EACvB,MAAM,CAAC,EAAE,iBAAiB,YAAA,EAC1B,QAAQ,CAAC,EAAE,MAAM,YAAA;CAK3B"}
package/dist/types.js ADDED
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Core type definitions for the HTTP client.
3
+ *
4
+ * @module
5
+ */
6
+ /**
7
+ * HTTP error with response details.
8
+ */
9
+ export class HttpError extends Error {
10
+ status;
11
+ response;
12
+ config;
13
+ errorKey;
14
+ constructor(message, status, response, config, errorKey) {
15
+ super(message);
16
+ this.status = status;
17
+ this.response = response;
18
+ this.config = config;
19
+ this.errorKey = errorKey;
20
+ this.name = 'HttpError';
21
+ }
22
+ }
23
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAsGH;;GAEG;AACH,MAAM,OAAO,SAAU,SAAQ,KAAK;IAGzB;IACA;IACA;IACA;IALT,YACE,OAAe,EACR,MAAc,EACd,QAAuB,EACvB,MAA0B,EAC1B,QAAiB;QAExB,KAAK,CAAC,OAAO,CAAC,CAAA;QALP,WAAM,GAAN,MAAM,CAAQ;QACd,aAAQ,GAAR,QAAQ,CAAe;QACvB,WAAM,GAAN,MAAM,CAAoB;QAC1B,aAAQ,GAAR,QAAQ,CAAS;QAGxB,IAAI,CAAC,IAAI,GAAG,WAAW,CAAA;IACzB,CAAC;CACF"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Response-envelope unwrappers.
3
+ *
4
+ * Apps and mock servers commonly return either a bare value (`T` or
5
+ * `T[]`) or an envelope `{ data: T }` / `{ data: T[] }`. These helpers
6
+ * normalize either shape into a predictable `T[]` or `T | null` for
7
+ * downstream code, eliminating the per-call `'data' in res ? ...` dance.
8
+ *
9
+ * Lifted out of the 59 apps that previously carried a local
10
+ * `lib/unwrap.ts` copy. The two functions cover the response shapes used
11
+ * by the molecule mock server and the typical resource routes — a bare
12
+ * value, a `{ data }` envelope, and (for lists) the RICH pagination
13
+ * envelope `{ data, total, limit, offset }` returned by the pre-built
14
+ * `@molecule/api-resource-*` list endpoints (comment, review,
15
+ * activity-feed, bookmark, …). Other bespoke envelopes are out of scope
16
+ * — handle those in the caller.
17
+ *
18
+ * @module
19
+ */
20
+ /**
21
+ * Normalize an unknown response body into a typed array.
22
+ *
23
+ * Accepts:
24
+ * - a bare array → returned as-is (cast)
25
+ * - `{ data: T[] }` envelope → the inner array
26
+ * - `HttpResponse<T[]>` (i.e. `{ data: T[], status, ... }`) → the inner array
27
+ * - `HttpResponse<{ data: T[] }>` (the response of an envelope-returning
28
+ * endpoint as it arrives from `@molecule/app-http`'s `HttpClient`) → the
29
+ * doubly-nested inner array
30
+ * - `HttpResponse<{ data: T[], total, limit, offset }>` (a RICH pagination
31
+ * envelope from a pre-built `@molecule/api-resource-*` list endpoint) → the
32
+ * inner `data` array (the numeric `total`/`limit`/`offset` make the shape
33
+ * unambiguous, so callers can pass the whole HttpResponse and still get the rows)
34
+ * - anything else → `[]`
35
+ *
36
+ * Callers commonly pass either the raw JSON body (e.g. from `fetch().then(r =>
37
+ * r.json())`) or the `HttpResponse` returned by `useHttpClient().get(...)`.
38
+ * Both shapes are handled here so pages don't have to remember to call
39
+ * `unwrapList(res.data)` vs `unwrapList(res)`.
40
+ *
41
+ * @param res - Raw response body OR an `HttpResponse` envelope from `@molecule/app-http`.
42
+ * @returns A typed array `T[]`; never `null`/`undefined`.
43
+ */
44
+ export declare function unwrapList<T>(res: unknown): T[];
45
+ /**
46
+ * Normalize an unknown response body into a single typed resource.
47
+ *
48
+ * Accepts:
49
+ * - a non-empty plain object → returned as-is (cast)
50
+ * - `{ data: T }` envelope → the inner value
51
+ * - `{ data: null }`, `{ data: undefined }`, `{ data: [] }`, or
52
+ * `{ data: {} }` (mock-server's no-match shape) → `null`
53
+ * - an empty object `{}` → `null`
54
+ * - arrays, primitives, `null`, `undefined` → `null`
55
+ *
56
+ * The "envelope contains an array → null" branch handles the case
57
+ * where the mock server returns `[]` for unmatched endpoints but the
58
+ * caller expects a single resource.
59
+ *
60
+ * @param res - Raw response body (e.g. `HttpResponse.data` from `@molecule/app-http`).
61
+ * @returns The typed resource `T`, or `null` when the response shape
62
+ * indicates "no resource" (including the various empty envelopes
63
+ * above).
64
+ */
65
+ export declare function unwrapSingle<T>(res: unknown): T | null;
66
+ //# sourceMappingURL=unwrap.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unwrap.d.ts","sourceRoot":"","sources":["../src/unwrap.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,UAAU,CAAC,CAAC,EAAE,GAAG,EAAE,OAAO,GAAG,CAAC,EAAE,CAmC/C;AAyED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,GAAG,EAAE,OAAO,GAAG,CAAC,GAAG,IAAI,CAmDtD"}