@rei-standard/amsg-client 2.4.0-next.0 → 2.4.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 +23 -10
- package/dist/index.cjs +21 -13
- package/dist/index.d.cts +26 -21
- package/dist/index.d.ts +26 -21
- package/dist/index.mjs +21 -13
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
`@rei-standard/amsg-client` 是 ReiStandard 主动消息标准的浏览器端 SDK 包,负责加密请求、解密响应和 Push 订阅。
|
|
4
4
|
|
|
5
|
-
## v2.4.0
|
|
5
|
+
## v2.4.0 — SSE consumer
|
|
6
6
|
|
|
7
|
-
新增 `consumeInstantStream(payload, endpointPath?, options)`:消费 amsg-instant 0.9.0+ 的 SSE 默认响应,按 frame 解析 `event: payload` / `event: done` / `event: error` 分发到 `options.onPayload`。前台场景下 push 不再绕 push service → SW → IDB → main thread 整条链路,延迟少一个数量级。详见下方 [SSE 流消费](#sse-流消费-consumeinstantstream240配合-amsg-instant-090)
|
|
7
|
+
新增 `consumeInstantStream(payload, endpointPath?, options)`:消费 amsg-instant 0.9.0+ 的 SSE 默认响应,按 frame 解析 `event: payload` / `event: done` / `event: error` 分发到 `options.onPayload`。前台场景下 push 不再绕 push service → SW → IDB → main thread 整条链路,延迟少一个数量级。详见下方 [SSE 流消费](#sse-流消费-consumeinstantstream240配合-amsg-instant-090)。请求体默认不再由 client 做本地体积限制;需要本地护栏时可在构造器传 `maxPayloadBytes`。
|
|
8
8
|
|
|
9
9
|
## v2.3.0 — Shared push types
|
|
10
10
|
|
|
@@ -179,14 +179,14 @@ await client.sendInstant({
|
|
|
179
179
|
|
|
180
180
|
`splitPattern` 类型是 `string | string[]`。`scheduleMessage` 也支持,`updateMessage` 可显式传 `splitPattern: null` 重置回默认。client SDK 完全透传不校验,所有错误在 Worker / Server 端返回(每项 ≤ 200 字符、数组 ≤ 10 项、必须能 `new RegExp()` 通过)。
|
|
181
181
|
|
|
182
|
-
|
|
182
|
+
**两个常见坑**:
|
|
183
183
|
|
|
184
184
|
- 传**正则 source**,不要带 `/.../` 也不要尾 flag。`'/foo/i'` 会被当字面量斜杠 + 字面量 `i`,不是大小写不敏感的 `foo`。大小写不敏感请用 `[Aa]` 字符类替代。
|
|
185
185
|
- 想让分隔符回贴到前一段(默认行为),把分隔符包进 `(...)` 捕获组。库**不会自动包**——传 `'\\n+'` 而不是 `'(\\n+)'` 会得到首尾相连、分隔符丢失的奇怪结果。
|
|
186
186
|
|
|
187
187
|
### SSE 流消费 `consumeInstantStream`(2.4.0+,配合 amsg-instant 0.9.0+)
|
|
188
188
|
|
|
189
|
-
`sendInstant()` 只在显式 `Accept: application/json` opt-out 模式下使用。amsg-instant 0.9.0 起默认走 SSE 流式传输——每条 push 通过 `event: payload`
|
|
189
|
+
`sendInstant()` 只在显式 `Accept: application/json` opt-out 模式下使用。amsg-instant 0.9.0 起默认走 SSE 流式传输——每条 push 通过 `event: payload` 直接打到主线程,前台延迟从约 1–3s(push service → SW → IDB → window)降到次百毫秒。Web Push backup 同时**常开 always-on**(即使 SSE enqueue 成功也照样发一份),用 SW / client 端按 `messageId` 做 dedupe 把两路收敛回一次。前台场景应该改用 `consumeInstantStream()`。
|
|
190
190
|
|
|
191
191
|
```js
|
|
192
192
|
const abort = new AbortController();
|
|
@@ -208,7 +208,12 @@ try {
|
|
|
208
208
|
}
|
|
209
209
|
```
|
|
210
210
|
|
|
211
|
-
请求体跟 `sendInstant()` 完全一样——包括必须的 `pushSubscription
|
|
211
|
+
请求体跟 `sendInstant()` 完全一样——包括必须的 `pushSubscription`。两条投递路径同时跑:
|
|
212
|
+
|
|
213
|
+
1. **SSE 直送**(首选)——payload 走 `event: payload` 直接到 `onPayload`。
|
|
214
|
+
2. **Web Push always-on backup**——成功 enqueue 的 payload 也会通过 `pushSubscription` 发一份;SSE 写失败 / 客户端断开 / enqueue throw 时也走这条路兜底。
|
|
215
|
+
|
|
216
|
+
同一 `messageId` 两路都到,由 SW 的 dedupe gate 或客户端按 ID 幂等去重收敛成一次业务投递与一次(必要时的)通知。
|
|
212
217
|
|
|
213
218
|
#### 错误语义
|
|
214
219
|
|
|
@@ -218,25 +223,33 @@ try {
|
|
|
218
223
|
|
|
219
224
|
`endpointPath` 默认 `'/instant'`,按需传 `'/continue'` 续跑 tool result。加密 / 明文两种 transport 与 `sendInstant()` 共享构造器配置(`instantEncryption` / `instantClientToken`),调用方无感。
|
|
220
225
|
|
|
221
|
-
### 本地软清空:`avatarUrl`
|
|
226
|
+
### 本地软清空:`avatarUrl` 与可选 payload 体积上限(2.2.4+ / 2.4.0+)
|
|
222
227
|
|
|
223
|
-
`scheduleMessage` / `sendInstant` / `updateMessage`
|
|
228
|
+
`scheduleMessage` / `sendInstant` / `consumeInstantStream` / `updateMessage` 在发请求**之前**会保留 `avatarUrl` 软清空保护。请求体大小默认不限制;如果你希望在 SDK 本地先挡住过大的请求,可以在构造器显式传 `maxPayloadBytes`:
|
|
229
|
+
|
|
230
|
+
```js
|
|
231
|
+
const client = new ReiClient({
|
|
232
|
+
baseUrl: '/api/v1',
|
|
233
|
+
userId,
|
|
234
|
+
maxPayloadBytes: 256_000, // 可选;默认 null / 不限制
|
|
235
|
+
});
|
|
236
|
+
```
|
|
224
237
|
|
|
225
238
|
| 触发条件 | 处理方式 | 触发原因(背景说明,不在 message 里) |
|
|
226
239
|
| --- | --- | --- |
|
|
227
240
|
| `payload.avatarUrl` 以 `data:` 开头(含 `data:image/...;base64,...`) | `console.warn` + 在 payload 上把 `avatarUrl` 置为 `null`,请求照发(`updateMessage` 从 patch 里删除该字段,保留服务端原头像) | base64 内嵌头像把单个 push payload 撑到几十 KB,远端 Web Push 服务直接返回 4KB 超限 / 网关 `413`。 |
|
|
228
241
|
| `payload.avatarUrl` 长度 > 2048 字符 | 同上 | 同上。建议用 CDN 缩略图 URL。 |
|
|
229
242
|
| `payload.avatarUrl` 不是字符串 | 同上 | 类型错误。 |
|
|
230
|
-
| `JSON.stringify(payload)` UTF-8
|
|
243
|
+
| 已配置 `maxPayloadBytes`,且 `JSON.stringify(payload)` UTF-8 字节数超过该值 | 抛出 `Error.code === 'PAYLOAD_TOO_LARGE_LOCAL'`,错误对象带 `.details = { method, actualBytes, limitBytes }` | 只在调用方主动需要本地请求体护栏时启用。Web Push 单条回复超限由 `amsg-instant` 的 BlobStore / multipart 输出链路处理,不靠 client 限制请求体。 |
|
|
231
244
|
|
|
232
|
-
头像是装饰字段,单个不合规 URL 不再让整次调度 / 推送挂掉;想拦到错误请监听 `console.warn`,或在调用前自己用 `validateAvatarUrl` 预检(server / instant
|
|
245
|
+
头像是装饰字段,单个不合规 URL 不再让整次调度 / 推送挂掉;想拦到错误请监听 `console.warn`,或在调用前自己用 `validateAvatarUrl` 预检(server / instant 包都有导出)。未配置 `maxPayloadBytes` 时不会产生 `PAYLOAD_TOO_LARGE_LOCAL`;配置后照常用 try/catch 捕获:
|
|
233
246
|
|
|
234
247
|
```js
|
|
235
248
|
try {
|
|
236
249
|
await client.sendInstant(payload);
|
|
237
250
|
} catch (err) {
|
|
238
251
|
if (err.code === 'PAYLOAD_TOO_LARGE_LOCAL') {
|
|
239
|
-
// err.details = { method: 'sendInstant', actualBytes:
|
|
252
|
+
// err.details = { method: 'sendInstant', actualBytes: 87320, limitBytes: 256000 }
|
|
240
253
|
} else {
|
|
241
254
|
throw err;
|
|
242
255
|
}
|
package/dist/index.cjs
CHANGED
|
@@ -35,7 +35,6 @@ __export(src_exports, {
|
|
|
35
35
|
module.exports = __toCommonJS(src_exports);
|
|
36
36
|
var import_amsg_shared = require("@rei-standard/amsg-shared");
|
|
37
37
|
var AVATAR_URL_MAX_LENGTH = 2048;
|
|
38
|
-
var PAYLOAD_LOCAL_MAX_BYTES = 3072;
|
|
39
38
|
function makeLocalError(code, message, details) {
|
|
40
39
|
const err = new Error(`[rei-standard-amsg-client] ${message}`);
|
|
41
40
|
err.code = code;
|
|
@@ -67,6 +66,7 @@ var ReiClient = class {
|
|
|
67
66
|
this._userKey = null;
|
|
68
67
|
this._instantEncryption = instantEncryption;
|
|
69
68
|
this._instantClientToken = typeof config.instantClientToken === "string" && config.instantClientToken ? config.instantClientToken : "";
|
|
69
|
+
this._maxPayloadBytes = normalizeMaxPayloadBytes(config.maxPayloadBytes);
|
|
70
70
|
}
|
|
71
71
|
/**
|
|
72
72
|
* Resolve the base URL for a given endpoint, falling back to `baseUrl`.
|
|
@@ -119,8 +119,8 @@ var ReiClient = class {
|
|
|
119
119
|
*
|
|
120
120
|
* If `avatarUrl` is unusable (`data:` URI, > 2 KB, or non-string), the
|
|
121
121
|
* client soft-strips it on the payload and emits a `console.warn` — the
|
|
122
|
-
* schedule still ships, just without an avatar.
|
|
123
|
-
*
|
|
122
|
+
* schedule still ships, just without an avatar. If `maxPayloadBytes` is
|
|
123
|
+
* configured, oversized JSON payloads throw `PAYLOAD_TOO_LARGE_LOCAL`.
|
|
124
124
|
*
|
|
125
125
|
* @param {Object} payload - Schedule message payload.
|
|
126
126
|
* @returns {Promise<Object>} API response body.
|
|
@@ -165,8 +165,8 @@ var ReiClient = class {
|
|
|
165
165
|
*
|
|
166
166
|
* If `avatarUrl` is unusable (`data:` URI, > 2 KB, or non-string), the
|
|
167
167
|
* client soft-strips it on the payload and emits a `console.warn` — the
|
|
168
|
-
* push still ships, just without an icon.
|
|
169
|
-
*
|
|
168
|
+
* push still ships, just without an icon. If `maxPayloadBytes` is
|
|
169
|
+
* configured, oversized JSON payloads throw `PAYLOAD_TOO_LARGE_LOCAL`.
|
|
170
170
|
*
|
|
171
171
|
* @param {Object} payload - Instant message payload.
|
|
172
172
|
* @param {string} [endpointPath] - Path under the resolved base URL. Default '/instant'.
|
|
@@ -347,8 +347,8 @@ ${piece}` : piece;
|
|
|
347
347
|
* If `updates.avatarUrl` is unusable (`data:` URI, > 2 KB, or non-string),
|
|
348
348
|
* the client soft-strips it from the patch and emits a `console.warn` —
|
|
349
349
|
* the rest of the update still applies, and the stored avatar is left
|
|
350
|
-
* untouched.
|
|
351
|
-
*
|
|
350
|
+
* untouched. If `maxPayloadBytes` is configured, oversized JSON patches
|
|
351
|
+
* throw `PAYLOAD_TOO_LARGE_LOCAL`.
|
|
352
352
|
*
|
|
353
353
|
* @param {string} uuid - Task UUID.
|
|
354
354
|
* @param {Object} updates - Fields to update.
|
|
@@ -469,21 +469,22 @@ ${piece}` : piece;
|
|
|
469
469
|
return false;
|
|
470
470
|
}
|
|
471
471
|
/**
|
|
472
|
-
*
|
|
473
|
-
*
|
|
474
|
-
*
|
|
472
|
+
* Enforce the optional local request payload cap before encryption.
|
|
473
|
+
* By default there is no SDK-level request-size limit; runtime, proxy,
|
|
474
|
+
* database, and LLM-provider limits remain the deployer's boundary.
|
|
475
475
|
*
|
|
476
476
|
* @private
|
|
477
477
|
* @param {string} bodyJson - `JSON.stringify(payload)`.
|
|
478
478
|
* @param {string} methodName
|
|
479
479
|
*/
|
|
480
480
|
_assertPayloadSize(bodyJson, methodName) {
|
|
481
|
+
if (this._maxPayloadBytes == null) return;
|
|
481
482
|
const bytes = new TextEncoder().encode(bodyJson).length;
|
|
482
|
-
if (bytes >
|
|
483
|
+
if (bytes > this._maxPayloadBytes) {
|
|
483
484
|
throw makeLocalError(
|
|
484
485
|
"PAYLOAD_TOO_LARGE_LOCAL",
|
|
485
|
-
`${methodName} payload \u4F53\u79EF ${bytes} \u5B57\u8282\u8D85\u8FC7\u672C\u5730\u4E0A\u9650 ${
|
|
486
|
-
{ method: methodName, actualBytes: bytes, limitBytes:
|
|
486
|
+
`${methodName} payload \u4F53\u79EF ${bytes} \u5B57\u8282\u8D85\u8FC7\u672C\u5730\u4E0A\u9650 ${this._maxPayloadBytes} \u5B57\u8282`,
|
|
487
|
+
{ method: methodName, actualBytes: bytes, limitBytes: this._maxPayloadBytes }
|
|
487
488
|
);
|
|
488
489
|
}
|
|
489
490
|
}
|
|
@@ -561,3 +562,10 @@ ${piece}` : piece;
|
|
|
561
562
|
return arr;
|
|
562
563
|
}
|
|
563
564
|
};
|
|
565
|
+
function normalizeMaxPayloadBytes(value) {
|
|
566
|
+
if (value === void 0 || value === null) return null;
|
|
567
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
568
|
+
throw new TypeError("[rei-standard-amsg-client] maxPayloadBytes must be a positive integer when set");
|
|
569
|
+
}
|
|
570
|
+
return value;
|
|
571
|
+
}
|
package/dist/index.d.cts
CHANGED
|
@@ -61,6 +61,9 @@ export { MESSAGE_KIND, MESSAGE_TYPE, PUSH_SOURCE, buildContentPush, buildErrorPu
|
|
|
61
61
|
* a *weak* shared secret — it ships inside any
|
|
62
62
|
* frontend bundle that uses it, so devtools can
|
|
63
63
|
* read it. Use for casual URL-direct abuse only.
|
|
64
|
+
* @property {number|null} [maxPayloadBytes=null] - Optional local UTF-8 byte cap for outgoing request
|
|
65
|
+
* payloads before encryption. `null` / omitted means
|
|
66
|
+
* no SDK-level request-size limit.
|
|
64
67
|
*/
|
|
65
68
|
|
|
66
69
|
/**
|
|
@@ -71,15 +74,6 @@ export { MESSAGE_KIND, MESSAGE_TYPE, PUSH_SOURCE, buildContentPush, buildErrorPu
|
|
|
71
74
|
*/
|
|
72
75
|
const AVATAR_URL_MAX_LENGTH = 2048;
|
|
73
76
|
|
|
74
|
-
/**
|
|
75
|
-
* Max byte length of a single outgoing payload (3 KB, measured pre-encryption
|
|
76
|
-
* on the plaintext JSON body). Anything over this is almost certainly a base64
|
|
77
|
-
* avatar smuggled into `avatarUrl` and will trigger downstream `413 Payload
|
|
78
|
-
* Too Large` or hit the Web Push 4 KB hard limit at delivery. We bail locally
|
|
79
|
-
* to save a remote round-trip and give a precise error.
|
|
80
|
-
*/
|
|
81
|
-
const PAYLOAD_LOCAL_MAX_BYTES = 3072;
|
|
82
|
-
|
|
83
77
|
function makeLocalError(code, message, details) {
|
|
84
78
|
const err = new Error(`[rei-standard-amsg-client] ${message}`);
|
|
85
79
|
err.code = code;
|
|
@@ -122,6 +116,8 @@ class ReiClient {
|
|
|
122
116
|
this._instantClientToken = typeof config.instantClientToken === 'string' && config.instantClientToken
|
|
123
117
|
? config.instantClientToken
|
|
124
118
|
: '';
|
|
119
|
+
/** @private */
|
|
120
|
+
this._maxPayloadBytes = normalizeMaxPayloadBytes(config.maxPayloadBytes);
|
|
125
121
|
}
|
|
126
122
|
|
|
127
123
|
/**
|
|
@@ -183,8 +179,8 @@ class ReiClient {
|
|
|
183
179
|
*
|
|
184
180
|
* If `avatarUrl` is unusable (`data:` URI, > 2 KB, or non-string), the
|
|
185
181
|
* client soft-strips it on the payload and emits a `console.warn` — the
|
|
186
|
-
* schedule still ships, just without an avatar.
|
|
187
|
-
*
|
|
182
|
+
* schedule still ships, just without an avatar. If `maxPayloadBytes` is
|
|
183
|
+
* configured, oversized JSON payloads throw `PAYLOAD_TOO_LARGE_LOCAL`.
|
|
188
184
|
*
|
|
189
185
|
* @param {Object} payload - Schedule message payload.
|
|
190
186
|
* @returns {Promise<Object>} API response body.
|
|
@@ -232,8 +228,8 @@ class ReiClient {
|
|
|
232
228
|
*
|
|
233
229
|
* If `avatarUrl` is unusable (`data:` URI, > 2 KB, or non-string), the
|
|
234
230
|
* client soft-strips it on the payload and emits a `console.warn` — the
|
|
235
|
-
* push still ships, just without an icon.
|
|
236
|
-
*
|
|
231
|
+
* push still ships, just without an icon. If `maxPayloadBytes` is
|
|
232
|
+
* configured, oversized JSON payloads throw `PAYLOAD_TOO_LARGE_LOCAL`.
|
|
237
233
|
*
|
|
238
234
|
* @param {Object} payload - Instant message payload.
|
|
239
235
|
* @param {string} [endpointPath] - Path under the resolved base URL. Default '/instant'.
|
|
@@ -430,8 +426,8 @@ class ReiClient {
|
|
|
430
426
|
* If `updates.avatarUrl` is unusable (`data:` URI, > 2 KB, or non-string),
|
|
431
427
|
* the client soft-strips it from the patch and emits a `console.warn` —
|
|
432
428
|
* the rest of the update still applies, and the stored avatar is left
|
|
433
|
-
* untouched.
|
|
434
|
-
*
|
|
429
|
+
* untouched. If `maxPayloadBytes` is configured, oversized JSON patches
|
|
430
|
+
* throw `PAYLOAD_TOO_LARGE_LOCAL`.
|
|
435
431
|
*
|
|
436
432
|
* @param {string} uuid - Task UUID.
|
|
437
433
|
* @param {Object} updates - Fields to update.
|
|
@@ -569,21 +565,22 @@ class ReiClient {
|
|
|
569
565
|
}
|
|
570
566
|
|
|
571
567
|
/**
|
|
572
|
-
*
|
|
573
|
-
*
|
|
574
|
-
*
|
|
568
|
+
* Enforce the optional local request payload cap before encryption.
|
|
569
|
+
* By default there is no SDK-level request-size limit; runtime, proxy,
|
|
570
|
+
* database, and LLM-provider limits remain the deployer's boundary.
|
|
575
571
|
*
|
|
576
572
|
* @private
|
|
577
573
|
* @param {string} bodyJson - `JSON.stringify(payload)`.
|
|
578
574
|
* @param {string} methodName
|
|
579
575
|
*/
|
|
580
576
|
_assertPayloadSize(bodyJson, methodName) {
|
|
577
|
+
if (this._maxPayloadBytes == null) return;
|
|
581
578
|
const bytes = new TextEncoder().encode(bodyJson).length;
|
|
582
|
-
if (bytes >
|
|
579
|
+
if (bytes > this._maxPayloadBytes) {
|
|
583
580
|
throw makeLocalError(
|
|
584
581
|
'PAYLOAD_TOO_LARGE_LOCAL',
|
|
585
|
-
`${methodName} payload 体积 ${bytes} 字节超过本地上限 ${
|
|
586
|
-
{ method: methodName, actualBytes: bytes, limitBytes:
|
|
582
|
+
`${methodName} payload 体积 ${bytes} 字节超过本地上限 ${this._maxPayloadBytes} 字节`,
|
|
583
|
+
{ method: methodName, actualBytes: bytes, limitBytes: this._maxPayloadBytes }
|
|
587
584
|
);
|
|
588
585
|
}
|
|
589
586
|
}
|
|
@@ -676,4 +673,12 @@ class ReiClient {
|
|
|
676
673
|
}
|
|
677
674
|
}
|
|
678
675
|
|
|
676
|
+
function normalizeMaxPayloadBytes(value) {
|
|
677
|
+
if (value === undefined || value === null) return null;
|
|
678
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
679
|
+
throw new TypeError('[rei-standard-amsg-client] maxPayloadBytes must be a positive integer when set');
|
|
680
|
+
}
|
|
681
|
+
return value;
|
|
682
|
+
}
|
|
683
|
+
|
|
679
684
|
export { ReiClient };
|
package/dist/index.d.ts
CHANGED
|
@@ -61,6 +61,9 @@ export { MESSAGE_KIND, MESSAGE_TYPE, PUSH_SOURCE, buildContentPush, buildErrorPu
|
|
|
61
61
|
* a *weak* shared secret — it ships inside any
|
|
62
62
|
* frontend bundle that uses it, so devtools can
|
|
63
63
|
* read it. Use for casual URL-direct abuse only.
|
|
64
|
+
* @property {number|null} [maxPayloadBytes=null] - Optional local UTF-8 byte cap for outgoing request
|
|
65
|
+
* payloads before encryption. `null` / omitted means
|
|
66
|
+
* no SDK-level request-size limit.
|
|
64
67
|
*/
|
|
65
68
|
|
|
66
69
|
/**
|
|
@@ -71,15 +74,6 @@ export { MESSAGE_KIND, MESSAGE_TYPE, PUSH_SOURCE, buildContentPush, buildErrorPu
|
|
|
71
74
|
*/
|
|
72
75
|
const AVATAR_URL_MAX_LENGTH = 2048;
|
|
73
76
|
|
|
74
|
-
/**
|
|
75
|
-
* Max byte length of a single outgoing payload (3 KB, measured pre-encryption
|
|
76
|
-
* on the plaintext JSON body). Anything over this is almost certainly a base64
|
|
77
|
-
* avatar smuggled into `avatarUrl` and will trigger downstream `413 Payload
|
|
78
|
-
* Too Large` or hit the Web Push 4 KB hard limit at delivery. We bail locally
|
|
79
|
-
* to save a remote round-trip and give a precise error.
|
|
80
|
-
*/
|
|
81
|
-
const PAYLOAD_LOCAL_MAX_BYTES = 3072;
|
|
82
|
-
|
|
83
77
|
function makeLocalError(code, message, details) {
|
|
84
78
|
const err = new Error(`[rei-standard-amsg-client] ${message}`);
|
|
85
79
|
err.code = code;
|
|
@@ -122,6 +116,8 @@ class ReiClient {
|
|
|
122
116
|
this._instantClientToken = typeof config.instantClientToken === 'string' && config.instantClientToken
|
|
123
117
|
? config.instantClientToken
|
|
124
118
|
: '';
|
|
119
|
+
/** @private */
|
|
120
|
+
this._maxPayloadBytes = normalizeMaxPayloadBytes(config.maxPayloadBytes);
|
|
125
121
|
}
|
|
126
122
|
|
|
127
123
|
/**
|
|
@@ -183,8 +179,8 @@ class ReiClient {
|
|
|
183
179
|
*
|
|
184
180
|
* If `avatarUrl` is unusable (`data:` URI, > 2 KB, or non-string), the
|
|
185
181
|
* client soft-strips it on the payload and emits a `console.warn` — the
|
|
186
|
-
* schedule still ships, just without an avatar.
|
|
187
|
-
*
|
|
182
|
+
* schedule still ships, just without an avatar. If `maxPayloadBytes` is
|
|
183
|
+
* configured, oversized JSON payloads throw `PAYLOAD_TOO_LARGE_LOCAL`.
|
|
188
184
|
*
|
|
189
185
|
* @param {Object} payload - Schedule message payload.
|
|
190
186
|
* @returns {Promise<Object>} API response body.
|
|
@@ -232,8 +228,8 @@ class ReiClient {
|
|
|
232
228
|
*
|
|
233
229
|
* If `avatarUrl` is unusable (`data:` URI, > 2 KB, or non-string), the
|
|
234
230
|
* client soft-strips it on the payload and emits a `console.warn` — the
|
|
235
|
-
* push still ships, just without an icon.
|
|
236
|
-
*
|
|
231
|
+
* push still ships, just without an icon. If `maxPayloadBytes` is
|
|
232
|
+
* configured, oversized JSON payloads throw `PAYLOAD_TOO_LARGE_LOCAL`.
|
|
237
233
|
*
|
|
238
234
|
* @param {Object} payload - Instant message payload.
|
|
239
235
|
* @param {string} [endpointPath] - Path under the resolved base URL. Default '/instant'.
|
|
@@ -430,8 +426,8 @@ class ReiClient {
|
|
|
430
426
|
* If `updates.avatarUrl` is unusable (`data:` URI, > 2 KB, or non-string),
|
|
431
427
|
* the client soft-strips it from the patch and emits a `console.warn` —
|
|
432
428
|
* the rest of the update still applies, and the stored avatar is left
|
|
433
|
-
* untouched.
|
|
434
|
-
*
|
|
429
|
+
* untouched. If `maxPayloadBytes` is configured, oversized JSON patches
|
|
430
|
+
* throw `PAYLOAD_TOO_LARGE_LOCAL`.
|
|
435
431
|
*
|
|
436
432
|
* @param {string} uuid - Task UUID.
|
|
437
433
|
* @param {Object} updates - Fields to update.
|
|
@@ -569,21 +565,22 @@ class ReiClient {
|
|
|
569
565
|
}
|
|
570
566
|
|
|
571
567
|
/**
|
|
572
|
-
*
|
|
573
|
-
*
|
|
574
|
-
*
|
|
568
|
+
* Enforce the optional local request payload cap before encryption.
|
|
569
|
+
* By default there is no SDK-level request-size limit; runtime, proxy,
|
|
570
|
+
* database, and LLM-provider limits remain the deployer's boundary.
|
|
575
571
|
*
|
|
576
572
|
* @private
|
|
577
573
|
* @param {string} bodyJson - `JSON.stringify(payload)`.
|
|
578
574
|
* @param {string} methodName
|
|
579
575
|
*/
|
|
580
576
|
_assertPayloadSize(bodyJson, methodName) {
|
|
577
|
+
if (this._maxPayloadBytes == null) return;
|
|
581
578
|
const bytes = new TextEncoder().encode(bodyJson).length;
|
|
582
|
-
if (bytes >
|
|
579
|
+
if (bytes > this._maxPayloadBytes) {
|
|
583
580
|
throw makeLocalError(
|
|
584
581
|
'PAYLOAD_TOO_LARGE_LOCAL',
|
|
585
|
-
`${methodName} payload 体积 ${bytes} 字节超过本地上限 ${
|
|
586
|
-
{ method: methodName, actualBytes: bytes, limitBytes:
|
|
582
|
+
`${methodName} payload 体积 ${bytes} 字节超过本地上限 ${this._maxPayloadBytes} 字节`,
|
|
583
|
+
{ method: methodName, actualBytes: bytes, limitBytes: this._maxPayloadBytes }
|
|
587
584
|
);
|
|
588
585
|
}
|
|
589
586
|
}
|
|
@@ -676,4 +673,12 @@ class ReiClient {
|
|
|
676
673
|
}
|
|
677
674
|
}
|
|
678
675
|
|
|
676
|
+
function normalizeMaxPayloadBytes(value) {
|
|
677
|
+
if (value === undefined || value === null) return null;
|
|
678
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
679
|
+
throw new TypeError('[rei-standard-amsg-client] maxPayloadBytes must be a positive integer when set');
|
|
680
|
+
}
|
|
681
|
+
return value;
|
|
682
|
+
}
|
|
683
|
+
|
|
679
684
|
export { ReiClient };
|
package/dist/index.mjs
CHANGED
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
isErrorPush
|
|
14
14
|
} from "@rei-standard/amsg-shared";
|
|
15
15
|
var AVATAR_URL_MAX_LENGTH = 2048;
|
|
16
|
-
var PAYLOAD_LOCAL_MAX_BYTES = 3072;
|
|
17
16
|
function makeLocalError(code, message, details) {
|
|
18
17
|
const err = new Error(`[rei-standard-amsg-client] ${message}`);
|
|
19
18
|
err.code = code;
|
|
@@ -45,6 +44,7 @@ var ReiClient = class {
|
|
|
45
44
|
this._userKey = null;
|
|
46
45
|
this._instantEncryption = instantEncryption;
|
|
47
46
|
this._instantClientToken = typeof config.instantClientToken === "string" && config.instantClientToken ? config.instantClientToken : "";
|
|
47
|
+
this._maxPayloadBytes = normalizeMaxPayloadBytes(config.maxPayloadBytes);
|
|
48
48
|
}
|
|
49
49
|
/**
|
|
50
50
|
* Resolve the base URL for a given endpoint, falling back to `baseUrl`.
|
|
@@ -97,8 +97,8 @@ var ReiClient = class {
|
|
|
97
97
|
*
|
|
98
98
|
* If `avatarUrl` is unusable (`data:` URI, > 2 KB, or non-string), the
|
|
99
99
|
* client soft-strips it on the payload and emits a `console.warn` — the
|
|
100
|
-
* schedule still ships, just without an avatar.
|
|
101
|
-
*
|
|
100
|
+
* schedule still ships, just without an avatar. If `maxPayloadBytes` is
|
|
101
|
+
* configured, oversized JSON payloads throw `PAYLOAD_TOO_LARGE_LOCAL`.
|
|
102
102
|
*
|
|
103
103
|
* @param {Object} payload - Schedule message payload.
|
|
104
104
|
* @returns {Promise<Object>} API response body.
|
|
@@ -143,8 +143,8 @@ var ReiClient = class {
|
|
|
143
143
|
*
|
|
144
144
|
* If `avatarUrl` is unusable (`data:` URI, > 2 KB, or non-string), the
|
|
145
145
|
* client soft-strips it on the payload and emits a `console.warn` — the
|
|
146
|
-
* push still ships, just without an icon.
|
|
147
|
-
*
|
|
146
|
+
* push still ships, just without an icon. If `maxPayloadBytes` is
|
|
147
|
+
* configured, oversized JSON payloads throw `PAYLOAD_TOO_LARGE_LOCAL`.
|
|
148
148
|
*
|
|
149
149
|
* @param {Object} payload - Instant message payload.
|
|
150
150
|
* @param {string} [endpointPath] - Path under the resolved base URL. Default '/instant'.
|
|
@@ -325,8 +325,8 @@ ${piece}` : piece;
|
|
|
325
325
|
* If `updates.avatarUrl` is unusable (`data:` URI, > 2 KB, or non-string),
|
|
326
326
|
* the client soft-strips it from the patch and emits a `console.warn` —
|
|
327
327
|
* the rest of the update still applies, and the stored avatar is left
|
|
328
|
-
* untouched.
|
|
329
|
-
*
|
|
328
|
+
* untouched. If `maxPayloadBytes` is configured, oversized JSON patches
|
|
329
|
+
* throw `PAYLOAD_TOO_LARGE_LOCAL`.
|
|
330
330
|
*
|
|
331
331
|
* @param {string} uuid - Task UUID.
|
|
332
332
|
* @param {Object} updates - Fields to update.
|
|
@@ -447,21 +447,22 @@ ${piece}` : piece;
|
|
|
447
447
|
return false;
|
|
448
448
|
}
|
|
449
449
|
/**
|
|
450
|
-
*
|
|
451
|
-
*
|
|
452
|
-
*
|
|
450
|
+
* Enforce the optional local request payload cap before encryption.
|
|
451
|
+
* By default there is no SDK-level request-size limit; runtime, proxy,
|
|
452
|
+
* database, and LLM-provider limits remain the deployer's boundary.
|
|
453
453
|
*
|
|
454
454
|
* @private
|
|
455
455
|
* @param {string} bodyJson - `JSON.stringify(payload)`.
|
|
456
456
|
* @param {string} methodName
|
|
457
457
|
*/
|
|
458
458
|
_assertPayloadSize(bodyJson, methodName) {
|
|
459
|
+
if (this._maxPayloadBytes == null) return;
|
|
459
460
|
const bytes = new TextEncoder().encode(bodyJson).length;
|
|
460
|
-
if (bytes >
|
|
461
|
+
if (bytes > this._maxPayloadBytes) {
|
|
461
462
|
throw makeLocalError(
|
|
462
463
|
"PAYLOAD_TOO_LARGE_LOCAL",
|
|
463
|
-
`${methodName} payload \u4F53\u79EF ${bytes} \u5B57\u8282\u8D85\u8FC7\u672C\u5730\u4E0A\u9650 ${
|
|
464
|
-
{ method: methodName, actualBytes: bytes, limitBytes:
|
|
464
|
+
`${methodName} payload \u4F53\u79EF ${bytes} \u5B57\u8282\u8D85\u8FC7\u672C\u5730\u4E0A\u9650 ${this._maxPayloadBytes} \u5B57\u8282`,
|
|
465
|
+
{ method: methodName, actualBytes: bytes, limitBytes: this._maxPayloadBytes }
|
|
465
466
|
);
|
|
466
467
|
}
|
|
467
468
|
}
|
|
@@ -539,6 +540,13 @@ ${piece}` : piece;
|
|
|
539
540
|
return arr;
|
|
540
541
|
}
|
|
541
542
|
};
|
|
543
|
+
function normalizeMaxPayloadBytes(value) {
|
|
544
|
+
if (value === void 0 || value === null) return null;
|
|
545
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
546
|
+
throw new TypeError("[rei-standard-amsg-client] maxPayloadBytes must be a positive integer when set");
|
|
547
|
+
}
|
|
548
|
+
return value;
|
|
549
|
+
}
|
|
542
550
|
export {
|
|
543
551
|
MESSAGE_KIND,
|
|
544
552
|
MESSAGE_TYPE,
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rei-standard/amsg-client",
|
|
3
|
-
"version": "2.4.0
|
|
3
|
+
"version": "2.4.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",
|
|
7
|
-
"url": "https://github.com/Tosd0/ReiStandard",
|
|
7
|
+
"url": "git+https://github.com/Tosd0/ReiStandard.git",
|
|
8
8
|
"directory": "packages/rei-standard-amsg/client"
|
|
9
9
|
},
|
|
10
10
|
"license": "MIT",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"node": ">=20"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@rei-standard/amsg-shared": "0.
|
|
36
|
+
"@rei-standard/amsg-shared": "0.2.0"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"tsup": "^8.0.0",
|