@blueprint-ts/core 4.0.0-beta.9 → 4.1.0-beta.1
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/CHANGELOG.md +24 -0
- package/docs/.vitepress/config.ts +1 -0
- package/docs/services/pagination/index.md +1 -1
- package/docs/services/pagination/updating-rows.md +16 -0
- package/docs/services/requests/drivers.md +68 -3
- package/docs/services/requests/events.md +22 -0
- package/docs/services/requests/file-uploads.md +105 -0
- package/docs/services/requests/request-bodies.md +2 -0
- package/package.json +1 -1
- package/release-tool.json +1 -1
- package/src/pagination/BasePaginator.ts +19 -0
- package/src/requests/BaseRequest.ts +42 -3
- package/src/requests/RequestEvents.enum.ts +2 -1
- package/src/requests/contracts/DriverConfigContract.ts +2 -0
- package/src/requests/drivers/xhr/XMLHttpRequestDriver.ts +138 -0
- package/src/requests/drivers/xhr/XMLHttpRequestResponse.ts +95 -0
- package/src/requests/index.ts +8 -3
- package/src/requests/types/RequestUploadProgress.ts +6 -0
- package/src/vue/forms/validation/rules/EmailRule.ts +1 -1
- package/tests/service/bulkRequests/BulkRequestSender.test.ts +76 -0
- package/tests/service/bulkRequests/BulkRequestWrapper.test.ts +51 -0
- package/tests/service/pagination/BasePaginator.test.ts +100 -0
- package/tests/service/pagination/InfiniteScroller.test.ts +101 -0
- package/tests/service/pagination/PageAwarePaginator.test.ts +133 -0
- package/tests/service/pagination/StatePaginator.test.ts +76 -0
- package/tests/service/pagination/VueViewDrivers.test.ts +46 -0
- package/tests/service/pagination/dtos/StatePaginationDataDto.test.ts +14 -0
- package/tests/service/persistenceDrivers/PersistenceDrivers.test.ts +56 -0
- package/tests/service/requests/BaseRequest.test.ts +250 -0
- package/tests/service/requests/BodiesAndFactories.test.ts +28 -0
- package/tests/service/requests/Enums.test.ts +20 -0
- package/tests/service/requests/ErrorHandler.test.ts +45 -1
- package/tests/service/requests/RequestErrorRouter.test.ts +44 -0
- package/tests/service/requests/Responses.test.ts +83 -0
- package/tests/service/requests/exceptions/Exceptions.test.ts +43 -0
- package/tests/service/requests/fetch/FetchDriver.test.ts +76 -0
- package/tests/service/requests/fetch/FetchResponse.test.ts +21 -0
- package/tests/service/requests/xhr/XMLHttpRequestDriver.test.ts +178 -0
- package/tests/service/support/DeferredPromise.test.ts +40 -0
- package/tests/service/support/helpers.test.ts +37 -0
- package/tests/vue/composables/useConfirmDialog.test.ts +77 -0
- package/tests/vue/composables/useGlobalCheckbox.test.ts +126 -0
- package/tests/vue/composables/useIsEmpty.test.ts +18 -0
- package/tests/vue/composables/useIsOpen.test.ts +25 -0
- package/tests/vue/composables/useIsOpenFromVar.test.ts +22 -0
- package/tests/vue/composables/useModelWrapper.test.ts +30 -0
- package/tests/vue/composables/useOnOpen.test.ts +26 -0
- package/tests/vue/forms/PropertyAwareArray.test.ts +30 -0
- package/tests/vue/forms/validation/ValidationRules.test.ts +79 -0
- package/tests/vue/requests/VueRequestLoaders.test.ts +48 -0
- package/tests/vue/router/routeResourceBinding/RouteResourceUtils.test.ts +70 -0
- package/tests/vue/state/State.test.ts +151 -0
- package/vitest.config.ts +10 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,27 @@
|
|
|
1
|
+
## 4.1.0-beta.1 - 2026-03-21 (beta)
|
|
2
|
+
|
|
3
|
+
# [4.1.0-beta.1](/compare/v4.0.0...v4.1.0-beta.1) (2026-03-21)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **requests:** export HeaderValue type 15e55c1
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* **requests:** add XMLHttpRequest upload progress support 19123df
|
|
14
|
+
## v4.0.0 - 2026-03-01
|
|
15
|
+
|
|
16
|
+
# [4.0.0](/compare/v4.0.0-beta.10...v4.0.0) (2026-03-01)
|
|
17
|
+
## v4.0.0-beta.10 - 2026-02-28 (beta)
|
|
18
|
+
|
|
19
|
+
# [4.0.0-beta.10](/compare/v4.0.0-beta.9...v4.0.0-beta.10) (2026-02-28)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
### Features
|
|
23
|
+
|
|
24
|
+
* **pagination:** support local row removal without reload 3a66689
|
|
1
25
|
## v4.0.0-beta.9 - 2026-02-28 (beta)
|
|
2
26
|
|
|
3
27
|
# [4.0.0-beta.9](/compare/v4.0.0-beta.8...v4.0.0-beta.9) (2026-02-28)
|
|
@@ -24,6 +24,7 @@ export default defineConfig({
|
|
|
24
24
|
{ text: 'Drivers', link: '/services/requests/drivers' },
|
|
25
25
|
{ text: 'Responses', link: '/services/requests/responses' },
|
|
26
26
|
{ text: 'Request Bodies', link: '/services/requests/request-bodies' },
|
|
27
|
+
{ text: 'File Uploads', link: '/services/requests/file-uploads' },
|
|
27
28
|
{ text: 'Headers', link: '/services/requests/headers' },
|
|
28
29
|
{ text: 'Concurrency', link: '/services/requests/concurrency' },
|
|
29
30
|
{ text: 'Aborting Requests', link: '/services/requests/abort-requests' },
|
|
@@ -38,7 +38,7 @@ If you are using Vue, the library provides `VuePaginationDriverFactory` and `Vue
|
|
|
38
38
|
- `flush`: clears existing data before applying the next page
|
|
39
39
|
- `replace`: replaces existing data instead of appending (useful for infinite scroll)
|
|
40
40
|
|
|
41
|
-
`updateRows`
|
|
41
|
+
`updateRows` and `removeRows` are available on all paginators. See [Updating Rows](./updating-rows).
|
|
42
42
|
|
|
43
43
|
## Using Laravel Pagination
|
|
44
44
|
|
|
@@ -34,3 +34,19 @@ const updated = paginator.updateRows(
|
|
|
34
34
|
(row) => ({ ...row, updatedAt: new Date().toISOString() })
|
|
35
35
|
)
|
|
36
36
|
```
|
|
37
|
+
|
|
38
|
+
## Removing Rows
|
|
39
|
+
|
|
40
|
+
Use `removeRows` to delete items from the current page data without reloading. By default it also decrements `total`
|
|
41
|
+
by the number of removed rows. Set `adjustTotal: false` to skip that behavior.
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
// Remove a single item
|
|
45
|
+
const removed = paginator.removeRows((row) => row.id === targetId)
|
|
46
|
+
|
|
47
|
+
// Remove all drafts without adjusting total
|
|
48
|
+
const removedDrafts = paginator.removeRows(
|
|
49
|
+
(row) => row.status === 'draft',
|
|
50
|
+
{ adjustTotal: false }
|
|
51
|
+
)
|
|
52
|
+
```
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# Drivers
|
|
2
2
|
|
|
3
|
-
Requests are executed by a request driver. The library includes a
|
|
4
|
-
own by implementing `RequestDriverContract`.
|
|
3
|
+
Requests are executed by a request driver. The library includes a `FetchDriver`, an `XMLHttpRequestDriver`, and also
|
|
4
|
+
lets you provide your own by implementing `RequestDriverContract`.
|
|
5
5
|
|
|
6
|
-
##
|
|
6
|
+
## Fetch Driver
|
|
7
7
|
|
|
8
8
|
```typescript
|
|
9
9
|
import { BaseRequest, FetchDriver } from '@blueprint-ts/core/requests'
|
|
@@ -17,6 +17,71 @@ The `FetchDriver` supports:
|
|
|
17
17
|
- `corsWithCredentials` configuration
|
|
18
18
|
- `AbortSignal` via request config
|
|
19
19
|
|
|
20
|
+
## XMLHttpRequest Driver
|
|
21
|
+
|
|
22
|
+
Use `XMLHttpRequestDriver` when you need upload progress events for file uploads:
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { BaseRequest, XMLHttpRequestDriver } from '@blueprint-ts/core/requests'
|
|
26
|
+
|
|
27
|
+
BaseRequest.setRequestDriver(new XMLHttpRequestDriver())
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
It supports the same configuration as `FetchDriver` and additionally forwards upload progress through
|
|
31
|
+
`RequestEvents.UPLOAD_PROGRESS`.
|
|
32
|
+
|
|
33
|
+
That includes:
|
|
34
|
+
|
|
35
|
+
- `corsWithCredentials`
|
|
36
|
+
- `headers`
|
|
37
|
+
- dynamic header callbacks such as `() => getCookie('XSRF-TOKEN')`
|
|
38
|
+
|
|
39
|
+
## Request-Defined Driver
|
|
40
|
+
|
|
41
|
+
If a specific request class should always use a different driver, define it inside the request:
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
import {
|
|
45
|
+
BaseRequest,
|
|
46
|
+
FetchDriver,
|
|
47
|
+
JsonResponse,
|
|
48
|
+
RequestMethodEnum,
|
|
49
|
+
XMLHttpRequestDriver
|
|
50
|
+
} from '@blueprint-ts/core/requests'
|
|
51
|
+
|
|
52
|
+
BaseRequest.setRequestDriver(new FetchDriver())
|
|
53
|
+
|
|
54
|
+
class UploadAvatarRequest extends BaseRequest<boolean, { message: string }, { ok: true }, JsonResponse<{ ok: true }>> {
|
|
55
|
+
public method(): RequestMethodEnum {
|
|
56
|
+
return RequestMethodEnum.POST
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
public url(): string {
|
|
60
|
+
return '/api/v1/avatar'
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
public getResponse(): JsonResponse<{ ok: true }> {
|
|
64
|
+
return new JsonResponse<{ ok: true }>()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
protected override getRequestDriver() {
|
|
68
|
+
return new XMLHttpRequestDriver({
|
|
69
|
+
corsWithCredentials: true,
|
|
70
|
+
headers: {
|
|
71
|
+
'X-XSRF-TOKEN': () => getCookie('XSRF-TOKEN')
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
This keeps the driver choice encapsulated inside the request class while still allowing the application to keep a
|
|
79
|
+
global default driver for everything else.
|
|
80
|
+
|
|
81
|
+
Important: request-defined drivers do not inherit configuration from the globally registered driver instance. If your
|
|
82
|
+
upload request needs credential support or shared headers, configure them on the `XMLHttpRequestDriver` you return from
|
|
83
|
+
`getRequestDriver()`.
|
|
84
|
+
|
|
20
85
|
## Custom Driver
|
|
21
86
|
|
|
22
87
|
To implement your own driver, implement `RequestDriverContract` and return a `ResponseHandlerContract`:
|
|
@@ -5,6 +5,7 @@ Requests can emit lifecycle events via `BaseRequest.on(...)`.
|
|
|
5
5
|
## Available Events
|
|
6
6
|
|
|
7
7
|
- `RequestEvents.LOADING`: Emits `true` when a request starts and `false` when it finishes.
|
|
8
|
+
- `RequestEvents.UPLOAD_PROGRESS`: Emits upload progress for drivers that support it, such as `XMLHttpRequestDriver`.
|
|
8
9
|
|
|
9
10
|
## Loading Event
|
|
10
11
|
|
|
@@ -29,3 +30,24 @@ request.on<boolean>(RequestEvents.LOADING, (isLoading) => {
|
|
|
29
30
|
// isLoading is typed as boolean
|
|
30
31
|
})
|
|
31
32
|
```
|
|
33
|
+
|
|
34
|
+
## Upload Progress Event
|
|
35
|
+
|
|
36
|
+
Use `RequestEvents.UPLOAD_PROGRESS` to drive file upload progress indicators:
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
import { RequestEvents, type RequestUploadProgress } from '@blueprint-ts/core/requests'
|
|
40
|
+
|
|
41
|
+
request.on<RequestUploadProgress>(RequestEvents.UPLOAD_PROGRESS, (progress) => {
|
|
42
|
+
console.log(progress.loaded, progress.total, progress.progress)
|
|
43
|
+
})
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The payload contains:
|
|
47
|
+
|
|
48
|
+
- `loaded`: Bytes uploaded so far.
|
|
49
|
+
- `total`: Total bytes when the browser can compute it.
|
|
50
|
+
- `lengthComputable`: Whether `total` is reliable.
|
|
51
|
+
- `progress`: A normalized value between `0` and `1` when `total` is known.
|
|
52
|
+
|
|
53
|
+
Note: The default `FetchDriver` does not emit upload progress. Use `XMLHttpRequestDriver` for upload progress support.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# File Uploads
|
|
2
|
+
|
|
3
|
+
Use `FormDataFactory` to build multipart payloads, and use `XMLHttpRequestDriver` when the consuming application needs
|
|
4
|
+
upload progress for a progress bar.
|
|
5
|
+
|
|
6
|
+
## Request Definition
|
|
7
|
+
|
|
8
|
+
```typescript
|
|
9
|
+
import {
|
|
10
|
+
BaseRequest,
|
|
11
|
+
FormDataFactory,
|
|
12
|
+
JsonResponse,
|
|
13
|
+
RequestMethodEnum,
|
|
14
|
+
XMLHttpRequestDriver
|
|
15
|
+
} from '@blueprint-ts/core/requests'
|
|
16
|
+
|
|
17
|
+
interface UploadAvatarPayload {
|
|
18
|
+
avatar: File
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface UploadAvatarResponse {
|
|
22
|
+
id: string
|
|
23
|
+
url: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
class UploadAvatarRequest extends BaseRequest<
|
|
27
|
+
boolean,
|
|
28
|
+
{ message: string },
|
|
29
|
+
UploadAvatarResponse,
|
|
30
|
+
JsonResponse<UploadAvatarResponse>,
|
|
31
|
+
UploadAvatarPayload
|
|
32
|
+
> {
|
|
33
|
+
public method(): RequestMethodEnum {
|
|
34
|
+
return RequestMethodEnum.POST
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
public url(): string {
|
|
38
|
+
return '/api/v1/avatar'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
public getResponse(): JsonResponse<UploadAvatarResponse> {
|
|
42
|
+
return new JsonResponse<UploadAvatarResponse>()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public override getRequestBodyFactory() {
|
|
46
|
+
return new FormDataFactory<UploadAvatarPayload>()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
protected override getRequestDriver() {
|
|
50
|
+
return new XMLHttpRequestDriver({
|
|
51
|
+
corsWithCredentials: true,
|
|
52
|
+
headers: {
|
|
53
|
+
'X-XSRF-TOKEN': () => getCookie('XSRF-TOKEN')
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Global Default Driver
|
|
61
|
+
|
|
62
|
+
You can keep `FetchDriver` as the application default. The upload request above will still use `XMLHttpRequestDriver`
|
|
63
|
+
because it defines its own driver internally:
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
import { BaseRequest, FetchDriver } from '@blueprint-ts/core/requests'
|
|
67
|
+
|
|
68
|
+
BaseRequest.setRequestDriver(new FetchDriver())
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Important: the upload request's `XMLHttpRequestDriver` does not inherit config from the global `FetchDriver`. If the
|
|
72
|
+
upload request needs credentials or shared headers, define them on the `XMLHttpRequestDriver` returned by
|
|
73
|
+
`getRequestDriver()`.
|
|
74
|
+
|
|
75
|
+
## Listening for Upload Progress
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
import { RequestEvents, type RequestUploadProgress } from '@blueprint-ts/core/requests'
|
|
79
|
+
|
|
80
|
+
const request = new UploadAvatarRequest()
|
|
81
|
+
|
|
82
|
+
request.on<RequestUploadProgress>(RequestEvents.UPLOAD_PROGRESS, (progress) => {
|
|
83
|
+
if (!progress.lengthComputable || progress.progress === undefined) {
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
progressBar.value = progress.progress * 100
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
await request.setBody({
|
|
91
|
+
avatar: fileInput.files![0],
|
|
92
|
+
}).send()
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Notes
|
|
96
|
+
|
|
97
|
+
- Upload progress requires `XMLHttpRequestDriver`. The default `FetchDriver` does not emit upload progress events.
|
|
98
|
+
- Define `XMLHttpRequestDriver` inside the upload request class when that request should always support progress.
|
|
99
|
+
- `XMLHttpRequestDriver` supports the same `corsWithCredentials` and `headers` options as `FetchDriver`, including
|
|
100
|
+
header callbacks.
|
|
101
|
+
- Request-defined drivers do not automatically inherit config from the globally registered driver.
|
|
102
|
+
- Some browsers cannot compute a reliable total size for every upload. Check `lengthComputable` before rendering a
|
|
103
|
+
percentage.
|
|
104
|
+
- Upload event listeners may cause CORS preflight requests on cross-origin uploads. Ensure the server is configured
|
|
105
|
+
accordingly.
|
|
@@ -54,6 +54,8 @@ public override getRequestBodyFactory() {
|
|
|
54
54
|
}
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
+
If you want to show upload progress for multipart file uploads, see [File Uploads](/services/requests/file-uploads).
|
|
58
|
+
|
|
57
59
|
## Custom Body Factories
|
|
58
60
|
|
|
59
61
|
You can implement your own body factory by returning a `BodyContract` with custom headers and serialization logic.
|
package/package.json
CHANGED
package/release-tool.json
CHANGED
|
@@ -55,6 +55,25 @@ export abstract class BasePaginator<ResourceInterface, ViewDriver extends BaseVi
|
|
|
55
55
|
return updated
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
public removeRows(
|
|
59
|
+
predicate: (row: ResourceInterface, index: number, data: ResourceInterface[]) => boolean,
|
|
60
|
+
options?: { adjustTotal?: boolean }
|
|
61
|
+
): number {
|
|
62
|
+
const data = this.viewDriver.getData()
|
|
63
|
+
const next = data.filter((row, index) => !predicate(row, index, data))
|
|
64
|
+
const removed = data.length - next.length
|
|
65
|
+
|
|
66
|
+
if (removed > 0) {
|
|
67
|
+
this.viewDriver.setData(next)
|
|
68
|
+
|
|
69
|
+
if (options?.adjustTotal ?? true) {
|
|
70
|
+
this.viewDriver.setTotal(Math.max(0, this.viewDriver.getTotal() - removed))
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return removed
|
|
75
|
+
}
|
|
76
|
+
|
|
58
77
|
public getTotal(): number {
|
|
59
78
|
return this.viewDriver.getTotal()
|
|
60
79
|
}
|
|
@@ -7,6 +7,7 @@ import { ResponseException } from './exceptions/ResponseException'
|
|
|
7
7
|
import { StaleResponseException } from './exceptions/StaleResponseException'
|
|
8
8
|
import { type DriverConfigContract } from './contracts/DriverConfigContract'
|
|
9
9
|
import { type BodyFactoryContract } from './contracts/BodyFactoryContract'
|
|
10
|
+
import { type BodyContract } from './contracts/BodyContract'
|
|
10
11
|
import { type RequestLoaderContract } from './contracts/RequestLoaderContract'
|
|
11
12
|
import { type RequestDriverContract } from './contracts/RequestDriverContract'
|
|
12
13
|
import { type RequestLoaderFactoryContract } from './contracts/RequestLoaderFactoryContract'
|
|
@@ -15,6 +16,7 @@ import { type HeadersContract } from './contracts/HeadersContract'
|
|
|
15
16
|
import { type ResponseHandlerContract } from './drivers/contracts/ResponseHandlerContract'
|
|
16
17
|
import { type ResponseContract } from './contracts/ResponseContract'
|
|
17
18
|
import { type RequestConcurrencyOptions } from './types/RequestConcurrencyOptions'
|
|
19
|
+
import { type RequestUploadProgress } from './types/RequestUploadProgress'
|
|
18
20
|
import { RequestConcurrencyMode } from './RequestConcurrencyMode.enum'
|
|
19
21
|
import { mergeDeep } from '../support/helpers'
|
|
20
22
|
import { v4 as uuidv4 } from 'uuid'
|
|
@@ -113,7 +115,8 @@ export abstract class BaseRequest<
|
|
|
113
115
|
}
|
|
114
116
|
|
|
115
117
|
public buildUrl(): URL {
|
|
116
|
-
const
|
|
118
|
+
const hasParams = this.params !== undefined && Object.keys(this.params).length > 0
|
|
119
|
+
const url = hasParams ? this.url() + '?' + qs.stringify(this.params) : this.url()
|
|
117
120
|
|
|
118
121
|
return new URL(url, this.baseUrl() ?? BaseRequest.defaultBaseUrl)
|
|
119
122
|
}
|
|
@@ -163,8 +166,9 @@ export abstract class BaseRequest<
|
|
|
163
166
|
const responseSkeleton = this.getResponse()
|
|
164
167
|
|
|
165
168
|
const requestBody = this.requestBody === undefined ? undefined : this.getRequestBodyFactory()?.make(this.requestBody)
|
|
169
|
+
const requestConfig = this.buildRequestConfig(requestBody, concurrencyKey, sequence, useLatest)
|
|
166
170
|
|
|
167
|
-
return
|
|
171
|
+
return this.resolveRequestDriver()
|
|
168
172
|
.send(
|
|
169
173
|
this.buildUrl(),
|
|
170
174
|
this.method(),
|
|
@@ -173,7 +177,7 @@ export abstract class BaseRequest<
|
|
|
173
177
|
...this.requestHeaders()
|
|
174
178
|
},
|
|
175
179
|
requestBody,
|
|
176
|
-
|
|
180
|
+
requestConfig
|
|
177
181
|
)
|
|
178
182
|
.then(async (responseHandler: ResponseHandlerContract) => {
|
|
179
183
|
if (useLatest && !this.isLatestSequence(concurrencyKey, sequence)) {
|
|
@@ -274,9 +278,44 @@ export abstract class BaseRequest<
|
|
|
274
278
|
return undefined
|
|
275
279
|
}
|
|
276
280
|
|
|
281
|
+
protected buildRequestConfig(
|
|
282
|
+
requestBody: BodyContract | undefined,
|
|
283
|
+
concurrencyKey: string,
|
|
284
|
+
sequence: number,
|
|
285
|
+
useLatest: boolean
|
|
286
|
+
): DriverConfigContract {
|
|
287
|
+
const config = this.getConfig() ?? {}
|
|
288
|
+
const onUploadProgress = config.onUploadProgress
|
|
289
|
+
|
|
290
|
+
if (requestBody === undefined) {
|
|
291
|
+
return config
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
...config,
|
|
296
|
+
onUploadProgress: (progress: RequestUploadProgress) => {
|
|
297
|
+
onUploadProgress?.(progress)
|
|
298
|
+
|
|
299
|
+
if (useLatest && !this.isLatestSequence(concurrencyKey, sequence)) {
|
|
300
|
+
return
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
this.dispatch<RequestUploadProgress>(RequestEvents.UPLOAD_PROGRESS, progress)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
277
308
|
protected getConfig(): DriverConfigContract | undefined {
|
|
278
309
|
return {
|
|
279
310
|
abortSignal: this.abortSignal
|
|
280
311
|
}
|
|
281
312
|
}
|
|
313
|
+
|
|
314
|
+
protected resolveRequestDriver(): RequestDriverContract {
|
|
315
|
+
return this.getRequestDriver() ?? BaseRequest.requestDriver
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
protected getRequestDriver(): RequestDriverContract | undefined {
|
|
319
|
+
return undefined
|
|
320
|
+
}
|
|
282
321
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { type HeadersContract } from './HeadersContract'
|
|
2
|
+
import { type RequestUploadProgress } from '../types/RequestUploadProgress'
|
|
2
3
|
|
|
3
4
|
export interface DriverConfigContract {
|
|
4
5
|
corsWithCredentials?: boolean | undefined
|
|
5
6
|
abortSignal?: AbortSignal | undefined
|
|
6
7
|
headers?: HeadersContract | undefined
|
|
8
|
+
onUploadProgress?: ((progress: RequestUploadProgress) => void) | undefined
|
|
7
9
|
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { ResponseException } from '../../exceptions/ResponseException'
|
|
2
|
+
import { RequestMethodEnum } from '../../RequestMethod.enum'
|
|
3
|
+
import { type HeadersContract, type HeaderValue } from '../../contracts/HeadersContract'
|
|
4
|
+
import { type BodyContract } from '../../contracts/BodyContract'
|
|
5
|
+
import { type RequestDriverContract } from '../../contracts/RequestDriverContract'
|
|
6
|
+
import { type DriverConfigContract } from '../../contracts/DriverConfigContract'
|
|
7
|
+
import { type ResponseHandlerContract } from '../contracts/ResponseHandlerContract'
|
|
8
|
+
import { XMLHttpRequestResponse } from './XMLHttpRequestResponse'
|
|
9
|
+
|
|
10
|
+
export class XMLHttpRequestDriver implements RequestDriverContract {
|
|
11
|
+
public constructor(protected config?: DriverConfigContract) {}
|
|
12
|
+
|
|
13
|
+
public async send(
|
|
14
|
+
url: URL | string,
|
|
15
|
+
method: RequestMethodEnum,
|
|
16
|
+
headers: HeadersContract,
|
|
17
|
+
body?: BodyContract,
|
|
18
|
+
requestConfig?: DriverConfigContract
|
|
19
|
+
): Promise<ResponseHandlerContract> {
|
|
20
|
+
const mergedConfig: DriverConfigContract = {
|
|
21
|
+
...this.config,
|
|
22
|
+
...(requestConfig ?? {})
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const mergedHeaders: HeadersContract = {
|
|
26
|
+
...this.config?.headers,
|
|
27
|
+
...headers,
|
|
28
|
+
...body?.getHeaders()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const resolvedHeaders = this.resolveHeaders(mergedHeaders)
|
|
32
|
+
|
|
33
|
+
return await new Promise<ResponseHandlerContract>((resolve, reject) => {
|
|
34
|
+
const request = new XMLHttpRequest()
|
|
35
|
+
const requestUrl = url instanceof URL ? url.toString() : url
|
|
36
|
+
const requestBody = [RequestMethodEnum.GET, RequestMethodEnum.HEAD].includes(method) ? undefined : body?.getContent()
|
|
37
|
+
const abortSignal = mergedConfig.abortSignal
|
|
38
|
+
const handleAbortSignal = () => request.abort()
|
|
39
|
+
|
|
40
|
+
const cleanup = () => {
|
|
41
|
+
request.onload = null
|
|
42
|
+
request.onerror = null
|
|
43
|
+
request.onabort = null
|
|
44
|
+
|
|
45
|
+
if (request.upload) {
|
|
46
|
+
request.upload.onprogress = null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
abortSignal?.removeEventListener('abort', handleAbortSignal)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
request.open(method, requestUrl, true)
|
|
53
|
+
request.responseType = 'blob'
|
|
54
|
+
request.withCredentials = this.getCorsWithCredentials(mergedConfig.corsWithCredentials)
|
|
55
|
+
|
|
56
|
+
for (const key in resolvedHeaders) {
|
|
57
|
+
request.setRequestHeader(key, resolvedHeaders[key] as string)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
request.onload = () => {
|
|
61
|
+
cleanup()
|
|
62
|
+
|
|
63
|
+
if (request.status === 0) {
|
|
64
|
+
reject(new Error('No response received.'))
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const response = new XMLHttpRequestResponse(request)
|
|
69
|
+
|
|
70
|
+
if (request.status < 200 || request.status >= 300) {
|
|
71
|
+
reject(new ResponseException(response))
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
resolve(response)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
request.onerror = () => {
|
|
79
|
+
cleanup()
|
|
80
|
+
reject(new Error('Network request failed.'))
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
request.onabort = () => {
|
|
84
|
+
cleanup()
|
|
85
|
+
reject(new DOMException('The operation was aborted.', 'AbortError'))
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (request.upload) {
|
|
89
|
+
request.upload.onprogress = (event: ProgressEvent<EventTarget>) => {
|
|
90
|
+
const total = event.lengthComputable ? event.total : undefined
|
|
91
|
+
|
|
92
|
+
mergedConfig.onUploadProgress?.({
|
|
93
|
+
loaded: event.loaded,
|
|
94
|
+
total: total,
|
|
95
|
+
lengthComputable: event.lengthComputable,
|
|
96
|
+
progress: total === undefined || total === 0 ? undefined : event.loaded / total
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (abortSignal?.aborted) {
|
|
102
|
+
handleAbortSignal()
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
abortSignal?.addEventListener('abort', handleAbortSignal, { once: true })
|
|
107
|
+
request.send(requestBody)
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
protected getCorsWithCredentials(corsWithCredentials: boolean | undefined): boolean {
|
|
112
|
+
if (corsWithCredentials === true) {
|
|
113
|
+
return true
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (corsWithCredentials === false) {
|
|
117
|
+
return false
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return this.config?.corsWithCredentials ?? false
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
protected resolveHeaders(headers: HeadersContract): HeadersContract {
|
|
124
|
+
const resolved: HeadersContract = {}
|
|
125
|
+
|
|
126
|
+
for (const key in headers) {
|
|
127
|
+
const value: HeaderValue | undefined = headers[key]
|
|
128
|
+
|
|
129
|
+
if (value === undefined) {
|
|
130
|
+
continue
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
resolved[key] = typeof value === 'function' ? value() : value
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return resolved
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { type HeadersContract } from '../../contracts/HeadersContract'
|
|
2
|
+
import { type ResponseHandlerContract } from '../contracts/ResponseHandlerContract'
|
|
3
|
+
|
|
4
|
+
export class XMLHttpRequestResponse implements ResponseHandlerContract {
|
|
5
|
+
protected response: Response
|
|
6
|
+
protected headers: HeadersContract
|
|
7
|
+
|
|
8
|
+
public constructor(protected request: XMLHttpRequest) {
|
|
9
|
+
this.headers = this.parseHeaders(request.getAllResponseHeaders())
|
|
10
|
+
this.response = new Response(this.getResponseBody(), {
|
|
11
|
+
status: request.status,
|
|
12
|
+
statusText: request.statusText,
|
|
13
|
+
headers: Object.entries(this.headers).map(([key, value]) => [key, String(value)])
|
|
14
|
+
})
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
public getStatusCode(): number | undefined {
|
|
18
|
+
return this.request.status
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public getHeaders(): HeadersContract {
|
|
22
|
+
return this.headers
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public getRawResponse(): Response {
|
|
26
|
+
return this.response
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public async json<ResponseBodyInterface>(): Promise<ResponseBodyInterface> {
|
|
30
|
+
return await this.response.json()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public async text(): Promise<string> {
|
|
34
|
+
return await this.response.text()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
public async blob(): Promise<Blob> {
|
|
38
|
+
return await this.response.blob()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
protected getResponseBody(): Blob | string | null {
|
|
42
|
+
if ([204, 205, 304].includes(this.request.status)) {
|
|
43
|
+
return null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (this.request.response === null || this.request.response === undefined) {
|
|
47
|
+
return null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (this.isBlobLike(this.request.response) || typeof this.request.response === 'string') {
|
|
51
|
+
return this.request.response
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return new Blob([this.request.response])
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
protected isBlobLike(value: unknown): value is Blob {
|
|
58
|
+
return (
|
|
59
|
+
value instanceof Blob ||
|
|
60
|
+
(typeof value === 'object' &&
|
|
61
|
+
value !== null &&
|
|
62
|
+
typeof (value as Blob).arrayBuffer === 'function' &&
|
|
63
|
+
typeof (value as Blob).stream === 'function' &&
|
|
64
|
+
typeof (value as Blob).text === 'function')
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
protected parseHeaders(rawHeaders: string): HeadersContract {
|
|
69
|
+
const headers: HeadersContract = {}
|
|
70
|
+
const lines = rawHeaders.trim()
|
|
71
|
+
|
|
72
|
+
if (lines.length === 0) {
|
|
73
|
+
return headers
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
for (const line of lines.split(/\r?\n/)) {
|
|
77
|
+
const separatorIndex = line.indexOf(':')
|
|
78
|
+
|
|
79
|
+
if (separatorIndex === -1) {
|
|
80
|
+
continue
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const key = line.slice(0, separatorIndex).trim()
|
|
84
|
+
const value = line.slice(separatorIndex + 1).trim()
|
|
85
|
+
|
|
86
|
+
if (key.length === 0) {
|
|
87
|
+
continue
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
headers[key] = key in headers ? `${String(headers[key])}, ${value}` : value
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return headers
|
|
94
|
+
}
|
|
95
|
+
}
|