@hkdigital/lib-sveltekit 0.1.67 → 0.1.69

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.
@@ -11,284 +11,359 @@ import { toURL } from './url.js';
11
11
  import { setRequestHeaders } from './headers.js';
12
12
  import { waitForAndCheckResponse } from './response.js';
13
13
 
14
- /**
15
- * @callback requestHandler
16
- * @param {Object} _
17
- * @param {AbortController} _.controller
18
- * @param {( reason?: Error ) => void} _.abort
19
- * @param {( delayMs: number) => void} _.timeout
20
- */
14
+ import { getCachedResponse, storeResponseInCache } from './caching.js';
21
15
 
22
16
  /**
23
- * Make GET request
17
+ * Default configuration for HTTP requests
24
18
  *
25
- * @param {object} _
19
+ * This object contains default settings used by the HTTP request functions.
20
+ * It can be used as a reference for available options and their default values.
26
21
  *
27
- * @param {string|URL} _.url - Url string or URL object
22
+ * @type {Object}
23
+ */
24
+ export const DEFAULT_HTTP_CONFIG = {
25
+ // Request
26
+ method: METHOD_GET,
27
+ urlSearchParams: null,
28
+ body: null,
29
+ headers: null,
30
+ withCredentials: false,
31
+ timeoutMs: null, // No timeout by default
32
+
33
+ // Fetch
34
+ mode: 'cors',
35
+ cache: 'no-cache',
36
+ redirect: 'follow',
37
+ referrerPolicy: 'no-referrer',
38
+
39
+ // Cache
40
+ cacheEnabled: true
41
+ };
42
+
43
+ /**
44
+ * Make a GET request
28
45
  *
29
- * @param {object} [_.urlSearchParams]
30
- * Parameters that should be added to the request url
46
+ * This function performs an HTTP GET request with optional parameters,
47
+ * headers, credentials, and timeout functionality.
31
48
  *
32
- * @param {object} [_.headers]
33
- * Object that contains custom headers. A header is a name, value pair.
49
+ * @param {import('./typedef').HttpRequestOptions} options
50
+ * Request configuration options
34
51
  *
35
- * e.g. options.headers = { "content-type": "application/json" }
52
+ * @returns {Promise<Response>} Response promise
36
53
  *
37
- * @param {requestHandler} [_.requestHandler]
54
+ * @example
55
+ * // Basic GET request
56
+ * const response = await httpGet({
57
+ * url: 'https://api.example.com/data'
58
+ * });
38
59
  *
39
- * @param {number} [_.timeoutMs]
40
- * If defined, this request will abort after the specified number of
41
- * milliseconds. Values above the the built-in request timeout won't work.
60
+ * @example
61
+ * // GET request with URL parameters and timeout
62
+ * const response = await httpGet({
63
+ * url: 'https://api.example.com/search',
64
+ * urlSearchParams: new URLSearchParams({ q: 'search term' }),
65
+ * timeoutMs: 5000
66
+ * });
42
67
  *
43
- * @returns {Promise<Response>} responsePromise
68
+ * @example
69
+ * // GET request with abort capability
70
+ * const response = await httpGet({
71
+ * url: 'https://api.example.com/large-data',
72
+ * requestHandler: ({ abort }) => {
73
+ * // Store abort function for later use
74
+ * window.abortDataRequest = abort;
75
+ * }
76
+ * });
44
77
  */
45
- export async function httpGet({
46
- url,
47
- urlSearchParams,
48
- headers,
49
- requestHandler,
50
- timeoutMs
51
- }) {
52
- const responsePromise = httpRequest({
53
- method: METHOD_GET,
54
- url,
55
- urlSearchParams,
56
- headers,
57
- requestHandler,
58
- timeoutMs
59
- });
60
-
61
- return await waitForAndCheckResponse(responsePromise, url);
78
+ export async function httpGet(options) {
79
+ return await httpRequest({
80
+ ...options,
81
+ method: METHOD_GET
82
+ });
62
83
  }
63
84
 
64
85
  /**
65
- * Make POST request
66
- *
67
- * @param {object} _
68
- *
69
- * @param {string|URL} _.url - Url string or URL object
70
- *
71
- * @param {any} [_.body] - POST data
86
+ * Make a POST request
72
87
  *
73
- * @param {object} [_.headers]
74
- * Object that contains custom headers. A header is a name, value pair.
88
+ * This function performs an HTTP POST request with optional body,
89
+ * headers, credentials, and timeout functionality.
75
90
  *
76
- * e.g. options.headers = { "content-type": "application/json" }
91
+ * @param {import('./typedef').HttpRequestOptions} options
92
+ * Request configuration options
77
93
  *
78
- * @param {requestHandler} [_.requestHandler]
94
+ * @returns {Promise<Response>} Response promise
79
95
  *
80
- * @param {number} [_.timeoutMs]
81
- * If defined, this request will abort after the specified number of
82
- * milliseconds. Values above the the built-in request timeout won't work.
96
+ * @example
97
+ * // Basic POST request with JSON data
98
+ * const response = await httpPost({
99
+ * url: 'https://api.example.com/users',
100
+ * body: JSON.stringify({ name: 'John Doe', email: 'john@example.com' }),
101
+ * headers: { 'content-type': 'application/json' }
102
+ * });
83
103
  *
84
- * @returns {Promise<Response>} responsePromise
104
+ * @example
105
+ * // POST request with timeout
106
+ * const response = await httpPost({
107
+ * url: 'https://api.example.com/upload',
108
+ * body: formData,
109
+ * timeoutMs: 30000 // 30 seconds timeout
110
+ * });
85
111
  */
86
- export async function httpPost({
87
- url,
88
- body = null,
89
- headers,
90
- requestHandler,
91
- timeoutMs
92
- }) {
93
- const responsePromise = httpRequest({
94
- method: METHOD_POST,
95
- url,
96
- body,
97
- headers,
98
- requestHandler,
99
- timeoutMs
100
- });
101
-
102
- return await waitForAndCheckResponse(responsePromise, url);
112
+ export async function httpPost(options) {
113
+ return await httpRequest({
114
+ ...options,
115
+ method: METHOD_POST
116
+ });
103
117
  }
104
118
 
105
119
  // -----------------------------------------------------------------------------
106
120
 
107
121
  /**
108
- * Make an HTTP request
109
- * - This is a low level function, consider using
110
- * httpGet, httpPost, jsonGet or jsonPost instead
122
+ * Make an HTTP request (low-level function)
111
123
  *
112
- * @param {object} _
124
+ * This is a low-level function that powers httpGet and httpPost.
125
+ * It provides complete control over request configuration.
113
126
  *
114
- * @param {string|URL} _.url - Url string or URL object
127
+ * @param {import('./typedef').HttpRequestOptions} options
128
+ * Request configuration options
115
129
  *
116
- * @param {string} _.method - Request method: METHOD_GET | METHOD_POST
130
+ * @throws {TypeError} If a network error occurred
131
+ * @returns {Promise<Response>} Response promise
117
132
  *
118
- * @param {object} [_.urlSearchParams] - URL search parameters as key-value pairs
133
+ * @example
134
+ * // Custom HTTP request with PUT method
135
+ * const response = await httpRequest({
136
+ * method: 'PUT',
137
+ * url: 'https://api.example.com/resources/123',
138
+ * body: JSON.stringify({ status: 'updated' }),
139
+ * headers: { 'content-type': 'application/json' },
140
+ * withCredentials: true
141
+ * });
119
142
  *
120
- * @param {any} [_.body] - POST data
121
- *
122
- * @param {object} [_.headers]
123
- * Object that contains custom headers. A header is a name, value pair.
124
- *
125
- * e.g. options.headers = { "content-type": "application/json" }
126
- *
127
- * @param {requestHandler} [_.requestHandler]
128
- *
129
- * @param {number} [_.timeoutMs]
130
- * If defined, this request will abort after the specified number of
131
- * milliseconds. Values above the the built-in request timeout won't work.
132
- *
133
- * @throws TypeError - If a network error occurred
134
- *
135
- * @note Check the `ok` property of the resolved response to check if the
136
- * response was successfull (e.g. in case of a 404, ok is false)
137
- *
138
- * @returns {Promise<Response>} responsePromise
143
+ * // Check if response was successful
144
+ * if (response.ok) {
145
+ * // Process response
146
+ * } else {
147
+ * // Handle error based on status
148
+ * }
139
149
  */
140
- export async function httpRequest({
141
- method,
142
- url,
143
- urlSearchParams = null,
144
- body = null,
145
- headers,
146
- requestHandler,
147
- timeoutMs
148
- }) {
149
- url = toURL(url);
150
-
151
- // @see https://developer.mozilla.org/en-US/docs/Web/API/Headers
152
-
153
- const requestHeaders = new Headers();
154
-
155
- if (headers) {
156
- setRequestHeaders(requestHeaders, headers);
157
-
158
- if (
159
- headers[CONTENT_TYPE] === APPLICATION_JSON &&
160
- typeof body !== 'string'
161
- ) {
162
- throw new Error(
163
- `Trying to send request with [content-type:${APPLICATION_JSON}], ` +
164
- 'but body is not a (JSON encoded) string.'
165
- );
166
- }
167
- // IDEA: try to decode the body to catch errors on client side
168
- }
169
-
170
- /** @type {RequestInit} */
171
- const init = {
172
- mode: 'cors',
173
- cache: 'no-cache',
174
- credentials: 'omit',
175
- redirect: 'follow',
176
- referrerPolicy: 'no-referrer',
177
- headers: requestHeaders
178
- };
179
-
180
- // Allow search params also for other request types than GET
181
-
182
- if (urlSearchParams) {
183
- if (!(urlSearchParams instanceof URLSearchParams)) {
184
- throw new Error(
185
- 'Invalid parameter [urlSearchParams] ' +
186
- '(expected instanceof URLSearchParams)'
187
- );
188
- }
189
-
190
- const existingParams = url.searchParams;
191
-
192
- for (const [name, value] of urlSearchParams.entries()) {
193
- if (existingParams.has(name)) {
194
- throw new Error(
195
- `Cannot set URL search parameter [${name}] ` +
196
- `in url [${url.href}] (already set)`
197
- );
198
- }
199
-
200
- existingParams.set(name, value);
201
- } // end for
202
- }
203
-
204
- //
205
- // Sort search params to make the url nicer
206
- //
207
- url.searchParams.sort();
208
-
209
- // console.log( "url", url );
210
-
211
- init.method = method;
212
-
213
- if (METHOD_POST === method) {
214
- init.body = body || null; /* : JSON.stringify( body ) */
215
- }
216
-
217
- // @see https://developer.mozilla.org/en-US/docs/Web/API/Request/Request
218
-
219
- // console.log( "init", init );
220
- // console.log( "headers", init.headers );
221
-
222
- const request = new Request(url, init);
223
-
224
- // @see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/abort
225
-
226
- const controller = new AbortController();
227
- const signal = controller.signal;
228
-
229
- //
230
- // A fetch() promise will reject with a TypeError when a network error
231
- // is encountered or CORS is misconfigured on the server-side,
232
- // although this usually means permission issues or similar
233
- // — a 404 does not constitute a network error, for example.
234
- // An accurate check for a successful fetch() would include checking
235
- // that the promise resolved, then checking that the Response.ok property
236
- // has a value of true. The code would look something like this:
237
- //
238
- // fetch()
239
- // .then( () => {
240
- // if( !response.ok ) {
241
- // throw new Error('Network response was not OK');
242
- // }
243
- // ...
244
- // }
245
- // .catch((error) => { .. }
246
- //
247
-
248
- const promise = fetch(request, { signal });
249
-
250
- if (requestHandler || timeoutMs) {
251
- /**
252
- * @type {(reason?: any) => void}
253
- */
254
- const abort = (reason) => {
255
- if (!reason) {
256
- reason = new AbortError(`Request [${url.href}] aborted`);
257
- }
258
-
259
- controller.abort(reason);
260
- };
261
-
262
- /**
263
- * Function that can be used to set a timeout on a request
264
- *
265
- * @param {number} delayMs
266
- */
267
- const timeout = (delayMs = 10000) => {
268
- expect.positiveNumber(delayMs);
269
-
270
- const timerId = setTimeout(() => {
271
- controller.abort(
272
- new TimeoutError(`Request [${url.href}] timed out [${delayMs}]`)
273
- );
274
- }, delayMs);
275
-
276
- promise.finally(() => {
277
- clearTimeout(timerId);
278
- });
279
- };
280
-
281
- if (timeoutMs) {
282
- timeout(timeoutMs);
283
- }
284
-
285
- if (requestHandler) {
286
- expect.function(requestHandler);
287
-
288
- requestHandler({ controller, abort, timeout });
289
- }
290
- }
291
-
292
- // response promise
293
- return promise;
150
+ export async function httpRequest(options) {
151
+ // Apply default configuration
152
+ const config = { ...DEFAULT_HTTP_CONFIG, ...options };
153
+
154
+ const {
155
+ method,
156
+ url: rawUrl,
157
+ urlSearchParams,
158
+ body,
159
+ headers,
160
+ withCredentials,
161
+ requestHandler,
162
+ timeoutMs,
163
+ mode,
164
+ cache,
165
+ redirect,
166
+ referrerPolicy,
167
+ cacheEnabled
168
+ } = config;
169
+
170
+ const url = toURL(rawUrl);
171
+
172
+ // Only consider caching for GET requests
173
+ const shouldAttemptCache = cacheEnabled && method === METHOD_GET;
174
+
175
+ // Try to get from cache if appropriate
176
+ if (shouldAttemptCache && cache !== 'no-store' && cache !== 'reload') {
177
+ const cacheKeyParams = { url, ...headers };
178
+ const cachedResponse = await getCachedResponse(cacheKeyParams);
179
+
180
+ if (cachedResponse) {
181
+ console.debug(`Cache hit [${url.pathname}]`);
182
+ return cachedResponse;
183
+ }
184
+ }
185
+
186
+ // @see https://developer.mozilla.org/en-US/docs/Web/API/Headers
187
+ const requestHeaders = new Headers();
188
+
189
+ if (headers) {
190
+ setRequestHeaders(requestHeaders, headers);
191
+
192
+ if (
193
+ headers[CONTENT_TYPE] === APPLICATION_JSON &&
194
+ typeof body !== 'string'
195
+ ) {
196
+ throw new Error(
197
+ `Trying to send request with [content-type:${APPLICATION_JSON}], ` +
198
+ 'but body is not a (JSON encoded) string.'
199
+ );
200
+ }
201
+ // IDEA: try to decode the body to catch errors on client side
202
+ }
203
+
204
+ /** @type {RequestInit} */
205
+ const init = {
206
+ mode,
207
+ cache,
208
+ credentials: withCredentials ? 'include': 'omit',
209
+ redirect,
210
+ referrerPolicy,
211
+ headers: requestHeaders
212
+ };
213
+
214
+ // Allow search params also for other request types than GET
215
+ if (urlSearchParams) {
216
+ if (!(urlSearchParams instanceof URLSearchParams)) {
217
+ throw new Error(
218
+ 'Invalid parameter [urlSearchParams] ' +
219
+ '(expected instanceof URLSearchParams)'
220
+ );
221
+ }
222
+
223
+ const existingParams = url.searchParams;
224
+
225
+ for (const [name, value] of urlSearchParams.entries()) {
226
+ if (existingParams.has(name)) {
227
+ throw new Error(
228
+ `Cannot set URL search parameter [${name}] ` +
229
+ `in url [${url.href}] (already set)`
230
+ );
231
+ }
232
+
233
+ existingParams.set(name, value);
234
+ } // end for
235
+ }
236
+
237
+ //
238
+ // Sort search params to make the url nicer
239
+ //
240
+ url.searchParams.sort();
241
+
242
+ init.method = method;
243
+
244
+ if (METHOD_POST === method) {
245
+ init.body = body || null; /* : JSON.stringify( body ) */
246
+ }
247
+
248
+ // @see https://developer.mozilla.org/en-US/docs/Web/API/Request/Request
249
+ const request = new Request(url, init);
250
+
251
+ // @see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/abort
252
+ const controller = new AbortController();
253
+ const signal = controller.signal;
254
+
255
+ //
256
+ // A fetch() promise will reject with a TypeError when a network error
257
+ // is encountered or CORS is misconfigured on the server-side,
258
+ // although this usually means permission issues or similar
259
+ // — a 404 does not constitute a network error, for example.
260
+ // An accurate check for a successful fetch() would include checking
261
+ // that the promise resolved, then checking that the Response.ok property
262
+ // has a value of true. The code would look something like this:
263
+ //
264
+ // fetch()
265
+ // .then( () => {
266
+ // if( !response.ok ) {
267
+ // throw new Error('Network response was not OK');
268
+ // }
269
+ // ...
270
+ // }
271
+ // .catch((error) => { .. }
272
+ //
273
+
274
+ const promise = fetch(request, { signal });
275
+
276
+ if (requestHandler || timeoutMs) {
277
+ /**
278
+ * @type {(reason?: any) => void}
279
+ */
280
+ const abort = (reason) => {
281
+ if (!reason) {
282
+ reason = new AbortError(`Request [${url.href}] aborted`);
283
+ }
284
+
285
+ controller.abort(reason);
286
+ };
287
+
288
+ /**
289
+ * Function that can be used to set a timeout on a request
290
+ *
291
+ * @param {number} delayMs - Milliseconds to wait before timeout
292
+ */
293
+ const timeout = (delayMs = 10000) => {
294
+ expect.positiveNumber(delayMs);
295
+
296
+ const timerId = setTimeout(() => {
297
+ controller.abort(
298
+ new TimeoutError(`Request [${url.href}] timed out [${delayMs}]`)
299
+ );
300
+ }, delayMs);
301
+
302
+ promise.finally(() => {
303
+ clearTimeout(timerId);
304
+ });
305
+ };
306
+
307
+ if (timeoutMs) {
308
+ timeout(timeoutMs);
309
+ }
310
+
311
+ if (requestHandler) {
312
+ expect.function(requestHandler);
313
+
314
+ requestHandler({ controller, abort, timeout });
315
+ }
316
+ }
317
+
318
+ // Wait for the response and check it
319
+ const response = await waitForAndCheckResponse(promise, url);
320
+
321
+ // If caching is enabled, store the response in cache
322
+ if (shouldAttemptCache && response.ok) {
323
+ // Extract cache control headers
324
+ const cacheControl = response.headers.get('Cache-Control') || '';
325
+ const etag = response.headers.get('ETag');
326
+ const lastModified = response.headers.get('Last-Modified');
327
+
328
+ // Parse cache-control directives
329
+ const directives = {};
330
+ cacheControl.split(',').forEach(directive => {
331
+ const [key, value] = directive.trim().split('=');
332
+ directives[key.toLowerCase()] = value !== undefined ? value : true;
333
+ });
334
+
335
+ // Determine if cacheable
336
+ const isCacheable = !directives['no-store'] && !directives['private'];
337
+
338
+ if (isCacheable) {
339
+ // Calculate expiration time
340
+ let expires = null;
341
+ if (directives['max-age']) {
342
+ const maxAge = parseInt(directives['max-age'], 10);
343
+ expires = Date.now() + (maxAge * 1000);
344
+ } else if (response.headers.get('Expires')) {
345
+ expires = new Date(response.headers.get('Expires')).getTime();
346
+ }
347
+
348
+ // Create stale info
349
+ const staleInfo = {
350
+ isStale: false,
351
+ fresh: null,
352
+ timestamp: Date.now(),
353
+ expires
354
+ };
355
+
356
+ // Store response in cache
357
+ const cacheKeyParams = { url, ...headers };
358
+ await storeResponseInCache(cacheKeyParams, response.clone(), {
359
+ etag,
360
+ lastModified,
361
+ expires,
362
+ immutable: directives['immutable'] || false
363
+ });
364
+ }
365
+ }
366
+
367
+ return response;
294
368
  }
369
+