@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 CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  `@rei-standard/amsg-client` 是 ReiStandard 主动消息标准的浏览器端 SDK 包,负责加密请求、解密响应和 Push 订阅。
4
4
 
5
- ## v2.4.0-next.0 — SSE consumer
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)。`sendInstant()` 字节级不变;老调用方升级零成本。
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
- **两个常见 footgun**:
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` 直接打到主线程,省掉 push service → SW → IDB → window 的绕路,前台延迟从约 1–3s 降到次百毫秒。前台场景应该改用 `consumeInstantStream()`。
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`:SSE 写失败或客户端断开时 amsg-instant 用它做 best-effort fallback push(同一 `messageId`,客户端按 ID 幂等去重即可)。
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` payload 体积(2.2.4+ / 2.3.0+)
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 字节数 > 3072 | 抛出 `Error.code === 'PAYLOAD_TOO_LARGE_LOCAL'`,错误对象带 `.details = { method, actualBytes, limitBytes }` | 远端网关 / Web Push 4KB 硬上限的本地兜底。 |
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 包都有导出)。`PAYLOAD_TOO_LARGE_LOCAL` 仍然是真正的"整包过大"信号,照常用 try/catch 捕获:
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: 8732, limitBytes: 3072 }
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. The only throw left is
123
- * `PAYLOAD_TOO_LARGE_LOCAL` JSON-serialized payload exceeds 3 KB.
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. The only throw left is
169
- * `PAYLOAD_TOO_LARGE_LOCAL` JSON-serialized payload exceeds 3 KB.
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. The only throw left is `PAYLOAD_TOO_LARGE_LOCAL`
351
- * JSON-serialized updates exceed 3 KB.
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
- * Reject outgoing payloads larger than 3 KB pre-encryption. Spares the
473
- * remote a guaranteed 413 / Web Push 4 KB-limit failure and gives the
474
- * caller a precise local error pointing at the size cap.
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 > PAYLOAD_LOCAL_MAX_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 ${PAYLOAD_LOCAL_MAX_BYTES} \u5B57\u8282`,
486
- { method: methodName, actualBytes: bytes, limitBytes: PAYLOAD_LOCAL_MAX_BYTES }
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. The only throw left is
187
- * `PAYLOAD_TOO_LARGE_LOCAL` JSON-serialized payload exceeds 3 KB.
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. The only throw left is
236
- * `PAYLOAD_TOO_LARGE_LOCAL` JSON-serialized payload exceeds 3 KB.
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. The only throw left is `PAYLOAD_TOO_LARGE_LOCAL`
434
- * JSON-serialized updates exceed 3 KB.
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
- * Reject outgoing payloads larger than 3 KB pre-encryption. Spares the
573
- * remote a guaranteed 413 / Web Push 4 KB-limit failure and gives the
574
- * caller a precise local error pointing at the size cap.
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 > PAYLOAD_LOCAL_MAX_BYTES) {
579
+ if (bytes > this._maxPayloadBytes) {
583
580
  throw makeLocalError(
584
581
  'PAYLOAD_TOO_LARGE_LOCAL',
585
- `${methodName} payload 体积 ${bytes} 字节超过本地上限 ${PAYLOAD_LOCAL_MAX_BYTES} 字节`,
586
- { method: methodName, actualBytes: bytes, limitBytes: PAYLOAD_LOCAL_MAX_BYTES }
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. The only throw left is
187
- * `PAYLOAD_TOO_LARGE_LOCAL` JSON-serialized payload exceeds 3 KB.
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. The only throw left is
236
- * `PAYLOAD_TOO_LARGE_LOCAL` JSON-serialized payload exceeds 3 KB.
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. The only throw left is `PAYLOAD_TOO_LARGE_LOCAL`
434
- * JSON-serialized updates exceed 3 KB.
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
- * Reject outgoing payloads larger than 3 KB pre-encryption. Spares the
573
- * remote a guaranteed 413 / Web Push 4 KB-limit failure and gives the
574
- * caller a precise local error pointing at the size cap.
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 > PAYLOAD_LOCAL_MAX_BYTES) {
579
+ if (bytes > this._maxPayloadBytes) {
583
580
  throw makeLocalError(
584
581
  'PAYLOAD_TOO_LARGE_LOCAL',
585
- `${methodName} payload 体积 ${bytes} 字节超过本地上限 ${PAYLOAD_LOCAL_MAX_BYTES} 字节`,
586
- { method: methodName, actualBytes: bytes, limitBytes: PAYLOAD_LOCAL_MAX_BYTES }
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. The only throw left is
101
- * `PAYLOAD_TOO_LARGE_LOCAL` JSON-serialized payload exceeds 3 KB.
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. The only throw left is
147
- * `PAYLOAD_TOO_LARGE_LOCAL` JSON-serialized payload exceeds 3 KB.
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. The only throw left is `PAYLOAD_TOO_LARGE_LOCAL`
329
- * JSON-serialized updates exceed 3 KB.
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
- * Reject outgoing payloads larger than 3 KB pre-encryption. Spares the
451
- * remote a guaranteed 413 / Web Push 4 KB-limit failure and gives the
452
- * caller a precise local error pointing at the size cap.
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 > PAYLOAD_LOCAL_MAX_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 ${PAYLOAD_LOCAL_MAX_BYTES} \u5B57\u8282`,
464
- { method: methodName, actualBytes: bytes, limitBytes: PAYLOAD_LOCAL_MAX_BYTES }
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-next.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.1.0"
36
+ "@rei-standard/amsg-shared": "0.2.0"
37
37
  },
38
38
  "devDependencies": {
39
39
  "tsup": "^8.0.0",