@hkdigital/lib-sveltekit 0.2.21 → 0.2.22
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 +149 -135
- package/dist/assets/autospuiten/car-paint-picker.js +41 -41
- package/dist/assets/autospuiten/labels.js +7 -7
- package/dist/classes/cache/IndexedDbCache.js +1407 -1407
- package/dist/classes/cache/MemoryResponseCache.js +138 -138
- package/dist/classes/cache/index.js +5 -5
- package/dist/classes/cache/typedef.js +41 -41
- package/dist/classes/data/IterableTree.js +243 -243
- package/dist/classes/data/Selector.js +190 -190
- package/dist/classes/data/index.js +2 -2
- package/dist/classes/events/EventEmitter.js +275 -275
- package/dist/classes/events/index.js +2 -2
- package/dist/classes/index.js +4 -4
- package/dist/classes/logging/Logger.js +210 -210
- package/dist/classes/logging/constants.js +16 -16
- package/dist/classes/logging/index.js +4 -4
- package/dist/classes/logging/typedef.js +17 -17
- package/dist/classes/promise/HkPromise.js +377 -377
- package/dist/classes/promise/index.js +1 -1
- package/dist/classes/services/ServiceBase.js +463 -463
- package/dist/classes/services/ServiceManager.js +614 -614
- package/dist/classes/services/index.js +5 -5
- package/dist/classes/services/service-states.js +205 -205
- package/dist/classes/services/typedef.js +179 -179
- package/dist/classes/stores/SubscribersCount.js +107 -107
- package/dist/classes/stores/index.js +1 -1
- package/dist/classes/streams/LogTransformStream.js +19 -19
- package/dist/classes/streams/ServerEventsStore.js +110 -110
- package/dist/classes/streams/TimeStampSource.js +26 -26
- package/dist/classes/streams/index.js +3 -3
- package/dist/classes/svelte/audio/AudioLoader.svelte.js +58 -58
- package/dist/classes/svelte/audio/AudioScene.svelte.js +324 -324
- package/dist/classes/svelte/audio/mocks.js +35 -35
- package/dist/classes/svelte/finite-state-machine/FiniteStateMachine.svelte.js +133 -133
- package/dist/classes/svelte/finite-state-machine/index.js +1 -1
- package/dist/classes/svelte/image/ImageLoader.svelte.js +45 -45
- package/dist/classes/svelte/image/ImageScene.svelte.js +249 -249
- package/dist/classes/svelte/image/ImageVariantsLoader.svelte.js +152 -152
- package/dist/classes/svelte/image/index.js +4 -4
- package/dist/classes/svelte/image/mocks.js +35 -35
- package/dist/classes/svelte/image/typedef.js +8 -8
- package/dist/classes/svelte/index.js +14 -14
- package/dist/classes/svelte/loading-state-machine/LoadingStateMachine.svelte.js +109 -109
- package/dist/classes/svelte/loading-state-machine/constants.js +16 -16
- package/dist/classes/svelte/loading-state-machine/index.js +3 -3
- package/dist/classes/svelte/network-loader/NetworkLoader.svelte.js +338 -338
- package/dist/classes/svelte/network-loader/constants.js +3 -3
- package/dist/classes/svelte/network-loader/index.js +3 -3
- package/dist/classes/svelte/network-loader/mocks.js +30 -30
- package/dist/classes/svelte/network-loader/typedef.js +8 -8
- package/dist/components/area/HkArea.svelte +49 -49
- package/dist/components/area/HkGridArea.svelte +77 -77
- package/dist/components/area/index.js +2 -2
- package/dist/components/buttons/button/Button.svelte +82 -82
- package/dist/components/buttons/button-icon-steeze/SteezeIconButton.svelte +30 -30
- package/dist/components/buttons/button-text/TextButton.svelte +21 -21
- package/dist/components/buttons/index.js +3 -3
- package/dist/components/debug/debug-panel-design-scaling/DebugPanelDesignScaling.svelte +146 -146
- package/dist/components/debug/index.js +1 -1
- package/dist/components/drag-drop/DragController.js +44 -44
- package/dist/components/drag-drop/DragDropContext.svelte +111 -111
- package/dist/components/drag-drop/Draggable.svelte +519 -519
- package/dist/components/drag-drop/{Dropzone.svelte → DropZone.svelte} +258 -258
- package/dist/components/drag-drop/DropZoneArea.svelte +119 -119
- package/dist/components/drag-drop/DropZoneList.svelte +125 -125
- package/dist/components/drag-drop/actions.js +26 -26
- package/dist/components/drag-drop/drag-state.svelte.js +322 -322
- package/dist/components/drag-drop/index.js +7 -7
- package/dist/components/drag-drop/util.js +85 -85
- package/dist/components/hkdev/blocks/TextBlock.svelte +46 -46
- package/dist/components/hkdev/buttons/CheckButton.svelte +62 -62
- package/dist/components/icons/HkIcon.svelte +86 -86
- package/dist/components/icons/HkTabIcon.svelte +116 -116
- package/dist/components/icons/SteezeIcon.svelte +97 -97
- package/dist/components/icons/index.js +6 -6
- package/dist/components/icons/typedef.js +16 -16
- package/dist/components/index.js +2 -2
- package/dist/components/inputs/index.js +1 -1
- package/dist/components/inputs/text-input/TestTextInput.svelte__ +102 -102
- package/dist/components/inputs/text-input/TextInput.svelte +223 -223
- package/dist/components/inputs/text-input/TextInput.svelte___ +83 -83
- package/dist/components/inputs/text-input/assets/IconInvalid.svelte +14 -14
- package/dist/components/inputs/text-input/assets/IconValid.svelte +12 -12
- package/dist/components/layout/grid-layers/GridLayers.svelte +63 -63
- package/dist/components/layout/grid-layers/GridLayers.svelte__heightFrom__ +372 -0
- package/dist/components/layout/grid-layers/util.js +74 -74
- package/dist/components/layout/index.js +1 -1
- package/dist/components/panels/index.js +1 -1
- package/dist/components/panels/panel/Panel.svelte +43 -43
- package/dist/components/rows/index.js +3 -3
- package/dist/components/rows/panel-grid-row/PanelGridRow.svelte +104 -104
- package/dist/components/rows/panel-row-2/PanelRow2.svelte +40 -40
- package/dist/components/tab-bar/HkTabBar.state.svelte.js +149 -149
- package/dist/components/tab-bar/HkTabBar.svelte +74 -74
- package/dist/components/tab-bar/HkTabBarSelector.state.svelte.js +93 -93
- package/dist/components/tab-bar/HkTabBarSelector.svelte +49 -49
- package/dist/components/tab-bar/index.js +17 -17
- package/dist/components/tab-bar/typedef.js +11 -11
- package/dist/config/imagetools-config.js +189 -189
- package/dist/config/imagetools.d.ts +72 -72
- package/dist/constants/bases.js +13 -13
- package/dist/constants/errors/api.js +9 -9
- package/dist/constants/errors/generic.js +5 -5
- package/dist/constants/errors/index.js +3 -3
- package/dist/constants/errors/jwt.js +5 -5
- package/dist/constants/http/headers.js +6 -6
- package/dist/constants/http/index.js +2 -2
- package/dist/constants/http/methods.js +14 -14
- package/dist/constants/index.js +3 -3
- package/dist/constants/mime/application.js +5 -5
- package/dist/constants/mime/audio.js +13 -13
- package/dist/constants/mime/image.js +3 -3
- package/dist/constants/mime/index.js +4 -4
- package/dist/constants/mime/text.js +2 -2
- package/dist/constants/regexp/index.js +31 -31
- package/dist/constants/regexp/inspiratie.js__ +95 -95
- package/dist/constants/regexp/text.js +49 -49
- package/dist/constants/regexp/user.js +32 -32
- package/dist/constants/regexp/web.js +3 -3
- package/dist/constants/state-labels/drag-states.js +6 -6
- package/dist/constants/state-labels/drop-states.js +6 -6
- package/dist/constants/state-labels/input-states.js +11 -11
- package/dist/constants/state-labels/submit-states.js +4 -4
- package/dist/constants/time.js +28 -28
- package/dist/css/utilities.css +43 -43
- package/dist/design/design-config.js +73 -73
- package/dist/design/tailwind-theme-extend.js +158 -158
- package/dist/features/button-group/ButtonGroup.svelte +82 -82
- package/dist/features/button-group/typedef.js +10 -10
- package/dist/features/compare-left-right/CompareLeftRight.svelte +179 -179
- package/dist/features/compare-left-right/index.js +1 -1
- package/dist/features/game-box/GameBox.svelte +577 -577
- package/dist/features/game-box/gamebox.util.js +83 -83
- package/dist/features/hk-app-layout/HkAppLayout.state.svelte.js +25 -25
- package/dist/features/hk-app-layout/HkAppLayout.svelte +251 -251
- package/dist/features/image-box/ImageBox.svelte +210 -210
- package/dist/features/image-box/index.js +5 -5
- package/dist/features/image-box/typedef.js +32 -32
- package/dist/features/index.js +23 -23
- package/dist/features/presenter/ImageSlide.svelte +64 -64
- package/dist/features/presenter/Presenter.state.svelte.js +638 -638
- package/dist/features/presenter/Presenter.svelte +142 -142
- package/dist/features/presenter/constants.js +7 -7
- package/dist/features/presenter/index.js +10 -10
- package/dist/features/presenter/typedef.js +106 -106
- package/dist/features/presenter/util.js +210 -210
- package/dist/features/virtual-viewport/VirtualViewport.svelte +196 -196
- package/dist/logging/adapters/console.js +114 -114
- package/dist/logging/adapters/pino.js +60 -60
- package/dist/logging/constants.js +1 -1
- package/dist/logging/factories/client.js +21 -21
- package/dist/logging/factories/server.js +22 -22
- package/dist/logging/factories/universal.js +23 -23
- package/dist/logging/index.js +8 -8
- package/dist/schemas/index.js +1 -1
- package/dist/schemas/validate-url.js +180 -180
- package/dist/server/index.js +1 -1
- package/dist/server/logger.js +94 -94
- package/dist/states/index.js +1 -1
- package/dist/states/navigation.svelte.js +55 -55
- package/dist/stores/index.js +1 -1
- package/dist/stores/theme.js +80 -80
- package/dist/themes/hkdev/components/blocks/text-block.css +34 -34
- package/dist/themes/hkdev/components/boxes/game-box.css +11 -11
- package/dist/themes/hkdev/components/buttons/button-icon-steeze.css +22 -22
- package/dist/themes/hkdev/components/buttons/button-text.css +32 -32
- package/dist/themes/hkdev/components/buttons/button.css +146 -146
- package/dist/themes/hkdev/components/buttons/skip-button.css +5 -5
- package/dist/themes/hkdev/components/drag-drop/draggable.css +73 -73
- package/dist/themes/hkdev/components/drag-drop/drop-zone.css +58 -58
- package/dist/themes/hkdev/components/icons/icon-steeze.css +15 -15
- package/dist/themes/hkdev/components/inputs/text-input.css +102 -102
- package/dist/themes/hkdev/components/panels/panel.css +25 -25
- package/dist/themes/hkdev/components/rows/panel-grid-row.css +4 -4
- package/dist/themes/hkdev/components/rows/panel-row-2.css +5 -5
- package/dist/themes/hkdev/components.css +29 -29
- package/dist/themes/hkdev/debug.css +1 -1
- package/dist/themes/hkdev/global/layout.css +32 -32
- package/dist/themes/hkdev/global/on-colors.css +32 -32
- package/dist/themes/hkdev/globals.css +3 -3
- package/dist/themes/hkdev/responsive.css +12 -12
- package/dist/themes/hkdev/theme-ext.js +12 -12
- package/dist/themes/hkdev/theme.css +218 -218
- package/dist/themes/index.js +1 -1
- package/dist/typedef/context.js +6 -6
- package/dist/typedef/drag.js +25 -25
- package/dist/typedef/drop.js +12 -12
- package/dist/typedef/image.js +38 -38
- package/dist/typedef/index.js +4 -4
- package/dist/util/array/index.js +436 -436
- package/dist/util/bases/base58.js +262 -262
- package/dist/util/bases/index.js +1 -1
- package/dist/util/compare/index.js +247 -247
- package/dist/util/css/css-vars.js +83 -83
- package/dist/util/css/index.js +1 -1
- package/dist/util/design-system/components/states.js +22 -22
- package/dist/util/design-system/css/clamp.js +66 -66
- package/dist/util/design-system/css/root-design-vars.js +102 -102
- package/dist/util/design-system/index.js +5 -5
- package/dist/util/design-system/layout/scaling.js +228 -228
- package/dist/util/design-system/skeleton.js +208 -208
- package/dist/util/design-system/tailwind.js +288 -288
- package/dist/util/env/index.js +9 -9
- package/dist/util/exceptions/index.d.ts +11 -0
- package/dist/util/exceptions/index.js +17 -0
- package/dist/util/expect/arrays.js +47 -47
- package/dist/util/expect/index.js +259 -259
- package/dist/util/expect/primitives.js +55 -55
- package/dist/util/expect/url.js +60 -60
- package/dist/util/function/index.js +218 -218
- package/dist/util/geo/index.js +26 -26
- package/dist/util/http/caching.js +263 -263
- package/dist/util/http/errors.js +97 -97
- package/dist/util/http/headers.js +75 -75
- package/dist/util/http/http-request.js +578 -578
- package/dist/util/http/index.js +22 -22
- package/dist/util/http/json-request.js +224 -224
- package/dist/util/http/mocks.js +65 -65
- package/dist/util/http/response.js +294 -294
- package/dist/util/http/test-data__/content-length-test-hkdigital-small.V4HfZyBQ.avif +0 -0
- package/dist/util/http/typedef.js +93 -93
- package/dist/util/http/url.js +52 -52
- package/dist/util/image/index.js +86 -86
- package/dist/util/index.d.ts +1 -0
- package/dist/util/index.js +3 -2
- package/dist/util/is/index.js +140 -140
- package/dist/util/iterate/index.js +234 -234
- package/dist/util/object/index.js +1361 -1361
- package/dist/util/singleton/index.js +97 -97
- package/dist/util/string/array-path.js +75 -75
- package/dist/util/string/convert.js +54 -54
- package/dist/util/string/fs.js +226 -226
- package/dist/util/string/index.js +5 -5
- package/dist/util/string/interpolate.js +61 -61
- package/dist/util/string/pad.js +10 -10
- package/dist/util/svelte/index.js +4 -4
- package/dist/util/svelte/loading/loading-tracker.svelte.js +108 -108
- package/dist/util/svelte/observe/index.js +49 -49
- package/dist/util/svelte/state-context/index.js +117 -117
- package/dist/util/svelte/wait/index.js +38 -38
- package/dist/util/sveltekit/index.js +1 -1
- package/dist/util/sveltekit/route-folders/index.js +101 -101
- package/dist/util/time/index.js +323 -323
- package/dist/util/unique/index.js +249 -249
- package/dist/valibot/date.js__ +10 -10
- package/dist/valibot/index.js +9 -9
- package/dist/valibot/url.js +95 -95
- package/dist/valibot/user.js +23 -23
- package/dist/zod/all.js +33 -33
- package/dist/zod/generic.js +11 -11
- package/dist/zod/javascript.js +32 -32
- package/dist/zod/user.js +16 -16
- package/dist/zod/web.js +52 -52
- package/package.json +133 -132
@@ -1,578 +1,578 @@
|
|
1
|
-
import {
|
2
|
-
METHOD_GET,
|
3
|
-
METHOD_POST,
|
4
|
-
METHOD_PUT,
|
5
|
-
METHOD_DELETE,
|
6
|
-
METHOD_PATCH,
|
7
|
-
METHOD_OPTIONS,
|
8
|
-
METHOD_HEAD
|
9
|
-
} from '../../constants/http/methods.js';
|
10
|
-
|
11
|
-
import { APPLICATION_JSON } from '../../constants/mime/application.js';
|
12
|
-
import { CONTENT_TYPE } from '../../constants/http/headers.js';
|
13
|
-
|
14
|
-
import { AbortError, TimeoutError } from '../../constants/errors/api.js';
|
15
|
-
|
16
|
-
import * as expect from '../expect/index.js';
|
17
|
-
|
18
|
-
import { toURL } from './url.js';
|
19
|
-
import { setRequestHeaders } from './headers.js';
|
20
|
-
import { waitForAndCheckResponse } from './response.js';
|
21
|
-
|
22
|
-
import { getCachedResponse, storeResponseInCache } from './caching.js';
|
23
|
-
|
24
|
-
import { isTestEnv } from '../env';
|
25
|
-
|
26
|
-
/**
|
27
|
-
* Default configuration for HTTP requests
|
28
|
-
*
|
29
|
-
* This object contains default settings used by the HTTP request functions.
|
30
|
-
* It can be used as a reference for available options and their default values.
|
31
|
-
*
|
32
|
-
* @type {Object}
|
33
|
-
*/
|
34
|
-
export const DEFAULT_HTTP_CONFIG = {
|
35
|
-
// Request
|
36
|
-
method: METHOD_GET,
|
37
|
-
urlSearchParams: null,
|
38
|
-
body: null,
|
39
|
-
headers: null,
|
40
|
-
withCredentials: false,
|
41
|
-
timeoutMs: null, // No timeout by default
|
42
|
-
|
43
|
-
// Fetch
|
44
|
-
mode: 'cors',
|
45
|
-
cache: 'no-cache',
|
46
|
-
redirect: 'follow',
|
47
|
-
referrerPolicy: 'no-referrer',
|
48
|
-
|
49
|
-
// Cache
|
50
|
-
cacheEnabled: true
|
51
|
-
};
|
52
|
-
|
53
|
-
/**
|
54
|
-
* Make a GET request
|
55
|
-
*
|
56
|
-
* This function performs an HTTP GET request with optional parameters,
|
57
|
-
* headers, credentials, and timeout functionality.
|
58
|
-
*
|
59
|
-
* @param {import('./typedef').HttpRequestOptions} options
|
60
|
-
* Request configuration options
|
61
|
-
*
|
62
|
-
* @returns {Promise<Response>} Response promise
|
63
|
-
*
|
64
|
-
* @example
|
65
|
-
* // Basic GET request
|
66
|
-
* const response = await httpGet({
|
67
|
-
* url: 'https://api.example.com/data'
|
68
|
-
* });
|
69
|
-
*
|
70
|
-
* @example
|
71
|
-
* // GET request with URL parameters and timeout
|
72
|
-
* const response = await httpGet({
|
73
|
-
* url: 'https://api.example.com/search',
|
74
|
-
* urlSearchParams: new URLSearchParams({ q: 'search term' }),
|
75
|
-
* timeoutMs: 5000
|
76
|
-
* });
|
77
|
-
*
|
78
|
-
* @example
|
79
|
-
* // GET request with abort capability
|
80
|
-
* const response = await httpGet({
|
81
|
-
* url: 'https://api.example.com/large-data',
|
82
|
-
* requestHandler: ({ abort }) => {
|
83
|
-
* // Store abort function for later use
|
84
|
-
* window.abortDataRequest = abort;
|
85
|
-
* }
|
86
|
-
* });
|
87
|
-
*/
|
88
|
-
export async function httpGet(options) {
|
89
|
-
return await httpRequest({
|
90
|
-
...options,
|
91
|
-
method: METHOD_GET
|
92
|
-
});
|
93
|
-
}
|
94
|
-
|
95
|
-
/**
|
96
|
-
* Make a POST request
|
97
|
-
*
|
98
|
-
* This function performs an HTTP POST request with optional body,
|
99
|
-
* headers, credentials, and timeout functionality.
|
100
|
-
*
|
101
|
-
* @param {import('./typedef').HttpRequestOptions} options
|
102
|
-
* Request configuration options
|
103
|
-
*
|
104
|
-
* @returns {Promise<Response>} Response promise
|
105
|
-
*
|
106
|
-
* @example
|
107
|
-
* // Basic POST request with JSON data
|
108
|
-
* const response = await httpPost({
|
109
|
-
* url: 'https://api.example.com/users',
|
110
|
-
* body: JSON.stringify({ name: 'John Doe', email: 'john@example.com' }),
|
111
|
-
* headers: { 'content-type': 'application/json' }
|
112
|
-
* });
|
113
|
-
*
|
114
|
-
* @example
|
115
|
-
* // POST request with timeout
|
116
|
-
* const response = await httpPost({
|
117
|
-
* url: 'https://api.example.com/upload',
|
118
|
-
* body: formData,
|
119
|
-
* timeoutMs: 30000 // 30 seconds timeout
|
120
|
-
* });
|
121
|
-
*/
|
122
|
-
export async function httpPost(options) {
|
123
|
-
return await httpRequest({
|
124
|
-
...options,
|
125
|
-
method: METHOD_POST
|
126
|
-
});
|
127
|
-
}
|
128
|
-
|
129
|
-
/**
|
130
|
-
* Make a PUT request
|
131
|
-
*
|
132
|
-
* This function performs an HTTP PUT request, typically used for updating
|
133
|
-
* or replacing resources. It supports optional body, headers, credentials,
|
134
|
-
* and timeout functionality.
|
135
|
-
*
|
136
|
-
* @param {import('./typedef').HttpRequestOptions} options
|
137
|
-
* Request configuration options
|
138
|
-
*
|
139
|
-
* @returns {Promise<Response>} Response promise
|
140
|
-
*
|
141
|
-
* @example
|
142
|
-
* // Update a user resource
|
143
|
-
* const response = await httpPut({
|
144
|
-
* url: 'https://api.example.com/users/123',
|
145
|
-
* body: JSON.stringify({ name: 'John Doe', email: 'john.doe@example.com' }),
|
146
|
-
* headers: { 'content-type': 'application/json' }
|
147
|
-
* });
|
148
|
-
*
|
149
|
-
* @example
|
150
|
-
* // Replace a resource with timeout
|
151
|
-
* const response = await httpPut({
|
152
|
-
* url: 'https://api.example.com/documents/456',
|
153
|
-
* body: documentData,
|
154
|
-
* timeoutMs: 10000 // 10 seconds timeout
|
155
|
-
* });
|
156
|
-
*/
|
157
|
-
export async function httpPut(options) {
|
158
|
-
return await httpRequest({
|
159
|
-
...options,
|
160
|
-
method: METHOD_PUT
|
161
|
-
});
|
162
|
-
}
|
163
|
-
|
164
|
-
/**
|
165
|
-
* Make a DELETE request
|
166
|
-
*
|
167
|
-
* This function performs an HTTP DELETE request, typically used for
|
168
|
-
* removing resources. It supports optional headers, credentials,
|
169
|
-
* and timeout functionality.
|
170
|
-
*
|
171
|
-
* @param {import('./typedef').HttpRequestOptions} options
|
172
|
-
* Request configuration options
|
173
|
-
*
|
174
|
-
* @returns {Promise<Response>} Response promise
|
175
|
-
*
|
176
|
-
* @example
|
177
|
-
* // Delete a user resource
|
178
|
-
* const response = await httpDelete({
|
179
|
-
* url: 'https://api.example.com/users/123'
|
180
|
-
* });
|
181
|
-
*
|
182
|
-
* @example
|
183
|
-
* // Delete with authorization and timeout
|
184
|
-
* const response = await httpDelete({
|
185
|
-
* url: 'https://api.example.com/posts/456',
|
186
|
-
* headers: { 'authorization': 'Bearer token123' },
|
187
|
-
* timeoutMs: 5000
|
188
|
-
* });
|
189
|
-
*/
|
190
|
-
export async function httpDelete(options) {
|
191
|
-
return await httpRequest({
|
192
|
-
...options,
|
193
|
-
method: METHOD_DELETE
|
194
|
-
});
|
195
|
-
}
|
196
|
-
|
197
|
-
/**
|
198
|
-
* Make a PATCH request
|
199
|
-
*
|
200
|
-
* This function performs an HTTP PATCH request, typically used for
|
201
|
-
* partial updates to resources. It supports optional body, headers,
|
202
|
-
* credentials, and timeout functionality.
|
203
|
-
*
|
204
|
-
* @param {import('./typedef').HttpRequestOptions} options
|
205
|
-
* Request configuration options
|
206
|
-
*
|
207
|
-
* @returns {Promise<Response>} Response promise
|
208
|
-
*
|
209
|
-
* @example
|
210
|
-
* // Partially update a user's email
|
211
|
-
* const response = await httpPatch({
|
212
|
-
* url: 'https://api.example.com/users/123',
|
213
|
-
* body: JSON.stringify({ email: 'newemail@example.com' }),
|
214
|
-
* headers: { 'content-type': 'application/json' }
|
215
|
-
* });
|
216
|
-
*
|
217
|
-
* @example
|
218
|
-
* // Apply JSON Patch operations
|
219
|
-
* const response = await httpPatch({
|
220
|
-
* url: 'https://api.example.com/documents/456',
|
221
|
-
* body: JSON.stringify([
|
222
|
-
* { op: 'replace', path: '/title', value: 'New Title' },
|
223
|
-
* { op: 'add', path: '/tags/-', value: 'updated' }
|
224
|
-
* ]),
|
225
|
-
* headers: { 'content-type': 'application/json-patch+json' }
|
226
|
-
* });
|
227
|
-
*/
|
228
|
-
export async function httpPatch(options) {
|
229
|
-
return await httpRequest({
|
230
|
-
...options,
|
231
|
-
method: METHOD_PATCH
|
232
|
-
});
|
233
|
-
}
|
234
|
-
|
235
|
-
/**
|
236
|
-
* Make an OPTIONS request
|
237
|
-
*
|
238
|
-
* This function performs an HTTP OPTIONS request, typically used for
|
239
|
-
* discovering allowed methods on a resource or for CORS preflight requests.
|
240
|
-
* It supports optional headers, credentials, and timeout functionality.
|
241
|
-
*
|
242
|
-
* @param {import('./typedef').HttpRequestOptions} options
|
243
|
-
* Request configuration options
|
244
|
-
*
|
245
|
-
* @returns {Promise<Response>} Response promise
|
246
|
-
*
|
247
|
-
* @example
|
248
|
-
* // Check allowed methods on a resource
|
249
|
-
* const response = await httpOptions({
|
250
|
-
* url: 'https://api.example.com/users/123'
|
251
|
-
* });
|
252
|
-
*
|
253
|
-
* const allowedMethods = response.headers.get('Allow');
|
254
|
-
* console.log('Allowed methods:', allowedMethods);
|
255
|
-
*
|
256
|
-
* @example
|
257
|
-
* // CORS preflight check
|
258
|
-
* const response = await httpOptions({
|
259
|
-
* url: 'https://api.example.com/data',
|
260
|
-
* headers: {
|
261
|
-
* 'Access-Control-Request-Method': 'POST',
|
262
|
-
* 'Access-Control-Request-Headers': 'Content-Type'
|
263
|
-
* }
|
264
|
-
* });
|
265
|
-
*/
|
266
|
-
export async function httpOptions(options) {
|
267
|
-
return await httpRequest({
|
268
|
-
...options,
|
269
|
-
method: METHOD_OPTIONS
|
270
|
-
});
|
271
|
-
}
|
272
|
-
|
273
|
-
/**
|
274
|
-
* Make a HEAD request
|
275
|
-
*
|
276
|
-
* This function performs an HTTP HEAD request, which returns only the
|
277
|
-
* headers of a response without the body. It's useful for checking
|
278
|
-
* resource existence, getting metadata, or cache validation.
|
279
|
-
*
|
280
|
-
* @param {import('./typedef').HttpRequestOptions} options
|
281
|
-
* Request configuration options
|
282
|
-
*
|
283
|
-
* @returns {Promise<Response>} Response promise (with empty body)
|
284
|
-
*
|
285
|
-
* @example
|
286
|
-
* // Check if a resource exists
|
287
|
-
* const response = await httpHead({
|
288
|
-
* url: 'https://api.example.com/users/123'
|
289
|
-
* });
|
290
|
-
*
|
291
|
-
* if (response.ok) {
|
292
|
-
* console.log('User exists');
|
293
|
-
* console.log('Last modified:', response.headers.get('Last-Modified'));
|
294
|
-
* }
|
295
|
-
*
|
296
|
-
* @example
|
297
|
-
* // Get content length without downloading
|
298
|
-
* const response = await httpHead({
|
299
|
-
* url: 'https://api.example.com/large-file.zip'
|
300
|
-
* });
|
301
|
-
*
|
302
|
-
* const contentLength = response.headers.get('Content-Length');
|
303
|
-
* console.log('File size:', contentLength, 'bytes');
|
304
|
-
*
|
305
|
-
* @example
|
306
|
-
* // Cache validation
|
307
|
-
* const response = await httpHead({
|
308
|
-
* url: 'https://api.example.com/data',
|
309
|
-
* headers: { 'If-None-Match': '"cached-etag-value"' }
|
310
|
-
* });
|
311
|
-
*
|
312
|
-
* if (response.status === 304) {
|
313
|
-
* console.log('Cache is still valid');
|
314
|
-
* }
|
315
|
-
*/
|
316
|
-
export async function httpHead(options) {
|
317
|
-
return await httpRequest({
|
318
|
-
...options,
|
319
|
-
method: METHOD_HEAD
|
320
|
-
});
|
321
|
-
}
|
322
|
-
|
323
|
-
// -----------------------------------------------------------------------------
|
324
|
-
|
325
|
-
/**
|
326
|
-
* Make an HTTP request (low-level function)
|
327
|
-
*
|
328
|
-
* This is a low-level function that powers httpGet and httpPost.
|
329
|
-
* It provides complete control over request configuration.
|
330
|
-
*
|
331
|
-
* @param {import('./typedef').HttpRequestOptions} options
|
332
|
-
* Request configuration options
|
333
|
-
*
|
334
|
-
* @throws {TypeError} If a network error occurred
|
335
|
-
* @returns {Promise<Response>} Response promise
|
336
|
-
*
|
337
|
-
* @example
|
338
|
-
* // Custom HTTP request with PUT method
|
339
|
-
* const response = await httpRequest({
|
340
|
-
* method: 'PUT',
|
341
|
-
* url: 'https://api.example.com/resources/123',
|
342
|
-
* body: JSON.stringify({ status: 'updated' }),
|
343
|
-
* headers: { 'content-type': 'application/json' },
|
344
|
-
* withCredentials: true
|
345
|
-
* });
|
346
|
-
*
|
347
|
-
* // Check if response was successful
|
348
|
-
* if (response.ok) {
|
349
|
-
* // Process response
|
350
|
-
* } else {
|
351
|
-
* // Handle error based on status
|
352
|
-
* }
|
353
|
-
*/
|
354
|
-
export async function httpRequest(options) {
|
355
|
-
// Apply default configuration
|
356
|
-
const config = { ...DEFAULT_HTTP_CONFIG, ...options };
|
357
|
-
|
358
|
-
const {
|
359
|
-
method,
|
360
|
-
url: rawUrl,
|
361
|
-
urlSearchParams,
|
362
|
-
body,
|
363
|
-
headers,
|
364
|
-
withCredentials,
|
365
|
-
requestHandler,
|
366
|
-
timeoutMs,
|
367
|
-
mode,
|
368
|
-
cache,
|
369
|
-
redirect,
|
370
|
-
referrerPolicy,
|
371
|
-
cacheEnabled
|
372
|
-
} = config;
|
373
|
-
|
374
|
-
const url = toURL(rawUrl);
|
375
|
-
|
376
|
-
// console.debug(`http:load [${url.pathname}]`);
|
377
|
-
|
378
|
-
// Only consider caching for GET requests
|
379
|
-
const shouldAttemptCache = cacheEnabled && method === METHOD_GET;
|
380
|
-
|
381
|
-
// Try to get from cache if appropriate
|
382
|
-
if (shouldAttemptCache && cache !== 'no-store' && cache !== 'reload') {
|
383
|
-
const cacheKeyParams = { url, ...headers };
|
384
|
-
const cachedResponse = await getCachedResponse(cacheKeyParams);
|
385
|
-
|
386
|
-
if (!isTestEnv) {
|
387
|
-
if (cachedResponse) {
|
388
|
-
console.debug(`http:cache-hit [${url.pathname}]`);
|
389
|
-
return cachedResponse;
|
390
|
-
} else {
|
391
|
-
console.debug(`http:cache-miss [${url.pathname}]`);
|
392
|
-
}
|
393
|
-
}
|
394
|
-
}
|
395
|
-
|
396
|
-
// @see https://developer.mozilla.org/en-US/docs/Web/API/Headers
|
397
|
-
const requestHeaders = new Headers();
|
398
|
-
|
399
|
-
if (headers) {
|
400
|
-
setRequestHeaders(requestHeaders, headers);
|
401
|
-
|
402
|
-
if (
|
403
|
-
headers[CONTENT_TYPE] === APPLICATION_JSON &&
|
404
|
-
typeof body !== 'string'
|
405
|
-
) {
|
406
|
-
throw new Error(
|
407
|
-
`Trying to send request with [content-type:${APPLICATION_JSON}], ` +
|
408
|
-
'but body is not a (JSON encoded) string.'
|
409
|
-
);
|
410
|
-
}
|
411
|
-
// IDEA: try to decode the body to catch errors on client side
|
412
|
-
}
|
413
|
-
|
414
|
-
/** @type {RequestInit} */
|
415
|
-
const init = {
|
416
|
-
mode,
|
417
|
-
cache,
|
418
|
-
credentials: withCredentials ? 'include' : 'omit',
|
419
|
-
redirect,
|
420
|
-
referrerPolicy,
|
421
|
-
headers: requestHeaders
|
422
|
-
};
|
423
|
-
|
424
|
-
// Allow search params also for other request types than GET
|
425
|
-
if (urlSearchParams) {
|
426
|
-
if (!(urlSearchParams instanceof URLSearchParams)) {
|
427
|
-
throw new Error(
|
428
|
-
'Invalid parameter [urlSearchParams] ' +
|
429
|
-
'(expected instanceof URLSearchParams)'
|
430
|
-
);
|
431
|
-
}
|
432
|
-
|
433
|
-
const existingParams = url.searchParams;
|
434
|
-
|
435
|
-
for (const [name, value] of urlSearchParams.entries()) {
|
436
|
-
if (existingParams.has(name)) {
|
437
|
-
throw new Error(
|
438
|
-
`Cannot set URL search parameter [${name}] ` +
|
439
|
-
`in url [${url.href}] (already set)`
|
440
|
-
);
|
441
|
-
}
|
442
|
-
|
443
|
-
existingParams.set(name, value);
|
444
|
-
} // end for
|
445
|
-
}
|
446
|
-
|
447
|
-
//
|
448
|
-
// Sort search params to make the url nicer
|
449
|
-
//
|
450
|
-
url.searchParams.sort();
|
451
|
-
|
452
|
-
init.method = method;
|
453
|
-
|
454
|
-
if (METHOD_POST === method) {
|
455
|
-
init.body = body || null; /* : JSON.stringify( body ) */
|
456
|
-
}
|
457
|
-
|
458
|
-
// @see https://developer.mozilla.org/en-US/docs/Web/API/Request/Request
|
459
|
-
const request = new Request(url, init);
|
460
|
-
|
461
|
-
// @see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/abort
|
462
|
-
const controller = new AbortController();
|
463
|
-
const signal = controller.signal;
|
464
|
-
|
465
|
-
//
|
466
|
-
// A fetch() promise will reject with a TypeError when a network error
|
467
|
-
// is encountered or CORS is misconfigured on the server-side,
|
468
|
-
// although this usually means permission issues or similar
|
469
|
-
// — a 404 does not constitute a network error, for example.
|
470
|
-
// An accurate check for a successful fetch() would include checking
|
471
|
-
// that the promise resolved, then checking that the Response.ok property
|
472
|
-
// has a value of true. The code would look something like this:
|
473
|
-
//
|
474
|
-
// fetch()
|
475
|
-
// .then( () => {
|
476
|
-
// if( !response.ok ) {
|
477
|
-
// throw new Error('Network response was not OK');
|
478
|
-
// }
|
479
|
-
// ...
|
480
|
-
// }
|
481
|
-
// .catch((error) => { .. }
|
482
|
-
//
|
483
|
-
|
484
|
-
const promise = fetch(request, { signal });
|
485
|
-
|
486
|
-
if (requestHandler || timeoutMs) {
|
487
|
-
/**
|
488
|
-
* @type {(reason?: any) => void}
|
489
|
-
*/
|
490
|
-
const abort = (reason) => {
|
491
|
-
if (!reason) {
|
492
|
-
reason = new AbortError(`Request [${url.href}] aborted`);
|
493
|
-
}
|
494
|
-
|
495
|
-
controller.abort(reason);
|
496
|
-
};
|
497
|
-
|
498
|
-
/**
|
499
|
-
* Function that can be used to set a timeout on a request
|
500
|
-
*
|
501
|
-
* @param {number} delayMs - Milliseconds to wait before timeout
|
502
|
-
*/
|
503
|
-
const timeout = (delayMs = 10000) => {
|
504
|
-
expect.positiveNumber(delayMs);
|
505
|
-
|
506
|
-
const timerId = setTimeout(() => {
|
507
|
-
controller.abort(
|
508
|
-
new TimeoutError(`Request [${url.href}] timed out [${delayMs}]`)
|
509
|
-
);
|
510
|
-
}, delayMs);
|
511
|
-
|
512
|
-
promise.finally(() => {
|
513
|
-
clearTimeout(timerId);
|
514
|
-
});
|
515
|
-
};
|
516
|
-
|
517
|
-
if (timeoutMs) {
|
518
|
-
timeout(timeoutMs);
|
519
|
-
}
|
520
|
-
|
521
|
-
if (requestHandler) {
|
522
|
-
expect.function(requestHandler);
|
523
|
-
|
524
|
-
requestHandler({ controller, abort, timeout });
|
525
|
-
}
|
526
|
-
}
|
527
|
-
|
528
|
-
// Wait for the response and check it
|
529
|
-
const response = await waitForAndCheckResponse(promise, url);
|
530
|
-
|
531
|
-
// If caching is enabled, store the response in cache
|
532
|
-
if (shouldAttemptCache && response.ok) {
|
533
|
-
// Extract cache control headers
|
534
|
-
const cacheControl = response.headers.get('Cache-Control') || '';
|
535
|
-
const etag = response.headers.get('ETag');
|
536
|
-
const lastModified = response.headers.get('Last-Modified');
|
537
|
-
|
538
|
-
// Parse cache-control directives
|
539
|
-
const directives = {};
|
540
|
-
cacheControl.split(',').forEach((directive) => {
|
541
|
-
const [key, value] = directive.trim().split('=');
|
542
|
-
directives[key.toLowerCase()] = value !== undefined ? value : true;
|
543
|
-
});
|
544
|
-
|
545
|
-
// Determine if cacheable
|
546
|
-
const isCacheable = !directives['no-store'] && !directives['private'];
|
547
|
-
|
548
|
-
if (isCacheable) {
|
549
|
-
// Calculate expiration time
|
550
|
-
let expires = null;
|
551
|
-
if (directives['max-age']) {
|
552
|
-
const maxAge = parseInt(directives['max-age'], 10);
|
553
|
-
expires = Date.now() + maxAge * 1000;
|
554
|
-
} else if (response.headers.get('Expires')) {
|
555
|
-
expires = new Date(response.headers.get('Expires')).getTime();
|
556
|
-
}
|
557
|
-
|
558
|
-
// Create stale info
|
559
|
-
// const staleInfo = {
|
560
|
-
// isStale: false,
|
561
|
-
// fresh: null,
|
562
|
-
// timestamp: Date.now(),
|
563
|
-
// expires
|
564
|
-
// };
|
565
|
-
|
566
|
-
// Store response in cache
|
567
|
-
const cacheKeyParams = { url, ...headers };
|
568
|
-
await storeResponseInCache(cacheKeyParams, response.clone(), {
|
569
|
-
etag,
|
570
|
-
lastModified,
|
571
|
-
expires,
|
572
|
-
immutable: directives['immutable'] || false
|
573
|
-
});
|
574
|
-
}
|
575
|
-
}
|
576
|
-
|
577
|
-
return response;
|
578
|
-
}
|
1
|
+
import {
|
2
|
+
METHOD_GET,
|
3
|
+
METHOD_POST,
|
4
|
+
METHOD_PUT,
|
5
|
+
METHOD_DELETE,
|
6
|
+
METHOD_PATCH,
|
7
|
+
METHOD_OPTIONS,
|
8
|
+
METHOD_HEAD
|
9
|
+
} from '../../constants/http/methods.js';
|
10
|
+
|
11
|
+
import { APPLICATION_JSON } from '../../constants/mime/application.js';
|
12
|
+
import { CONTENT_TYPE } from '../../constants/http/headers.js';
|
13
|
+
|
14
|
+
import { AbortError, TimeoutError } from '../../constants/errors/api.js';
|
15
|
+
|
16
|
+
import * as expect from '../expect/index.js';
|
17
|
+
|
18
|
+
import { toURL } from './url.js';
|
19
|
+
import { setRequestHeaders } from './headers.js';
|
20
|
+
import { waitForAndCheckResponse } from './response.js';
|
21
|
+
|
22
|
+
import { getCachedResponse, storeResponseInCache } from './caching.js';
|
23
|
+
|
24
|
+
import { isTestEnv } from '../env';
|
25
|
+
|
26
|
+
/**
|
27
|
+
* Default configuration for HTTP requests
|
28
|
+
*
|
29
|
+
* This object contains default settings used by the HTTP request functions.
|
30
|
+
* It can be used as a reference for available options and their default values.
|
31
|
+
*
|
32
|
+
* @type {Object}
|
33
|
+
*/
|
34
|
+
export const DEFAULT_HTTP_CONFIG = {
|
35
|
+
// Request
|
36
|
+
method: METHOD_GET,
|
37
|
+
urlSearchParams: null,
|
38
|
+
body: null,
|
39
|
+
headers: null,
|
40
|
+
withCredentials: false,
|
41
|
+
timeoutMs: null, // No timeout by default
|
42
|
+
|
43
|
+
// Fetch
|
44
|
+
mode: 'cors',
|
45
|
+
cache: 'no-cache',
|
46
|
+
redirect: 'follow',
|
47
|
+
referrerPolicy: 'no-referrer',
|
48
|
+
|
49
|
+
// Cache
|
50
|
+
cacheEnabled: true
|
51
|
+
};
|
52
|
+
|
53
|
+
/**
|
54
|
+
* Make a GET request
|
55
|
+
*
|
56
|
+
* This function performs an HTTP GET request with optional parameters,
|
57
|
+
* headers, credentials, and timeout functionality.
|
58
|
+
*
|
59
|
+
* @param {import('./typedef').HttpRequestOptions} options
|
60
|
+
* Request configuration options
|
61
|
+
*
|
62
|
+
* @returns {Promise<Response>} Response promise
|
63
|
+
*
|
64
|
+
* @example
|
65
|
+
* // Basic GET request
|
66
|
+
* const response = await httpGet({
|
67
|
+
* url: 'https://api.example.com/data'
|
68
|
+
* });
|
69
|
+
*
|
70
|
+
* @example
|
71
|
+
* // GET request with URL parameters and timeout
|
72
|
+
* const response = await httpGet({
|
73
|
+
* url: 'https://api.example.com/search',
|
74
|
+
* urlSearchParams: new URLSearchParams({ q: 'search term' }),
|
75
|
+
* timeoutMs: 5000
|
76
|
+
* });
|
77
|
+
*
|
78
|
+
* @example
|
79
|
+
* // GET request with abort capability
|
80
|
+
* const response = await httpGet({
|
81
|
+
* url: 'https://api.example.com/large-data',
|
82
|
+
* requestHandler: ({ abort }) => {
|
83
|
+
* // Store abort function for later use
|
84
|
+
* window.abortDataRequest = abort;
|
85
|
+
* }
|
86
|
+
* });
|
87
|
+
*/
|
88
|
+
export async function httpGet(options) {
|
89
|
+
return await httpRequest({
|
90
|
+
...options,
|
91
|
+
method: METHOD_GET
|
92
|
+
});
|
93
|
+
}
|
94
|
+
|
95
|
+
/**
|
96
|
+
* Make a POST request
|
97
|
+
*
|
98
|
+
* This function performs an HTTP POST request with optional body,
|
99
|
+
* headers, credentials, and timeout functionality.
|
100
|
+
*
|
101
|
+
* @param {import('./typedef').HttpRequestOptions} options
|
102
|
+
* Request configuration options
|
103
|
+
*
|
104
|
+
* @returns {Promise<Response>} Response promise
|
105
|
+
*
|
106
|
+
* @example
|
107
|
+
* // Basic POST request with JSON data
|
108
|
+
* const response = await httpPost({
|
109
|
+
* url: 'https://api.example.com/users',
|
110
|
+
* body: JSON.stringify({ name: 'John Doe', email: 'john@example.com' }),
|
111
|
+
* headers: { 'content-type': 'application/json' }
|
112
|
+
* });
|
113
|
+
*
|
114
|
+
* @example
|
115
|
+
* // POST request with timeout
|
116
|
+
* const response = await httpPost({
|
117
|
+
* url: 'https://api.example.com/upload',
|
118
|
+
* body: formData,
|
119
|
+
* timeoutMs: 30000 // 30 seconds timeout
|
120
|
+
* });
|
121
|
+
*/
|
122
|
+
export async function httpPost(options) {
|
123
|
+
return await httpRequest({
|
124
|
+
...options,
|
125
|
+
method: METHOD_POST
|
126
|
+
});
|
127
|
+
}
|
128
|
+
|
129
|
+
/**
|
130
|
+
* Make a PUT request
|
131
|
+
*
|
132
|
+
* This function performs an HTTP PUT request, typically used for updating
|
133
|
+
* or replacing resources. It supports optional body, headers, credentials,
|
134
|
+
* and timeout functionality.
|
135
|
+
*
|
136
|
+
* @param {import('./typedef').HttpRequestOptions} options
|
137
|
+
* Request configuration options
|
138
|
+
*
|
139
|
+
* @returns {Promise<Response>} Response promise
|
140
|
+
*
|
141
|
+
* @example
|
142
|
+
* // Update a user resource
|
143
|
+
* const response = await httpPut({
|
144
|
+
* url: 'https://api.example.com/users/123',
|
145
|
+
* body: JSON.stringify({ name: 'John Doe', email: 'john.doe@example.com' }),
|
146
|
+
* headers: { 'content-type': 'application/json' }
|
147
|
+
* });
|
148
|
+
*
|
149
|
+
* @example
|
150
|
+
* // Replace a resource with timeout
|
151
|
+
* const response = await httpPut({
|
152
|
+
* url: 'https://api.example.com/documents/456',
|
153
|
+
* body: documentData,
|
154
|
+
* timeoutMs: 10000 // 10 seconds timeout
|
155
|
+
* });
|
156
|
+
*/
|
157
|
+
export async function httpPut(options) {
|
158
|
+
return await httpRequest({
|
159
|
+
...options,
|
160
|
+
method: METHOD_PUT
|
161
|
+
});
|
162
|
+
}
|
163
|
+
|
164
|
+
/**
|
165
|
+
* Make a DELETE request
|
166
|
+
*
|
167
|
+
* This function performs an HTTP DELETE request, typically used for
|
168
|
+
* removing resources. It supports optional headers, credentials,
|
169
|
+
* and timeout functionality.
|
170
|
+
*
|
171
|
+
* @param {import('./typedef').HttpRequestOptions} options
|
172
|
+
* Request configuration options
|
173
|
+
*
|
174
|
+
* @returns {Promise<Response>} Response promise
|
175
|
+
*
|
176
|
+
* @example
|
177
|
+
* // Delete a user resource
|
178
|
+
* const response = await httpDelete({
|
179
|
+
* url: 'https://api.example.com/users/123'
|
180
|
+
* });
|
181
|
+
*
|
182
|
+
* @example
|
183
|
+
* // Delete with authorization and timeout
|
184
|
+
* const response = await httpDelete({
|
185
|
+
* url: 'https://api.example.com/posts/456',
|
186
|
+
* headers: { 'authorization': 'Bearer token123' },
|
187
|
+
* timeoutMs: 5000
|
188
|
+
* });
|
189
|
+
*/
|
190
|
+
export async function httpDelete(options) {
|
191
|
+
return await httpRequest({
|
192
|
+
...options,
|
193
|
+
method: METHOD_DELETE
|
194
|
+
});
|
195
|
+
}
|
196
|
+
|
197
|
+
/**
|
198
|
+
* Make a PATCH request
|
199
|
+
*
|
200
|
+
* This function performs an HTTP PATCH request, typically used for
|
201
|
+
* partial updates to resources. It supports optional body, headers,
|
202
|
+
* credentials, and timeout functionality.
|
203
|
+
*
|
204
|
+
* @param {import('./typedef').HttpRequestOptions} options
|
205
|
+
* Request configuration options
|
206
|
+
*
|
207
|
+
* @returns {Promise<Response>} Response promise
|
208
|
+
*
|
209
|
+
* @example
|
210
|
+
* // Partially update a user's email
|
211
|
+
* const response = await httpPatch({
|
212
|
+
* url: 'https://api.example.com/users/123',
|
213
|
+
* body: JSON.stringify({ email: 'newemail@example.com' }),
|
214
|
+
* headers: { 'content-type': 'application/json' }
|
215
|
+
* });
|
216
|
+
*
|
217
|
+
* @example
|
218
|
+
* // Apply JSON Patch operations
|
219
|
+
* const response = await httpPatch({
|
220
|
+
* url: 'https://api.example.com/documents/456',
|
221
|
+
* body: JSON.stringify([
|
222
|
+
* { op: 'replace', path: '/title', value: 'New Title' },
|
223
|
+
* { op: 'add', path: '/tags/-', value: 'updated' }
|
224
|
+
* ]),
|
225
|
+
* headers: { 'content-type': 'application/json-patch+json' }
|
226
|
+
* });
|
227
|
+
*/
|
228
|
+
export async function httpPatch(options) {
|
229
|
+
return await httpRequest({
|
230
|
+
...options,
|
231
|
+
method: METHOD_PATCH
|
232
|
+
});
|
233
|
+
}
|
234
|
+
|
235
|
+
/**
|
236
|
+
* Make an OPTIONS request
|
237
|
+
*
|
238
|
+
* This function performs an HTTP OPTIONS request, typically used for
|
239
|
+
* discovering allowed methods on a resource or for CORS preflight requests.
|
240
|
+
* It supports optional headers, credentials, and timeout functionality.
|
241
|
+
*
|
242
|
+
* @param {import('./typedef').HttpRequestOptions} options
|
243
|
+
* Request configuration options
|
244
|
+
*
|
245
|
+
* @returns {Promise<Response>} Response promise
|
246
|
+
*
|
247
|
+
* @example
|
248
|
+
* // Check allowed methods on a resource
|
249
|
+
* const response = await httpOptions({
|
250
|
+
* url: 'https://api.example.com/users/123'
|
251
|
+
* });
|
252
|
+
*
|
253
|
+
* const allowedMethods = response.headers.get('Allow');
|
254
|
+
* console.log('Allowed methods:', allowedMethods);
|
255
|
+
*
|
256
|
+
* @example
|
257
|
+
* // CORS preflight check
|
258
|
+
* const response = await httpOptions({
|
259
|
+
* url: 'https://api.example.com/data',
|
260
|
+
* headers: {
|
261
|
+
* 'Access-Control-Request-Method': 'POST',
|
262
|
+
* 'Access-Control-Request-Headers': 'Content-Type'
|
263
|
+
* }
|
264
|
+
* });
|
265
|
+
*/
|
266
|
+
export async function httpOptions(options) {
|
267
|
+
return await httpRequest({
|
268
|
+
...options,
|
269
|
+
method: METHOD_OPTIONS
|
270
|
+
});
|
271
|
+
}
|
272
|
+
|
273
|
+
/**
|
274
|
+
* Make a HEAD request
|
275
|
+
*
|
276
|
+
* This function performs an HTTP HEAD request, which returns only the
|
277
|
+
* headers of a response without the body. It's useful for checking
|
278
|
+
* resource existence, getting metadata, or cache validation.
|
279
|
+
*
|
280
|
+
* @param {import('./typedef').HttpRequestOptions} options
|
281
|
+
* Request configuration options
|
282
|
+
*
|
283
|
+
* @returns {Promise<Response>} Response promise (with empty body)
|
284
|
+
*
|
285
|
+
* @example
|
286
|
+
* // Check if a resource exists
|
287
|
+
* const response = await httpHead({
|
288
|
+
* url: 'https://api.example.com/users/123'
|
289
|
+
* });
|
290
|
+
*
|
291
|
+
* if (response.ok) {
|
292
|
+
* console.log('User exists');
|
293
|
+
* console.log('Last modified:', response.headers.get('Last-Modified'));
|
294
|
+
* }
|
295
|
+
*
|
296
|
+
* @example
|
297
|
+
* // Get content length without downloading
|
298
|
+
* const response = await httpHead({
|
299
|
+
* url: 'https://api.example.com/large-file.zip'
|
300
|
+
* });
|
301
|
+
*
|
302
|
+
* const contentLength = response.headers.get('Content-Length');
|
303
|
+
* console.log('File size:', contentLength, 'bytes');
|
304
|
+
*
|
305
|
+
* @example
|
306
|
+
* // Cache validation
|
307
|
+
* const response = await httpHead({
|
308
|
+
* url: 'https://api.example.com/data',
|
309
|
+
* headers: { 'If-None-Match': '"cached-etag-value"' }
|
310
|
+
* });
|
311
|
+
*
|
312
|
+
* if (response.status === 304) {
|
313
|
+
* console.log('Cache is still valid');
|
314
|
+
* }
|
315
|
+
*/
|
316
|
+
export async function httpHead(options) {
|
317
|
+
return await httpRequest({
|
318
|
+
...options,
|
319
|
+
method: METHOD_HEAD
|
320
|
+
});
|
321
|
+
}
|
322
|
+
|
323
|
+
// -----------------------------------------------------------------------------
|
324
|
+
|
325
|
+
/**
|
326
|
+
* Make an HTTP request (low-level function)
|
327
|
+
*
|
328
|
+
* This is a low-level function that powers httpGet and httpPost.
|
329
|
+
* It provides complete control over request configuration.
|
330
|
+
*
|
331
|
+
* @param {import('./typedef').HttpRequestOptions} options
|
332
|
+
* Request configuration options
|
333
|
+
*
|
334
|
+
* @throws {TypeError} If a network error occurred
|
335
|
+
* @returns {Promise<Response>} Response promise
|
336
|
+
*
|
337
|
+
* @example
|
338
|
+
* // Custom HTTP request with PUT method
|
339
|
+
* const response = await httpRequest({
|
340
|
+
* method: 'PUT',
|
341
|
+
* url: 'https://api.example.com/resources/123',
|
342
|
+
* body: JSON.stringify({ status: 'updated' }),
|
343
|
+
* headers: { 'content-type': 'application/json' },
|
344
|
+
* withCredentials: true
|
345
|
+
* });
|
346
|
+
*
|
347
|
+
* // Check if response was successful
|
348
|
+
* if (response.ok) {
|
349
|
+
* // Process response
|
350
|
+
* } else {
|
351
|
+
* // Handle error based on status
|
352
|
+
* }
|
353
|
+
*/
|
354
|
+
export async function httpRequest(options) {
|
355
|
+
// Apply default configuration
|
356
|
+
const config = { ...DEFAULT_HTTP_CONFIG, ...options };
|
357
|
+
|
358
|
+
const {
|
359
|
+
method,
|
360
|
+
url: rawUrl,
|
361
|
+
urlSearchParams,
|
362
|
+
body,
|
363
|
+
headers,
|
364
|
+
withCredentials,
|
365
|
+
requestHandler,
|
366
|
+
timeoutMs,
|
367
|
+
mode,
|
368
|
+
cache,
|
369
|
+
redirect,
|
370
|
+
referrerPolicy,
|
371
|
+
cacheEnabled
|
372
|
+
} = config;
|
373
|
+
|
374
|
+
const url = toURL(rawUrl);
|
375
|
+
|
376
|
+
// console.debug(`http:load [${url.pathname}]`);
|
377
|
+
|
378
|
+
// Only consider caching for GET requests
|
379
|
+
const shouldAttemptCache = cacheEnabled && method === METHOD_GET;
|
380
|
+
|
381
|
+
// Try to get from cache if appropriate
|
382
|
+
if (shouldAttemptCache && cache !== 'no-store' && cache !== 'reload') {
|
383
|
+
const cacheKeyParams = { url, ...headers };
|
384
|
+
const cachedResponse = await getCachedResponse(cacheKeyParams);
|
385
|
+
|
386
|
+
if (!isTestEnv) {
|
387
|
+
if (cachedResponse) {
|
388
|
+
console.debug(`http:cache-hit [${url.pathname}]`);
|
389
|
+
return cachedResponse;
|
390
|
+
} else {
|
391
|
+
console.debug(`http:cache-miss [${url.pathname}]`);
|
392
|
+
}
|
393
|
+
}
|
394
|
+
}
|
395
|
+
|
396
|
+
// @see https://developer.mozilla.org/en-US/docs/Web/API/Headers
|
397
|
+
const requestHeaders = new Headers();
|
398
|
+
|
399
|
+
if (headers) {
|
400
|
+
setRequestHeaders(requestHeaders, headers);
|
401
|
+
|
402
|
+
if (
|
403
|
+
headers[CONTENT_TYPE] === APPLICATION_JSON &&
|
404
|
+
typeof body !== 'string'
|
405
|
+
) {
|
406
|
+
throw new Error(
|
407
|
+
`Trying to send request with [content-type:${APPLICATION_JSON}], ` +
|
408
|
+
'but body is not a (JSON encoded) string.'
|
409
|
+
);
|
410
|
+
}
|
411
|
+
// IDEA: try to decode the body to catch errors on client side
|
412
|
+
}
|
413
|
+
|
414
|
+
/** @type {RequestInit} */
|
415
|
+
const init = {
|
416
|
+
mode,
|
417
|
+
cache,
|
418
|
+
credentials: withCredentials ? 'include' : 'omit',
|
419
|
+
redirect,
|
420
|
+
referrerPolicy,
|
421
|
+
headers: requestHeaders
|
422
|
+
};
|
423
|
+
|
424
|
+
// Allow search params also for other request types than GET
|
425
|
+
if (urlSearchParams) {
|
426
|
+
if (!(urlSearchParams instanceof URLSearchParams)) {
|
427
|
+
throw new Error(
|
428
|
+
'Invalid parameter [urlSearchParams] ' +
|
429
|
+
'(expected instanceof URLSearchParams)'
|
430
|
+
);
|
431
|
+
}
|
432
|
+
|
433
|
+
const existingParams = url.searchParams;
|
434
|
+
|
435
|
+
for (const [name, value] of urlSearchParams.entries()) {
|
436
|
+
if (existingParams.has(name)) {
|
437
|
+
throw new Error(
|
438
|
+
`Cannot set URL search parameter [${name}] ` +
|
439
|
+
`in url [${url.href}] (already set)`
|
440
|
+
);
|
441
|
+
}
|
442
|
+
|
443
|
+
existingParams.set(name, value);
|
444
|
+
} // end for
|
445
|
+
}
|
446
|
+
|
447
|
+
//
|
448
|
+
// Sort search params to make the url nicer
|
449
|
+
//
|
450
|
+
url.searchParams.sort();
|
451
|
+
|
452
|
+
init.method = method;
|
453
|
+
|
454
|
+
if (METHOD_POST === method) {
|
455
|
+
init.body = body || null; /* : JSON.stringify( body ) */
|
456
|
+
}
|
457
|
+
|
458
|
+
// @see https://developer.mozilla.org/en-US/docs/Web/API/Request/Request
|
459
|
+
const request = new Request(url, init);
|
460
|
+
|
461
|
+
// @see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/abort
|
462
|
+
const controller = new AbortController();
|
463
|
+
const signal = controller.signal;
|
464
|
+
|
465
|
+
//
|
466
|
+
// A fetch() promise will reject with a TypeError when a network error
|
467
|
+
// is encountered or CORS is misconfigured on the server-side,
|
468
|
+
// although this usually means permission issues or similar
|
469
|
+
// — a 404 does not constitute a network error, for example.
|
470
|
+
// An accurate check for a successful fetch() would include checking
|
471
|
+
// that the promise resolved, then checking that the Response.ok property
|
472
|
+
// has a value of true. The code would look something like this:
|
473
|
+
//
|
474
|
+
// fetch()
|
475
|
+
// .then( () => {
|
476
|
+
// if( !response.ok ) {
|
477
|
+
// throw new Error('Network response was not OK');
|
478
|
+
// }
|
479
|
+
// ...
|
480
|
+
// }
|
481
|
+
// .catch((error) => { .. }
|
482
|
+
//
|
483
|
+
|
484
|
+
const promise = fetch(request, { signal });
|
485
|
+
|
486
|
+
if (requestHandler || timeoutMs) {
|
487
|
+
/**
|
488
|
+
* @type {(reason?: any) => void}
|
489
|
+
*/
|
490
|
+
const abort = (reason) => {
|
491
|
+
if (!reason) {
|
492
|
+
reason = new AbortError(`Request [${url.href}] aborted`);
|
493
|
+
}
|
494
|
+
|
495
|
+
controller.abort(reason);
|
496
|
+
};
|
497
|
+
|
498
|
+
/**
|
499
|
+
* Function that can be used to set a timeout on a request
|
500
|
+
*
|
501
|
+
* @param {number} delayMs - Milliseconds to wait before timeout
|
502
|
+
*/
|
503
|
+
const timeout = (delayMs = 10000) => {
|
504
|
+
expect.positiveNumber(delayMs);
|
505
|
+
|
506
|
+
const timerId = setTimeout(() => {
|
507
|
+
controller.abort(
|
508
|
+
new TimeoutError(`Request [${url.href}] timed out [${delayMs}]`)
|
509
|
+
);
|
510
|
+
}, delayMs);
|
511
|
+
|
512
|
+
promise.finally(() => {
|
513
|
+
clearTimeout(timerId);
|
514
|
+
});
|
515
|
+
};
|
516
|
+
|
517
|
+
if (timeoutMs) {
|
518
|
+
timeout(timeoutMs);
|
519
|
+
}
|
520
|
+
|
521
|
+
if (requestHandler) {
|
522
|
+
expect.function(requestHandler);
|
523
|
+
|
524
|
+
requestHandler({ controller, abort, timeout });
|
525
|
+
}
|
526
|
+
}
|
527
|
+
|
528
|
+
// Wait for the response and check it
|
529
|
+
const response = await waitForAndCheckResponse(promise, url);
|
530
|
+
|
531
|
+
// If caching is enabled, store the response in cache
|
532
|
+
if (shouldAttemptCache && response.ok) {
|
533
|
+
// Extract cache control headers
|
534
|
+
const cacheControl = response.headers.get('Cache-Control') || '';
|
535
|
+
const etag = response.headers.get('ETag');
|
536
|
+
const lastModified = response.headers.get('Last-Modified');
|
537
|
+
|
538
|
+
// Parse cache-control directives
|
539
|
+
const directives = {};
|
540
|
+
cacheControl.split(',').forEach((directive) => {
|
541
|
+
const [key, value] = directive.trim().split('=');
|
542
|
+
directives[key.toLowerCase()] = value !== undefined ? value : true;
|
543
|
+
});
|
544
|
+
|
545
|
+
// Determine if cacheable
|
546
|
+
const isCacheable = !directives['no-store'] && !directives['private'];
|
547
|
+
|
548
|
+
if (isCacheable) {
|
549
|
+
// Calculate expiration time
|
550
|
+
let expires = null;
|
551
|
+
if (directives['max-age']) {
|
552
|
+
const maxAge = parseInt(directives['max-age'], 10);
|
553
|
+
expires = Date.now() + maxAge * 1000;
|
554
|
+
} else if (response.headers.get('Expires')) {
|
555
|
+
expires = new Date(response.headers.get('Expires')).getTime();
|
556
|
+
}
|
557
|
+
|
558
|
+
// Create stale info
|
559
|
+
// const staleInfo = {
|
560
|
+
// isStale: false,
|
561
|
+
// fresh: null,
|
562
|
+
// timestamp: Date.now(),
|
563
|
+
// expires
|
564
|
+
// };
|
565
|
+
|
566
|
+
// Store response in cache
|
567
|
+
const cacheKeyParams = { url, ...headers };
|
568
|
+
await storeResponseInCache(cacheKeyParams, response.clone(), {
|
569
|
+
etag,
|
570
|
+
lastModified,
|
571
|
+
expires,
|
572
|
+
immutable: directives['immutable'] || false
|
573
|
+
});
|
574
|
+
}
|
575
|
+
}
|
576
|
+
|
577
|
+
return response;
|
578
|
+
}
|