@blueprint-ts/core 4.1.0-beta.1 → 4.1.0-beta.2

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 CHANGED
@@ -1,3 +1,16 @@
1
+ ## 4.1.0-beta.2 - 2026-03-25 (beta)
2
+
3
+ # [4.1.0-beta.2](/compare/4.1.0-beta.1...4.1.0-beta.2) (2026-03-25)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * Tighten response header typing f767572
9
+
10
+
11
+ ### Features
12
+
13
+ * Support Blob and ArrayBuffer bodies a265df3
1
14
  ## 4.1.0-beta.1 - 2026-03-21 (beta)
2
15
 
3
16
  # [4.1.0-beta.1](/compare/v4.0.0...v4.1.0-beta.1) (2026-03-21)
@@ -1,7 +1,15 @@
1
1
  # File Uploads
2
2
 
3
- Use `FormDataFactory` to build multipart payloads, and use `XMLHttpRequestDriver` when the consuming application needs
4
- upload progress for a progress bar.
3
+ Use `FormDataFactory` for multipart uploads with fields, and use `BinaryBodyFactory` when the endpoint expects a raw
4
+ binary body such as a chunk `PUT`. Use `XMLHttpRequestDriver` when the consuming application needs upload progress for a
5
+ progress bar.
6
+
7
+ Choose the body factory based on the wire format your endpoint expects:
8
+
9
+ - Use `FormDataFactory` when the request includes normal fields plus one or more files.
10
+ - Use `BinaryBodyFactory` when the request body itself is the file or chunk.
11
+ - Use `FetchDriver` if you just need to send the upload.
12
+ - Use `XMLHttpRequestDriver` if you also need upload progress events.
5
13
 
6
14
  ## Request Definition
7
15
 
@@ -92,10 +100,65 @@ await request.setBody({
92
100
  }).send()
93
101
  ```
94
102
 
103
+ ## Raw Chunk Uploads
104
+
105
+ ```typescript
106
+ import {
107
+ BaseRequest,
108
+ BinaryBodyFactory,
109
+ JsonResponse,
110
+ RequestMethodEnum,
111
+ XMLHttpRequestDriver
112
+ } from '@blueprint-ts/core/requests'
113
+
114
+ interface UploadPartResponse {
115
+ etag: string
116
+ }
117
+
118
+ class UploadPartRequest extends BaseRequest<
119
+ boolean,
120
+ { message: string },
121
+ UploadPartResponse,
122
+ JsonResponse<UploadPartResponse>,
123
+ Uint8Array
124
+ > {
125
+ public method(): RequestMethodEnum {
126
+ return RequestMethodEnum.PUT
127
+ }
128
+
129
+ public url(): string {
130
+ return '/api/v1/uploads/part'
131
+ }
132
+
133
+ public getResponse(): JsonResponse<UploadPartResponse> {
134
+ return new JsonResponse<UploadPartResponse>()
135
+ }
136
+
137
+ public override getRequestBodyFactory() {
138
+ return new BinaryBodyFactory<Uint8Array>('application/octet-stream')
139
+ }
140
+
141
+ public override requestHeaders() {
142
+ return {
143
+ 'X-Part-Number': '1'
144
+ }
145
+ }
146
+
147
+ protected override getRequestDriver() {
148
+ return new XMLHttpRequestDriver()
149
+ }
150
+ }
151
+ ```
152
+
153
+ In this example, `setBody(...)` can receive a `Blob`, `ArrayBuffer`, `Uint8Array`, or another typed-array/data-view
154
+ payload supported by `BinaryBodyFactory`.
155
+
95
156
  ## Notes
96
157
 
97
158
  - Upload progress requires `XMLHttpRequestDriver`. The default `FetchDriver` does not emit upload progress events.
98
159
  - Define `XMLHttpRequestDriver` inside the upload request class when that request should always support progress.
160
+ - `BinaryBodyFactory` only sets `Content-Type` automatically when the body is a `Blob` with a non-empty `type`.
161
+ For `ArrayBuffer` and typed-array uploads, pass the expected content type explicitly.
99
162
  - `XMLHttpRequestDriver` supports the same `corsWithCredentials` and `headers` options as `FetchDriver`, including
100
163
  header callbacks.
101
164
  - Request-defined drivers do not automatically inherit config from the globally registered driver.
@@ -56,6 +56,34 @@ public override getRequestBodyFactory() {
56
56
 
57
57
  If you want to show upload progress for multipart file uploads, see [File Uploads](/services/requests/file-uploads).
58
58
 
59
+ ## Raw Binary Bodies
60
+
61
+ Use `BinaryBodyFactory` when the request body should be sent as raw binary instead of multipart form data. This is a
62
+ better fit for chunk uploads, binary artifact pushes, and endpoints that expect the request body as-is:
63
+
64
+ ```typescript
65
+ import { BinaryBodyFactory } from '@blueprint-ts/core/requests'
66
+
67
+ public override getRequestBodyFactory() {
68
+ return new BinaryBodyFactory<ArrayBuffer>('application/octet-stream')
69
+ }
70
+ ```
71
+
72
+ `BinaryBodyFactory` supports:
73
+
74
+ - `Blob`
75
+ - `ArrayBuffer`
76
+ - typed-array and view values such as `Uint8Array` or `DataView`
77
+
78
+ `Content-Type` resolution works like this:
79
+
80
+ - If you pass a content type to `BinaryBodyFactory`, Blueprint sends that `Content-Type` header.
81
+ - Otherwise, if the body is a `Blob` with a non-empty `type`, Blueprint uses `Blob.type`.
82
+ - Otherwise, Blueprint does not add a `Content-Type` header for you.
83
+
84
+ `BinaryBodyFactory` works with both `FetchDriver` and `XMLHttpRequestDriver`. Choose `XMLHttpRequestDriver` only when
85
+ the consuming application needs upload progress events.
86
+
59
87
  ## Custom Body Factories
60
88
 
61
89
  You can implement your own body factory by returning a `BodyContract` with custom headers and serialization logic.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blueprint-ts/core",
3
- "version": "4.1.0-beta.1",
3
+ "version": "4.1.0-beta.2",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -0,0 +1,31 @@
1
+ import { type BodyContent, type BodyContract } from '../contracts/BodyContract'
2
+ import { type HeadersContract } from '../contracts/HeadersContract'
3
+
4
+ export class BinaryBody<RequestBody extends Exclude<BodyContent, string | FormData>> implements BodyContract {
5
+ public constructor(
6
+ protected data: RequestBody,
7
+ protected contentType?: string
8
+ ) {}
9
+
10
+ public getHeaders(): HeadersContract {
11
+ const contentType = this.resolveContentType()
12
+
13
+ return contentType === undefined ? {} : { 'Content-Type': contentType }
14
+ }
15
+
16
+ public getContent(): RequestBody {
17
+ return this.data
18
+ }
19
+
20
+ protected resolveContentType(): string | undefined {
21
+ if (this.contentType !== undefined) {
22
+ return this.contentType
23
+ }
24
+
25
+ if (typeof Blob !== 'undefined' && this.data instanceof Blob && this.data.type !== '') {
26
+ return this.data.type
27
+ }
28
+
29
+ return undefined
30
+ }
31
+ }
@@ -1,7 +1,9 @@
1
1
  import { type HeadersContract } from './HeadersContract'
2
2
 
3
+ export type BodyContent = string | FormData | Blob | ArrayBuffer | ArrayBufferView<ArrayBuffer>
4
+
3
5
  export interface BodyContract {
4
- getContent(): string | FormData
6
+ getContent(): BodyContent
5
7
 
6
8
  getHeaders(): HeadersContract
7
9
  }
@@ -3,3 +3,7 @@ export type HeaderValue = string | (() => string)
3
3
  export interface HeadersContract {
4
4
  [key: string]: HeaderValue
5
5
  }
6
+
7
+ export interface ResolvedHeadersContract {
8
+ [key: string]: string
9
+ }
@@ -1,8 +1,8 @@
1
- import { type HeadersContract } from '../../contracts/HeadersContract'
1
+ import { type ResolvedHeadersContract } from '../../contracts/HeadersContract'
2
2
 
3
3
  export interface ResponseHandlerContract {
4
4
  getStatusCode(): number | undefined
5
- getHeaders(): HeadersContract
5
+ getHeaders(): ResolvedHeadersContract
6
6
  getRawResponse(): Response
7
7
  json<ResponseBodyInterface>(): Promise<ResponseBodyInterface>
8
8
  text(): Promise<string>
@@ -2,7 +2,7 @@ import { ResponseException } from '../../exceptions/ResponseException'
2
2
  import { FetchResponse } from './FetchResponse'
3
3
  import { RequestMethodEnum } from '../../RequestMethod.enum'
4
4
  import { type HeadersContract, type HeaderValue } from '../../contracts/HeadersContract'
5
- import { type BodyContract } from '../../contracts/BodyContract'
5
+ import { type BodyContent, type BodyContract } from '../../contracts/BodyContract'
6
6
  import { type RequestDriverContract } from '../../contracts/RequestDriverContract'
7
7
  import { type DriverConfigContract } from '../../contracts/DriverConfigContract'
8
8
  import { type ResponseHandlerContract } from '../contracts/ResponseHandlerContract'
@@ -17,7 +17,7 @@ interface FetchDriverConfig {
17
17
  headers: HeadersContract
18
18
  credentials?: FetchDriverCredentialConfigEnum | undefined
19
19
  signal?: AbortSignal | undefined
20
- body?: string | FormData | Blob | ArrayBuffer | ArrayBufferView | URLSearchParams | undefined
20
+ body?: BodyContent | URLSearchParams | undefined
21
21
  }
22
22
 
23
23
  export class FetchDriver implements RequestDriverContract {
@@ -1,4 +1,4 @@
1
- import { type HeadersContract } from '../../contracts/HeadersContract'
1
+ import { type ResolvedHeadersContract } from '../../contracts/HeadersContract'
2
2
  import { type ResponseHandlerContract } from '../contracts/ResponseHandlerContract'
3
3
 
4
4
  export class FetchResponse implements ResponseHandlerContract {
@@ -8,7 +8,7 @@ export class FetchResponse implements ResponseHandlerContract {
8
8
  return this.response.status
9
9
  }
10
10
 
11
- public getHeaders(): HeadersContract {
11
+ public getHeaders(): ResolvedHeadersContract {
12
12
  return Object.fromEntries(this.response.headers)
13
13
  }
14
14
 
@@ -1,9 +1,9 @@
1
- import { type HeadersContract } from '../../contracts/HeadersContract'
1
+ import { type ResolvedHeadersContract } from '../../contracts/HeadersContract'
2
2
  import { type ResponseHandlerContract } from '../contracts/ResponseHandlerContract'
3
3
 
4
4
  export class XMLHttpRequestResponse implements ResponseHandlerContract {
5
5
  protected response: Response
6
- protected headers: HeadersContract
6
+ protected headers: ResolvedHeadersContract
7
7
 
8
8
  public constructor(protected request: XMLHttpRequest) {
9
9
  this.headers = this.parseHeaders(request.getAllResponseHeaders())
@@ -18,7 +18,7 @@ export class XMLHttpRequestResponse implements ResponseHandlerContract {
18
18
  return this.request.status
19
19
  }
20
20
 
21
- public getHeaders(): HeadersContract {
21
+ public getHeaders(): ResolvedHeadersContract {
22
22
  return this.headers
23
23
  }
24
24
 
@@ -65,8 +65,8 @@ export class XMLHttpRequestResponse implements ResponseHandlerContract {
65
65
  )
66
66
  }
67
67
 
68
- protected parseHeaders(rawHeaders: string): HeadersContract {
69
- const headers: HeadersContract = {}
68
+ protected parseHeaders(rawHeaders: string): ResolvedHeadersContract {
69
+ const headers: ResolvedHeadersContract = {}
70
70
  const lines = rawHeaders.trim()
71
71
 
72
72
  if (lines.length === 0) {
@@ -0,0 +1,13 @@
1
+ import { BinaryBody } from '../bodies/BinaryBody'
2
+ import { type BodyFactoryContract } from '../contracts/BodyFactoryContract'
3
+ import { type BodyContent, type BodyContract } from '../contracts/BodyContract'
4
+
5
+ export type BinaryBodyContent = Exclude<BodyContent, string | FormData>
6
+
7
+ export class BinaryBodyFactory<RequestBodyInterface extends BinaryBodyContent> implements BodyFactoryContract<RequestBodyInterface> {
8
+ public constructor(protected contentType?: string) {}
9
+
10
+ public make(body: RequestBodyInterface): BodyContract {
11
+ return new BinaryBody<RequestBodyInterface>(body, this.contentType)
12
+ }
13
+ }
@@ -9,9 +9,11 @@ import { RequestErrorRouter } from './RequestErrorRouter'
9
9
  import { RequestEvents } from './RequestEvents.enum'
10
10
  import { RequestMethodEnum } from './RequestMethod.enum'
11
11
  import { RequestConcurrencyMode } from './RequestConcurrencyMode.enum'
12
+ import { BinaryBody } from './bodies/BinaryBody'
12
13
  import { JsonBodyFactory } from './factories/JsonBodyFactory'
14
+ import { BinaryBodyFactory, type BinaryBodyContent } from './factories/BinaryBodyFactory'
13
15
  import { FormDataFactory } from './factories/FormDataFactory'
14
- import { type BodyContract } from './contracts/BodyContract'
16
+ import { type BodyContent, type BodyContract } from './contracts/BodyContract'
15
17
  import { type RequestLoaderContract } from './contracts/RequestLoaderContract'
16
18
  import { type RequestDriverContract } from './contracts/RequestDriverContract'
17
19
  import { type RequestLoaderFactoryContract } from './contracts/RequestLoaderFactoryContract'
@@ -21,7 +23,7 @@ import { type ResponseHandlerContract } from './drivers/contracts/ResponseHandle
21
23
  import { type BaseRequestContract } from './contracts/BaseRequestContract'
22
24
  import { ResponseException } from './exceptions/ResponseException'
23
25
  import { StaleResponseException } from './exceptions/StaleResponseException'
24
- import { type HeaderValue, type HeadersContract } from './contracts/HeadersContract'
26
+ import { type HeaderValue, type HeadersContract, type ResolvedHeadersContract } from './contracts/HeadersContract'
25
27
  import { type RequestConcurrencyOptions } from './types/RequestConcurrencyOptions'
26
28
  import { type RequestUploadProgress } from './types/RequestUploadProgress'
27
29
  import { XMLHttpRequestDriver } from './drivers/xhr/XMLHttpRequestDriver'
@@ -40,12 +42,16 @@ export {
40
42
  RequestConcurrencyMode,
41
43
  ResponseException,
42
44
  StaleResponseException,
45
+ BinaryBody,
43
46
  JsonBodyFactory,
47
+ BinaryBodyFactory,
44
48
  FormDataFactory,
45
49
  XMLHttpRequestDriver
46
50
  }
47
51
 
48
52
  export type {
53
+ BodyContent,
54
+ BinaryBodyContent,
49
55
  RequestDriverContract,
50
56
  RequestLoaderContract,
51
57
  BodyContract,
@@ -56,6 +62,7 @@ export type {
56
62
  BaseRequestContract,
57
63
  HeaderValue,
58
64
  HeadersContract,
65
+ ResolvedHeadersContract,
59
66
  RequestConcurrencyOptions,
60
67
  RequestUploadProgress
61
68
  }
@@ -1,5 +1,5 @@
1
1
  import { type ResponseHandlerContract } from '../drivers/contracts/ResponseHandlerContract'
2
- import { type HeadersContract } from '../contracts/HeadersContract'
2
+ import { type ResolvedHeadersContract } from '../contracts/HeadersContract'
3
3
  import { type ResponseContract } from '../contracts/ResponseContract'
4
4
 
5
5
  export abstract class BaseResponse<ResponseInterface> implements ResponseContract<ResponseInterface> {
@@ -27,7 +27,7 @@ export abstract class BaseResponse<ResponseInterface> implements ResponseContrac
27
27
  return this.response?.getStatusCode()
28
28
  }
29
29
 
30
- public getHeaders(): HeadersContract | undefined {
30
+ public getHeaders(): ResolvedHeadersContract | undefined {
31
31
  return this.response?.getHeaders()
32
32
  }
33
33
 
@@ -1,5 +1,7 @@
1
1
  import { describe, expect, it } from 'vitest'
2
+ import { BinaryBody } from '../../../src/requests/bodies/BinaryBody'
2
3
  import { JsonBody } from '../../../src/requests/bodies/JsonBody'
4
+ import { BinaryBodyFactory } from '../../../src/requests/factories/BinaryBodyFactory'
3
5
  import { JsonBodyFactory } from '../../../src/requests/factories/JsonBodyFactory'
4
6
  import { FormDataFactory } from '../../../src/requests/factories/FormDataFactory'
5
7
  import { FormDataBody } from '../../../src/requests/bodies/FormDataBody'
@@ -19,6 +21,28 @@ describe('Request bodies and factories', () => {
19
21
  expect(body).toBeInstanceOf(JsonBody)
20
22
  })
21
23
 
24
+ it('BinaryBody returns explicit content type and binary content', () => {
25
+ const body = new BinaryBody(new Uint8Array([1, 2, 3]), 'application/octet-stream')
26
+
27
+ expect(body.getHeaders()).toEqual({ 'Content-Type': 'application/octet-stream' })
28
+ expect(body.getContent()).toEqual(new Uint8Array([1, 2, 3]))
29
+ })
30
+
31
+ it('BinaryBody uses Blob mime type when no explicit content type is given', () => {
32
+ const blob = new Blob(['hello'], { type: 'application/custom-binary' })
33
+ const body = new BinaryBody(blob)
34
+
35
+ expect(body.getHeaders()).toEqual({ 'Content-Type': 'application/custom-binary' })
36
+ expect(body.getContent()).toBe(blob)
37
+ })
38
+
39
+ it('BinaryBodyFactory returns BinaryBody', () => {
40
+ const factory = new BinaryBodyFactory<Uint8Array>('application/octet-stream')
41
+ const body = factory.make(new Uint8Array([4, 5, 6]))
42
+
43
+ expect(body).toBeInstanceOf(BinaryBody)
44
+ })
45
+
22
46
  it('FormDataFactory returns FormDataBody', () => {
23
47
  const factory = new FormDataFactory<{ name: string }>()
24
48
  const body = factory.make({ name: 'alice' })
@@ -3,10 +3,10 @@ import { FetchDriver } from '../../../../src/requests/drivers/fetch/FetchDriver'
3
3
  import { FetchResponse } from '../../../../src/requests/drivers/fetch/FetchResponse'
4
4
  import { RequestMethodEnum } from '../../../../src/requests/RequestMethod.enum'
5
5
  import { ResponseException } from '../../../../src/requests/exceptions/ResponseException'
6
- import type { BodyContract } from '../../../../src/requests/contracts/BodyContract'
6
+ import type { BodyContent, BodyContract } from '../../../../src/requests/contracts/BodyContract'
7
7
 
8
- const createBody = (content: string): BodyContract => ({
9
- getHeaders: () => ({ 'Content-Type': 'application/json' }),
8
+ const createBody = (content: BodyContent, headers: Record<string, string> = { 'Content-Type': 'application/json' }): BodyContract => ({
9
+ getHeaders: () => headers,
10
10
  getContent: () => content,
11
11
  })
12
12
 
@@ -65,6 +65,24 @@ describe('FetchDriver', () => {
65
65
  expect(config.body).toBeUndefined()
66
66
  })
67
67
 
68
+ it('passes Blob bodies through to fetch unchanged', async () => {
69
+ const response = new Response('ok', { status: 200 })
70
+ ;(global.fetch as unknown as ReturnType<typeof vi.fn>).mockResolvedValue(response)
71
+
72
+ const blob = new Blob(['chunk'], { type: 'application/octet-stream' })
73
+ const driver = new FetchDriver()
74
+
75
+ await driver.send(
76
+ 'https://example.com',
77
+ RequestMethodEnum.PUT,
78
+ {},
79
+ createBody(blob, { 'Content-Type': 'application/octet-stream' })
80
+ )
81
+
82
+ const [, config] = (global.fetch as unknown as ReturnType<typeof vi.fn>).mock.calls[0]
83
+ expect(config.body).toBe(blob)
84
+ })
85
+
68
86
  it('throws ResponseException when response is not ok', async () => {
69
87
  const response = new Response('fail', { status: 500 })
70
88
  ;(global.fetch as unknown as ReturnType<typeof vi.fn>).mockResolvedValue(response)
@@ -3,10 +3,10 @@ import { XMLHttpRequestDriver } from '../../../../src/requests/drivers/xhr/XMLHt
3
3
  import { XMLHttpRequestResponse } from '../../../../src/requests/drivers/xhr/XMLHttpRequestResponse'
4
4
  import { RequestMethodEnum } from '../../../../src/requests/RequestMethod.enum'
5
5
  import { ResponseException } from '../../../../src/requests/exceptions/ResponseException'
6
- import type { BodyContract } from '../../../../src/requests/contracts/BodyContract'
6
+ import type { BodyContent, BodyContract } from '../../../../src/requests/contracts/BodyContract'
7
7
 
8
- const createBody = (content: string): BodyContract => ({
9
- getHeaders: () => ({ 'Content-Type': 'application/json' }),
8
+ const createBody = (content: BodyContent, headers: Record<string, string> = { 'Content-Type': 'application/json' }): BodyContract => ({
9
+ getHeaders: () => headers,
10
10
  getContent: () => content,
11
11
  })
12
12
 
@@ -148,6 +148,25 @@ describe('XMLHttpRequestDriver', () => {
148
148
  expect(request.sentBody).toBeUndefined()
149
149
  })
150
150
 
151
+ it('passes typed array bodies through to xhr unchanged', async () => {
152
+ const driver = new XMLHttpRequestDriver()
153
+ const chunk = new Uint8Array([1, 2, 3, 4])
154
+
155
+ const promise = driver.send(
156
+ 'https://example.com',
157
+ RequestMethodEnum.PUT,
158
+ {},
159
+ createBody(chunk, { 'Content-Type': 'application/octet-stream' })
160
+ )
161
+
162
+ const request = MockXMLHttpRequest.instances[0]
163
+ request.triggerLoad()
164
+
165
+ await promise
166
+
167
+ expect(request.sentBody).toBe(chunk)
168
+ })
169
+
151
170
  it('throws ResponseException when the response status is not ok', async () => {
152
171
  const driver = new XMLHttpRequestDriver()
153
172