@rei-standard/amsg-client 2.6.0 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/dist/index.cjs +29 -5
- package/dist/index.d.cts +86 -3
- package/dist/index.d.ts +86 -3
- package/dist/index.mjs +29 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -178,6 +178,13 @@ interface DeliverOptions {
|
|
|
178
178
|
// / X-Client-Token / Authorization
|
|
179
179
|
authorization?: string; // 透传成 Authorization header(与 sendInstant 对齐)
|
|
180
180
|
endpointPath?: string; // 默认 '/instant',可改 '/continue' 续跑
|
|
181
|
+
compressRequest?: boolean | { thresholdBytes?: number }; // 可选请求体 gzip。不传/falsy = 关(行为不变)
|
|
182
|
+
// true / {} = 开,阈值默认 16384 字节(16KB)
|
|
183
|
+
// { thresholdBytes: N } = 开 + 自定义阈值
|
|
184
|
+
// 仅当 body 超阈值且运行时有 CompressionStream 才压;
|
|
185
|
+
// 否则发明文(优雅降级,绝不抛)。压时发 gzip 字节 +
|
|
186
|
+
// 头 X-Amsg-Request-Encoding: gzip(非标准 Content-
|
|
187
|
+
// Encoding),由接收端 gunzip。SSE / JSON 两路通用。
|
|
181
188
|
}
|
|
182
189
|
|
|
183
190
|
interface ObservedDeliveryReceipt {
|
|
@@ -199,6 +206,8 @@ interface RawReadMeta {
|
|
|
199
206
|
|
|
200
207
|
> `onRawRead` 是诊断钩子:SSE 解析层默认丢弃 `:` 注释行(含每秒一发的 keepalive),出问题时无从判断「静默期里到底有没有字节到达」。挂上它就能在 raw `reader.read()` 这一层看到每次读到的原始字节与 keepalive 帧。不传则零开销、行为不变。
|
|
201
208
|
|
|
209
|
+
> `compressRequest` 用于大 body 上传:开启后,要发的 JSON 在上网线前 gzip(中文 + 重复结构压缩比很高),网线上字节小了就能在慢/不稳链路的发送超时之前传完,且上下文一字不动。仅当 body 超阈值且运行时支持 `CompressionStream` 才压,否则照常发明文;压缩出错也兜回明文,永不影响发送。压缩的是请求体,与响应 / `onChunk` / `onRawRead` 无关。接收端需按 `X-Amsg-Request-Encoding: gzip` 头自行解压。
|
|
210
|
+
|
|
202
211
|
### `delivery.mode` 必须显式选
|
|
203
212
|
|
|
204
213
|
| mode | 何时用 | outcome 取值 |
|
package/dist/index.cjs
CHANGED
|
@@ -54,6 +54,25 @@ function classifyContentType(contentType) {
|
|
|
54
54
|
if (/^application\/[\w.+-]+\+json$/.test(main)) return "json";
|
|
55
55
|
return "unknown";
|
|
56
56
|
}
|
|
57
|
+
var COMPRESS_REQUEST_DEFAULT_THRESHOLD = 16384;
|
|
58
|
+
var COMPRESS_REQUEST_HEADER = "X-Amsg-Request-Encoding";
|
|
59
|
+
async function maybeCompressRequestBody(body, compressRequest) {
|
|
60
|
+
if (!compressRequest) return { body, header: null };
|
|
61
|
+
const threshold = typeof compressRequest === "object" && typeof compressRequest.thresholdBytes === "number" ? compressRequest.thresholdBytes : COMPRESS_REQUEST_DEFAULT_THRESHOLD;
|
|
62
|
+
try {
|
|
63
|
+
if (typeof CompressionStream === "undefined") return { body, header: null };
|
|
64
|
+
const bytes = new TextEncoder().encode(body);
|
|
65
|
+
if (bytes.length <= threshold) return { body, header: null };
|
|
66
|
+
const gz = new Uint8Array(
|
|
67
|
+
await new Response(
|
|
68
|
+
new Blob([bytes]).stream().pipeThrough(new CompressionStream("gzip"))
|
|
69
|
+
).arrayBuffer()
|
|
70
|
+
);
|
|
71
|
+
return { body: gz, header: COMPRESS_REQUEST_HEADER };
|
|
72
|
+
} catch {
|
|
73
|
+
return { body, header: null };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
57
76
|
var ReiClient = class {
|
|
58
77
|
/**
|
|
59
78
|
* @param {ReiClientConfig} config
|
|
@@ -316,7 +335,8 @@ var ReiClient = class {
|
|
|
316
335
|
headers,
|
|
317
336
|
authorization,
|
|
318
337
|
endpointPath,
|
|
319
|
-
onRawRead
|
|
338
|
+
onRawRead,
|
|
339
|
+
compressRequest
|
|
320
340
|
} = opts;
|
|
321
341
|
if (!delivery || typeof delivery !== "object") {
|
|
322
342
|
throw new TypeError("[rei-standard-amsg-client] deliver() requires opts.delivery (discriminated union)");
|
|
@@ -378,7 +398,8 @@ var ReiClient = class {
|
|
|
378
398
|
const result = await this._runInstantTransport(built, {
|
|
379
399
|
signal: internalAbort.signal,
|
|
380
400
|
onChunk: wrappedOnChunk,
|
|
381
|
-
onRawRead
|
|
401
|
+
onRawRead,
|
|
402
|
+
compressRequest
|
|
382
403
|
});
|
|
383
404
|
if (finalized) return;
|
|
384
405
|
transportEnded = true;
|
|
@@ -657,14 +678,17 @@ var ReiClient = class {
|
|
|
657
678
|
*
|
|
658
679
|
* @private
|
|
659
680
|
* @param {{ url: string, headers: Record<string, string>, body: string }} built
|
|
660
|
-
* @param {{ signal: AbortSignal, onChunk?: (p: unknown) => Promise<void> | void, onRawRead?: (meta: RawReadMeta) => void }} opts
|
|
681
|
+
* @param {{ signal: AbortSignal, onChunk?: (p: unknown) => Promise<void> | void, onRawRead?: (meta: RawReadMeta) => void, compressRequest?: boolean | { thresholdBytes?: number } }} opts
|
|
661
682
|
* `onRawRead` is forwarded to the SSE consumer for raw read-loop telemetry (see `DeliverOptions.onRawRead`).
|
|
683
|
+
* `compressRequest` opts the request body into gzip before `fetch` (see `DeliverOptions.compressRequest`).
|
|
662
684
|
* @returns {Promise<{ kind: 'sse' } | { kind: 'json', body: unknown }>}
|
|
663
685
|
*/
|
|
664
686
|
async _runInstantTransport(built, opts) {
|
|
665
|
-
const { signal, onChunk, onRawRead } = opts;
|
|
687
|
+
const { signal, onChunk, onRawRead, compressRequest } = opts;
|
|
666
688
|
const { url, headers, body } = built;
|
|
667
|
-
const
|
|
689
|
+
const { body: wireBody, header: compressionHeader } = await maybeCompressRequestBody(body, compressRequest);
|
|
690
|
+
const wireHeaders = compressionHeader ? { ...headers, [compressionHeader]: "gzip" } : headers;
|
|
691
|
+
const res = await fetch(url, { method: "POST", headers: wireHeaders, body: wireBody, signal });
|
|
668
692
|
if (!res.ok) {
|
|
669
693
|
const text2 = await res.text().catch(() => "");
|
|
670
694
|
const err = new Error(`Instant request failed: ${res.status} ${text2}`);
|
package/dist/index.d.cts
CHANGED
|
@@ -201,6 +201,17 @@ const TEXT_ENCODER = new TextEncoder();
|
|
|
201
201
|
* silently drops. Use it to tell "connection alive but no business data" apart from "no bytes flowing
|
|
202
202
|
* at all" when diagnosing stalled streams. Purely observational: throws are swallowed and never affect
|
|
203
203
|
* transport. Not invoked for the JSON transport.
|
|
204
|
+
* @property {boolean | { thresholdBytes?: number }} [compressRequest] - Opt-in gzip of the request
|
|
205
|
+
* BODY before it is sent (applies to both the SSE and JSON transports — it compresses the request,
|
|
206
|
+
* not the response). Omit / falsy = OFF and behavior is fully unchanged (backward compatible).
|
|
207
|
+
* `true` or `{}` enables it at the default 16384-byte (16 KB) threshold; `{ thresholdBytes: N }`
|
|
208
|
+
* sets a custom threshold. When enabled, the body is gzip-compressed only if its UTF-8 byte length
|
|
209
|
+
* exceeds the threshold AND the runtime provides `CompressionStream`; otherwise it is sent as
|
|
210
|
+
* plaintext (graceful degradation, never throws). On compression the request gains the custom
|
|
211
|
+
* header `X-Amsg-Request-Encoding: gzip` (NOT standard `Content-Encoding`, which CDNs / proxies
|
|
212
|
+
* would auto-decompress and double-decode) and the body is the raw gzip bytes — the receiving
|
|
213
|
+
* worker is responsible for gunzipping. Use it when delivering large bodies over slow / flaky
|
|
214
|
+
* uplinks where a big upload can outrun the connection's send timeout.
|
|
204
215
|
*/
|
|
205
216
|
|
|
206
217
|
/**
|
|
@@ -266,6 +277,68 @@ function classifyContentType(contentType) {
|
|
|
266
277
|
return 'unknown';
|
|
267
278
|
}
|
|
268
279
|
|
|
280
|
+
/**
|
|
281
|
+
* Default size floor for request-body gzip: bodies at or below this are not
|
|
282
|
+
* worth compressing (the gzip header/overhead can outweigh the gain on tiny
|
|
283
|
+
* payloads). 16 KB matches the contract documented on `DeliverOptions.compressRequest`.
|
|
284
|
+
*/
|
|
285
|
+
const COMPRESS_REQUEST_DEFAULT_THRESHOLD = 16384;
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Custom request header used to mark a gzip-compressed body. Deliberately NOT
|
|
289
|
+
* the standard `Content-Encoding` — CDNs / reverse proxies (Cloudflare, etc.)
|
|
290
|
+
* auto-decompress `Content-Encoding: gzip` on the way in, which would double-
|
|
291
|
+
* decompress and corrupt the body. The receiving worker keys off this custom
|
|
292
|
+
* header to know it must gunzip the body itself.
|
|
293
|
+
*/
|
|
294
|
+
const COMPRESS_REQUEST_HEADER = 'X-Amsg-Request-Encoding';
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Optionally gzip a request body string before it hits `fetch`.
|
|
298
|
+
*
|
|
299
|
+
* Pure optimization with graceful degradation: returns the original plaintext
|
|
300
|
+
* body (and no extra header) whenever compression is disabled, the body is at
|
|
301
|
+
* or below the threshold, the runtime lacks `CompressionStream`, or anything
|
|
302
|
+
* throws. The wire bytes shrink (Chinese / repetitive JSON compresses ~5-8x)
|
|
303
|
+
* so large uploads finish before flaky links time out — without dropping any
|
|
304
|
+
* context. Decompression is the receiving worker's job (keyed off
|
|
305
|
+
* `X-Amsg-Request-Encoding: gzip`).
|
|
306
|
+
*
|
|
307
|
+
* @param {string} body - The already-serialized request body (plaintext JSON).
|
|
308
|
+
* @param {boolean | { thresholdBytes?: number } | undefined} compressRequest
|
|
309
|
+
* `undefined`/falsy ⇒ disabled (no-op, backward compatible). `true` / `{}` ⇒
|
|
310
|
+
* enabled at the 16 KB default. `{ thresholdBytes: N }` ⇒ enabled at N bytes.
|
|
311
|
+
* @returns {Promise<{ body: string | Uint8Array, header: string | null }>}
|
|
312
|
+
* `header` is the gzip marker header name to set when compression happened,
|
|
313
|
+
* or `null` to send plaintext with no extra header.
|
|
314
|
+
*/
|
|
315
|
+
async function maybeCompressRequestBody(body, compressRequest) {
|
|
316
|
+
// Disabled / no opt-in ⇒ behavior unchanged.
|
|
317
|
+
if (!compressRequest) return { body, header: null };
|
|
318
|
+
|
|
319
|
+
const threshold =
|
|
320
|
+
typeof compressRequest === 'object' && typeof compressRequest.thresholdBytes === 'number'
|
|
321
|
+
? compressRequest.thresholdBytes
|
|
322
|
+
: COMPRESS_REQUEST_DEFAULT_THRESHOLD;
|
|
323
|
+
|
|
324
|
+
try {
|
|
325
|
+
if (typeof CompressionStream === 'undefined') return { body, header: null };
|
|
326
|
+
|
|
327
|
+
const bytes = new TextEncoder().encode(body);
|
|
328
|
+
if (bytes.length <= threshold) return { body, header: null };
|
|
329
|
+
|
|
330
|
+
const gz = new Uint8Array(
|
|
331
|
+
await new Response(
|
|
332
|
+
new Blob([bytes]).stream().pipeThrough(new CompressionStream('gzip'))
|
|
333
|
+
).arrayBuffer()
|
|
334
|
+
);
|
|
335
|
+
return { body: gz, header: COMPRESS_REQUEST_HEADER };
|
|
336
|
+
} catch {
|
|
337
|
+
// Compression is an optimization, never a failure mode: fall back to plaintext.
|
|
338
|
+
return { body, header: null };
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
269
342
|
class ReiClient {
|
|
270
343
|
/**
|
|
271
344
|
* @param {ReiClientConfig} config
|
|
@@ -560,6 +633,7 @@ class ReiClient {
|
|
|
560
633
|
const {
|
|
561
634
|
delivery, timeoutMs, onChunk, postTransportGraceMs,
|
|
562
635
|
signal, headers, authorization, endpointPath, onRawRead,
|
|
636
|
+
compressRequest,
|
|
563
637
|
} = opts;
|
|
564
638
|
|
|
565
639
|
if (!delivery || typeof delivery !== 'object') {
|
|
@@ -653,6 +727,7 @@ class ReiClient {
|
|
|
653
727
|
signal: internalAbort.signal,
|
|
654
728
|
onChunk: wrappedOnChunk,
|
|
655
729
|
onRawRead,
|
|
730
|
+
compressRequest,
|
|
656
731
|
});
|
|
657
732
|
if (finalized) return;
|
|
658
733
|
transportEnded = true;
|
|
@@ -1005,15 +1080,23 @@ class ReiClient {
|
|
|
1005
1080
|
*
|
|
1006
1081
|
* @private
|
|
1007
1082
|
* @param {{ url: string, headers: Record<string, string>, body: string }} built
|
|
1008
|
-
* @param {{ signal: AbortSignal, onChunk?: (p: unknown) => Promise<void> | void, onRawRead?: (meta: RawReadMeta) => void }} opts
|
|
1083
|
+
* @param {{ signal: AbortSignal, onChunk?: (p: unknown) => Promise<void> | void, onRawRead?: (meta: RawReadMeta) => void, compressRequest?: boolean | { thresholdBytes?: number } }} opts
|
|
1009
1084
|
* `onRawRead` is forwarded to the SSE consumer for raw read-loop telemetry (see `DeliverOptions.onRawRead`).
|
|
1085
|
+
* `compressRequest` opts the request body into gzip before `fetch` (see `DeliverOptions.compressRequest`).
|
|
1010
1086
|
* @returns {Promise<{ kind: 'sse' } | { kind: 'json', body: unknown }>}
|
|
1011
1087
|
*/
|
|
1012
1088
|
async _runInstantTransport(built, opts) {
|
|
1013
|
-
const { signal, onChunk, onRawRead } = opts;
|
|
1089
|
+
const { signal, onChunk, onRawRead, compressRequest } = opts;
|
|
1014
1090
|
const { url, headers, body } = built;
|
|
1015
1091
|
|
|
1016
|
-
|
|
1092
|
+
// Optionally gzip the request body (opt-in, graceful fallback to plaintext).
|
|
1093
|
+
const { body: wireBody, header: compressionHeader } =
|
|
1094
|
+
await maybeCompressRequestBody(body, compressRequest);
|
|
1095
|
+
const wireHeaders = compressionHeader
|
|
1096
|
+
? { ...headers, [compressionHeader]: 'gzip' }
|
|
1097
|
+
: headers;
|
|
1098
|
+
|
|
1099
|
+
const res = await fetch(url, { method: 'POST', headers: wireHeaders, body: wireBody, signal });
|
|
1017
1100
|
|
|
1018
1101
|
if (!res.ok) {
|
|
1019
1102
|
const text = await res.text().catch(() => '');
|
package/dist/index.d.ts
CHANGED
|
@@ -201,6 +201,17 @@ const TEXT_ENCODER = new TextEncoder();
|
|
|
201
201
|
* silently drops. Use it to tell "connection alive but no business data" apart from "no bytes flowing
|
|
202
202
|
* at all" when diagnosing stalled streams. Purely observational: throws are swallowed and never affect
|
|
203
203
|
* transport. Not invoked for the JSON transport.
|
|
204
|
+
* @property {boolean | { thresholdBytes?: number }} [compressRequest] - Opt-in gzip of the request
|
|
205
|
+
* BODY before it is sent (applies to both the SSE and JSON transports — it compresses the request,
|
|
206
|
+
* not the response). Omit / falsy = OFF and behavior is fully unchanged (backward compatible).
|
|
207
|
+
* `true` or `{}` enables it at the default 16384-byte (16 KB) threshold; `{ thresholdBytes: N }`
|
|
208
|
+
* sets a custom threshold. When enabled, the body is gzip-compressed only if its UTF-8 byte length
|
|
209
|
+
* exceeds the threshold AND the runtime provides `CompressionStream`; otherwise it is sent as
|
|
210
|
+
* plaintext (graceful degradation, never throws). On compression the request gains the custom
|
|
211
|
+
* header `X-Amsg-Request-Encoding: gzip` (NOT standard `Content-Encoding`, which CDNs / proxies
|
|
212
|
+
* would auto-decompress and double-decode) and the body is the raw gzip bytes — the receiving
|
|
213
|
+
* worker is responsible for gunzipping. Use it when delivering large bodies over slow / flaky
|
|
214
|
+
* uplinks where a big upload can outrun the connection's send timeout.
|
|
204
215
|
*/
|
|
205
216
|
|
|
206
217
|
/**
|
|
@@ -266,6 +277,68 @@ function classifyContentType(contentType) {
|
|
|
266
277
|
return 'unknown';
|
|
267
278
|
}
|
|
268
279
|
|
|
280
|
+
/**
|
|
281
|
+
* Default size floor for request-body gzip: bodies at or below this are not
|
|
282
|
+
* worth compressing (the gzip header/overhead can outweigh the gain on tiny
|
|
283
|
+
* payloads). 16 KB matches the contract documented on `DeliverOptions.compressRequest`.
|
|
284
|
+
*/
|
|
285
|
+
const COMPRESS_REQUEST_DEFAULT_THRESHOLD = 16384;
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Custom request header used to mark a gzip-compressed body. Deliberately NOT
|
|
289
|
+
* the standard `Content-Encoding` — CDNs / reverse proxies (Cloudflare, etc.)
|
|
290
|
+
* auto-decompress `Content-Encoding: gzip` on the way in, which would double-
|
|
291
|
+
* decompress and corrupt the body. The receiving worker keys off this custom
|
|
292
|
+
* header to know it must gunzip the body itself.
|
|
293
|
+
*/
|
|
294
|
+
const COMPRESS_REQUEST_HEADER = 'X-Amsg-Request-Encoding';
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Optionally gzip a request body string before it hits `fetch`.
|
|
298
|
+
*
|
|
299
|
+
* Pure optimization with graceful degradation: returns the original plaintext
|
|
300
|
+
* body (and no extra header) whenever compression is disabled, the body is at
|
|
301
|
+
* or below the threshold, the runtime lacks `CompressionStream`, or anything
|
|
302
|
+
* throws. The wire bytes shrink (Chinese / repetitive JSON compresses ~5-8x)
|
|
303
|
+
* so large uploads finish before flaky links time out — without dropping any
|
|
304
|
+
* context. Decompression is the receiving worker's job (keyed off
|
|
305
|
+
* `X-Amsg-Request-Encoding: gzip`).
|
|
306
|
+
*
|
|
307
|
+
* @param {string} body - The already-serialized request body (plaintext JSON).
|
|
308
|
+
* @param {boolean | { thresholdBytes?: number } | undefined} compressRequest
|
|
309
|
+
* `undefined`/falsy ⇒ disabled (no-op, backward compatible). `true` / `{}` ⇒
|
|
310
|
+
* enabled at the 16 KB default. `{ thresholdBytes: N }` ⇒ enabled at N bytes.
|
|
311
|
+
* @returns {Promise<{ body: string | Uint8Array, header: string | null }>}
|
|
312
|
+
* `header` is the gzip marker header name to set when compression happened,
|
|
313
|
+
* or `null` to send plaintext with no extra header.
|
|
314
|
+
*/
|
|
315
|
+
async function maybeCompressRequestBody(body, compressRequest) {
|
|
316
|
+
// Disabled / no opt-in ⇒ behavior unchanged.
|
|
317
|
+
if (!compressRequest) return { body, header: null };
|
|
318
|
+
|
|
319
|
+
const threshold =
|
|
320
|
+
typeof compressRequest === 'object' && typeof compressRequest.thresholdBytes === 'number'
|
|
321
|
+
? compressRequest.thresholdBytes
|
|
322
|
+
: COMPRESS_REQUEST_DEFAULT_THRESHOLD;
|
|
323
|
+
|
|
324
|
+
try {
|
|
325
|
+
if (typeof CompressionStream === 'undefined') return { body, header: null };
|
|
326
|
+
|
|
327
|
+
const bytes = new TextEncoder().encode(body);
|
|
328
|
+
if (bytes.length <= threshold) return { body, header: null };
|
|
329
|
+
|
|
330
|
+
const gz = new Uint8Array(
|
|
331
|
+
await new Response(
|
|
332
|
+
new Blob([bytes]).stream().pipeThrough(new CompressionStream('gzip'))
|
|
333
|
+
).arrayBuffer()
|
|
334
|
+
);
|
|
335
|
+
return { body: gz, header: COMPRESS_REQUEST_HEADER };
|
|
336
|
+
} catch {
|
|
337
|
+
// Compression is an optimization, never a failure mode: fall back to plaintext.
|
|
338
|
+
return { body, header: null };
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
269
342
|
class ReiClient {
|
|
270
343
|
/**
|
|
271
344
|
* @param {ReiClientConfig} config
|
|
@@ -560,6 +633,7 @@ class ReiClient {
|
|
|
560
633
|
const {
|
|
561
634
|
delivery, timeoutMs, onChunk, postTransportGraceMs,
|
|
562
635
|
signal, headers, authorization, endpointPath, onRawRead,
|
|
636
|
+
compressRequest,
|
|
563
637
|
} = opts;
|
|
564
638
|
|
|
565
639
|
if (!delivery || typeof delivery !== 'object') {
|
|
@@ -653,6 +727,7 @@ class ReiClient {
|
|
|
653
727
|
signal: internalAbort.signal,
|
|
654
728
|
onChunk: wrappedOnChunk,
|
|
655
729
|
onRawRead,
|
|
730
|
+
compressRequest,
|
|
656
731
|
});
|
|
657
732
|
if (finalized) return;
|
|
658
733
|
transportEnded = true;
|
|
@@ -1005,15 +1080,23 @@ class ReiClient {
|
|
|
1005
1080
|
*
|
|
1006
1081
|
* @private
|
|
1007
1082
|
* @param {{ url: string, headers: Record<string, string>, body: string }} built
|
|
1008
|
-
* @param {{ signal: AbortSignal, onChunk?: (p: unknown) => Promise<void> | void, onRawRead?: (meta: RawReadMeta) => void }} opts
|
|
1083
|
+
* @param {{ signal: AbortSignal, onChunk?: (p: unknown) => Promise<void> | void, onRawRead?: (meta: RawReadMeta) => void, compressRequest?: boolean | { thresholdBytes?: number } }} opts
|
|
1009
1084
|
* `onRawRead` is forwarded to the SSE consumer for raw read-loop telemetry (see `DeliverOptions.onRawRead`).
|
|
1085
|
+
* `compressRequest` opts the request body into gzip before `fetch` (see `DeliverOptions.compressRequest`).
|
|
1010
1086
|
* @returns {Promise<{ kind: 'sse' } | { kind: 'json', body: unknown }>}
|
|
1011
1087
|
*/
|
|
1012
1088
|
async _runInstantTransport(built, opts) {
|
|
1013
|
-
const { signal, onChunk, onRawRead } = opts;
|
|
1089
|
+
const { signal, onChunk, onRawRead, compressRequest } = opts;
|
|
1014
1090
|
const { url, headers, body } = built;
|
|
1015
1091
|
|
|
1016
|
-
|
|
1092
|
+
// Optionally gzip the request body (opt-in, graceful fallback to plaintext).
|
|
1093
|
+
const { body: wireBody, header: compressionHeader } =
|
|
1094
|
+
await maybeCompressRequestBody(body, compressRequest);
|
|
1095
|
+
const wireHeaders = compressionHeader
|
|
1096
|
+
? { ...headers, [compressionHeader]: 'gzip' }
|
|
1097
|
+
: headers;
|
|
1098
|
+
|
|
1099
|
+
const res = await fetch(url, { method: 'POST', headers: wireHeaders, body: wireBody, signal });
|
|
1017
1100
|
|
|
1018
1101
|
if (!res.ok) {
|
|
1019
1102
|
const text = await res.text().catch(() => '');
|
package/dist/index.mjs
CHANGED
|
@@ -32,6 +32,25 @@ function classifyContentType(contentType) {
|
|
|
32
32
|
if (/^application\/[\w.+-]+\+json$/.test(main)) return "json";
|
|
33
33
|
return "unknown";
|
|
34
34
|
}
|
|
35
|
+
var COMPRESS_REQUEST_DEFAULT_THRESHOLD = 16384;
|
|
36
|
+
var COMPRESS_REQUEST_HEADER = "X-Amsg-Request-Encoding";
|
|
37
|
+
async function maybeCompressRequestBody(body, compressRequest) {
|
|
38
|
+
if (!compressRequest) return { body, header: null };
|
|
39
|
+
const threshold = typeof compressRequest === "object" && typeof compressRequest.thresholdBytes === "number" ? compressRequest.thresholdBytes : COMPRESS_REQUEST_DEFAULT_THRESHOLD;
|
|
40
|
+
try {
|
|
41
|
+
if (typeof CompressionStream === "undefined") return { body, header: null };
|
|
42
|
+
const bytes = new TextEncoder().encode(body);
|
|
43
|
+
if (bytes.length <= threshold) return { body, header: null };
|
|
44
|
+
const gz = new Uint8Array(
|
|
45
|
+
await new Response(
|
|
46
|
+
new Blob([bytes]).stream().pipeThrough(new CompressionStream("gzip"))
|
|
47
|
+
).arrayBuffer()
|
|
48
|
+
);
|
|
49
|
+
return { body: gz, header: COMPRESS_REQUEST_HEADER };
|
|
50
|
+
} catch {
|
|
51
|
+
return { body, header: null };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
35
54
|
var ReiClient = class {
|
|
36
55
|
/**
|
|
37
56
|
* @param {ReiClientConfig} config
|
|
@@ -294,7 +313,8 @@ var ReiClient = class {
|
|
|
294
313
|
headers,
|
|
295
314
|
authorization,
|
|
296
315
|
endpointPath,
|
|
297
|
-
onRawRead
|
|
316
|
+
onRawRead,
|
|
317
|
+
compressRequest
|
|
298
318
|
} = opts;
|
|
299
319
|
if (!delivery || typeof delivery !== "object") {
|
|
300
320
|
throw new TypeError("[rei-standard-amsg-client] deliver() requires opts.delivery (discriminated union)");
|
|
@@ -356,7 +376,8 @@ var ReiClient = class {
|
|
|
356
376
|
const result = await this._runInstantTransport(built, {
|
|
357
377
|
signal: internalAbort.signal,
|
|
358
378
|
onChunk: wrappedOnChunk,
|
|
359
|
-
onRawRead
|
|
379
|
+
onRawRead,
|
|
380
|
+
compressRequest
|
|
360
381
|
});
|
|
361
382
|
if (finalized) return;
|
|
362
383
|
transportEnded = true;
|
|
@@ -635,14 +656,17 @@ var ReiClient = class {
|
|
|
635
656
|
*
|
|
636
657
|
* @private
|
|
637
658
|
* @param {{ url: string, headers: Record<string, string>, body: string }} built
|
|
638
|
-
* @param {{ signal: AbortSignal, onChunk?: (p: unknown) => Promise<void> | void, onRawRead?: (meta: RawReadMeta) => void }} opts
|
|
659
|
+
* @param {{ signal: AbortSignal, onChunk?: (p: unknown) => Promise<void> | void, onRawRead?: (meta: RawReadMeta) => void, compressRequest?: boolean | { thresholdBytes?: number } }} opts
|
|
639
660
|
* `onRawRead` is forwarded to the SSE consumer for raw read-loop telemetry (see `DeliverOptions.onRawRead`).
|
|
661
|
+
* `compressRequest` opts the request body into gzip before `fetch` (see `DeliverOptions.compressRequest`).
|
|
640
662
|
* @returns {Promise<{ kind: 'sse' } | { kind: 'json', body: unknown }>}
|
|
641
663
|
*/
|
|
642
664
|
async _runInstantTransport(built, opts) {
|
|
643
|
-
const { signal, onChunk, onRawRead } = opts;
|
|
665
|
+
const { signal, onChunk, onRawRead, compressRequest } = opts;
|
|
644
666
|
const { url, headers, body } = built;
|
|
645
|
-
const
|
|
667
|
+
const { body: wireBody, header: compressionHeader } = await maybeCompressRequestBody(body, compressRequest);
|
|
668
|
+
const wireHeaders = compressionHeader ? { ...headers, [compressionHeader]: "gzip" } : headers;
|
|
669
|
+
const res = await fetch(url, { method: "POST", headers: wireHeaders, body: wireBody, signal });
|
|
646
670
|
if (!res.ok) {
|
|
647
671
|
const text2 = await res.text().catch(() => "");
|
|
648
672
|
const err = new Error(`Instant request failed: ${res.status} ${text2}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rei-standard/amsg-client",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.0",
|
|
4
4
|
"description": "ReiStandard Active Messaging browser client SDK — also re-exports shared push types, builders, and guards from @rei-standard/amsg-shared",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|