@w3lib/solana-batch-call 0.2.0 → 0.2.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 +23 -0
- package/README.md +3 -2
- package/dist/address.d.ts +1 -1
- package/dist/address.d.ts.map +1 -1
- package/dist/address.js +10 -1
- package/dist/address.js.map +1 -1
- package/dist/client.d.ts +2 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +191 -78
- package/dist/client.js.map +1 -1
- package/dist/rpc.d.ts +8 -1
- package/dist/rpc.d.ts.map +1 -1
- package/dist/rpc.js +205 -30
- package/dist/rpc.js.map +1 -1
- package/dist/shortcuts.d.ts.map +1 -1
- package/dist/shortcuts.js +13 -1
- package/dist/shortcuts.js.map +1 -1
- package/package.json +1 -1
- package/src/address.ts +11 -1
- package/src/client.ts +239 -80
- package/src/rpc.ts +222 -30
- package/src/shortcuts.ts +14 -1
package/src/rpc.ts
CHANGED
|
@@ -17,28 +17,67 @@ export class RpcError extends Error {
|
|
|
17
17
|
this.name = 'RpcError'
|
|
18
18
|
this.code = code
|
|
19
19
|
this.data = data
|
|
20
|
+
const kind = classifyRpcError(code, message)
|
|
20
21
|
this.nodeFault =
|
|
21
22
|
code === 403 ||
|
|
22
|
-
code === 429 ||
|
|
23
23
|
code === -32005 ||
|
|
24
|
-
// 节点高度没到 minContextSlot
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
// 节点高度没到 minContextSlot、限频、服务端临时故障(繁忙、过载、暂不可用、超时):换节点可能成功;
|
|
25
|
+
// 临时故障也不应据此关闭批量(降级只会加重负载)
|
|
26
|
+
kind === 'min-slot' ||
|
|
27
|
+
kind === 'rate-limited' ||
|
|
28
|
+
kind === 'transient' ||
|
|
29
|
+
(/forbidden|blocked|personal token|api.?key|too many|not allowed|unauthori[sz]ed|limit exceeded|disabled|not available|not supported/i.test(message) &&
|
|
30
|
+
kind !== 'too-many-accounts')
|
|
28
31
|
}
|
|
29
32
|
}
|
|
30
33
|
|
|
34
|
+
/**
|
|
35
|
+
* JSON-RPC 错误的分类(RpcError.nodeFault、HTTP 5xx 的处理共用,避免两份清单各自维护):
|
|
36
|
+
* - method-limit / too-many-accounts:节点对批量 / 单次账户数的限制,按限制重新分批或缩小
|
|
37
|
+
* - invalid-params:参数错误(-32602),确定性的
|
|
38
|
+
* - min-slot / rate-limited / transient:节点落后、限频、服务端临时故障,换节点
|
|
39
|
+
* - internal:-32603 internal error,含义不明确(可能是临时故障,也可能是节点拒绝批量)
|
|
40
|
+
*/
|
|
41
|
+
type RpcErrorKind = 'method-limit' | 'too-many-accounts' | 'invalid-params' | 'min-slot' | 'rate-limited' | 'transient' | 'internal' | 'other'
|
|
42
|
+
|
|
43
|
+
function classifyRpcError(code: number | undefined, message: string): RpcErrorKind {
|
|
44
|
+
if (METHOD_LIMIT.test(message)) return 'method-limit'
|
|
45
|
+
if (TOO_MANY_ACCOUNTS.test(message)) return 'too-many-accounts'
|
|
46
|
+
if (code === -32602) return 'invalid-params'
|
|
47
|
+
if (code === MIN_CONTEXT_SLOT_NOT_REACHED || /minimum context slot/i.test(message)) return 'min-slot'
|
|
48
|
+
if (code === 429 || RATE_LIMITED.test(message)) return 'rate-limited'
|
|
49
|
+
if (TRANSIENT.test(message)) return 'transient'
|
|
50
|
+
if (code === -32603) return 'internal'
|
|
51
|
+
return 'other'
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** 节点限制单个方法在批量里的数量(如 publicnode:“Maximum number of 'getMultipleAccounts' calls in a batch request is 1”) */
|
|
55
|
+
const METHOD_LIMIT = /maximum number of '([^']+)' calls in a batch request is (\d+)/i
|
|
56
|
+
|
|
57
|
+
/** 限频的报错 */
|
|
58
|
+
const RATE_LIMITED = /rate.?limit|too many requests/i
|
|
59
|
+
|
|
60
|
+
/** 服务端临时故障的报错(如 -32000 server is busy、-32603 + 503 overloaded) */
|
|
61
|
+
const TRANSIENT = /\bbusy\b|overload|temporar(il)?y|unavailable|try again|timed? ?out|timeout/i
|
|
62
|
+
|
|
63
|
+
/** 节点限制单次 getMultipleAccounts 的账户数(如 “Too many accounts requested”):缩小每次的账户数后重试,不是节点故障 */
|
|
64
|
+
const TOO_MANY_ACCOUNTS = /too many accounts/i
|
|
65
|
+
|
|
31
66
|
/** HTTP 层错误(非 2xx、网络错误、超时) */
|
|
32
67
|
export class HttpError extends Error {
|
|
33
68
|
readonly status: number | undefined
|
|
34
69
|
/** 请求超时(HttpRpcOptions.timeout) */
|
|
35
70
|
readonly timeout: boolean
|
|
36
71
|
|
|
37
|
-
|
|
72
|
+
/** 5xx 的响应体是 JSON-RPC 错误(服务端故障,或节点用 5xx 拒绝批量请求) */
|
|
73
|
+
readonly rpcError: boolean
|
|
74
|
+
|
|
75
|
+
constructor(message: string, status?: number, timeout = false, rpcError = false) {
|
|
38
76
|
super(message)
|
|
39
77
|
this.name = 'HttpError'
|
|
40
78
|
this.status = status
|
|
41
79
|
this.timeout = timeout
|
|
80
|
+
this.rpcError = rpcError
|
|
42
81
|
}
|
|
43
82
|
}
|
|
44
83
|
|
|
@@ -118,6 +157,9 @@ let nextId = 1
|
|
|
118
157
|
/** 批量请求被拒绝、可以降级为逐条请求的 HTTP 状态 */
|
|
119
158
|
const BATCH_REJECTED_STATUS = new Set([400, 404, 405, 415, 422])
|
|
120
159
|
|
|
160
|
+
/** 节点不支持批量时,多久后重新尝试批量 */
|
|
161
|
+
const BATCH_RETRY_AFTER = 10 * 60 * 1000
|
|
162
|
+
|
|
121
163
|
function isBatchLimitError(message: string | undefined): boolean {
|
|
122
164
|
return !!message && /batch/i.test(message) && /more than \d+|too (large|many|big)|not (allowed|supported)|exceed/i.test(message)
|
|
123
165
|
}
|
|
@@ -136,7 +178,14 @@ export class HttpRpc implements RpcTransport {
|
|
|
136
178
|
readonly #retries: number
|
|
137
179
|
readonly #fetch: typeof fetch
|
|
138
180
|
#maxBatchSize: number
|
|
139
|
-
|
|
181
|
+
/** 节点对单个方法在一个批量里的数量限制(如 publicnode:getMultipleAccounts 最多 1 个),从报错里学到 */
|
|
182
|
+
readonly #methodLimits = new Map<string, number>()
|
|
183
|
+
/** options.batch:调用方是否允许批量 */
|
|
184
|
+
readonly #batchOption: boolean
|
|
185
|
+
/** 节点明确表示不支持批量时,在这之前逐条请求(之后重新尝试批量) */
|
|
186
|
+
#batchDisabledUntil = 0
|
|
187
|
+
/** 节点限制单次 getMultipleAccounts 的账户数时学到的上限 */
|
|
188
|
+
#maxAccounts: number | undefined
|
|
140
189
|
#queue: Pending[] = []
|
|
141
190
|
#timer: ReturnType<typeof setTimeout> | null = null
|
|
142
191
|
|
|
@@ -146,13 +195,59 @@ export class HttpRpc implements RpcTransport {
|
|
|
146
195
|
this.#timeout = options.timeout ?? 10_000
|
|
147
196
|
this.#headers = { 'content-type': 'application/json', ...options.headers }
|
|
148
197
|
this.#maxBatchSize = Math.max(1, options.maxBatchSize ?? 20)
|
|
149
|
-
this.#
|
|
198
|
+
this.#batchOption = options.batch ?? true
|
|
150
199
|
this.#batchWait = options.batchWait ?? 0
|
|
151
200
|
this.#retries = Math.max(0, options.retries ?? 2)
|
|
152
201
|
this.#fetch = options.fetch ?? ((...args) => fetch(...args))
|
|
153
202
|
}
|
|
154
203
|
|
|
155
204
|
request<T = unknown>(method: string, params: readonly unknown[] = []): Promise<T> {
|
|
205
|
+
if (method === 'getMultipleAccounts' && Array.isArray(params[0])) {
|
|
206
|
+
return this.#getMultipleAccounts(params) as Promise<T>
|
|
207
|
+
}
|
|
208
|
+
return this.#enqueue<T>(method, params)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* getMultipleAccounts:节点限制单次账户数时(“Too many accounts requested”),减半后分多次请求、合并结果,
|
|
213
|
+
* 并记下上限,之后直接按上限拆分。合并后 context.slot 取最小值
|
|
214
|
+
*/
|
|
215
|
+
async #getMultipleAccounts(params: readonly unknown[]): Promise<unknown> {
|
|
216
|
+
const [addresses, ...rest] = params as [unknown[], ...unknown[]]
|
|
217
|
+
const max = this.#maxAccounts
|
|
218
|
+
if (max !== undefined && addresses.length > max) {
|
|
219
|
+
const parts: unknown[][] = []
|
|
220
|
+
for (let i = 0; i < addresses.length; i += max) {
|
|
221
|
+
parts.push(addresses.slice(i, i + max))
|
|
222
|
+
}
|
|
223
|
+
const results = (await Promise.all(parts.map((part) => this.#getMultipleAccounts([part, ...rest])))) as Array<{
|
|
224
|
+
context?: { slot?: number }
|
|
225
|
+
value?: unknown[]
|
|
226
|
+
}>
|
|
227
|
+
const slots = results.map((r) => r?.context?.slot).filter((slot): slot is number => typeof slot === 'number')
|
|
228
|
+
return {
|
|
229
|
+
...results[0],
|
|
230
|
+
context: { ...results[0]?.context, ...(slots.length ? { slot: Math.min(...slots) } : {}) },
|
|
231
|
+
value: results.flatMap((r) => r?.value ?? []),
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
try {
|
|
235
|
+
return await this.#enqueue('getMultipleAccounts', params)
|
|
236
|
+
} catch (err) {
|
|
237
|
+
if (err instanceof RpcError && TOO_MANY_ACCOUNTS.test(err.message) && addresses.length > 1) {
|
|
238
|
+
this.#maxAccounts = Math.max(1, Math.min(this.#maxAccounts ?? Number.POSITIVE_INFINITY, Math.floor(addresses.length / 2)))
|
|
239
|
+
return this.#getMultipleAccounts(params)
|
|
240
|
+
}
|
|
241
|
+
throw err
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** 是否合并成批量请求:调用方允许,且不在 “节点不支持批量” 的冷却期内 */
|
|
246
|
+
get #batch(): boolean {
|
|
247
|
+
return this.#batchOption && Date.now() >= this.#batchDisabledUntil
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
#enqueue<T>(method: string, params: readonly unknown[]): Promise<T> {
|
|
156
251
|
return new Promise<T>((resolve, reject) => {
|
|
157
252
|
this.#queue.push({ method, params, resolve: resolve as (value: unknown) => void, reject })
|
|
158
253
|
if (!this.#batch || this.#queue.length >= this.#maxBatchSize) {
|
|
@@ -170,12 +265,51 @@ export class HttpRpc implements RpcTransport {
|
|
|
170
265
|
}
|
|
171
266
|
const queue = this.#queue
|
|
172
267
|
this.#queue = []
|
|
173
|
-
for (
|
|
174
|
-
void this.#dispatch(
|
|
268
|
+
for (const chunk of this.#split(queue)) {
|
|
269
|
+
void this.#dispatch(chunk)
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** 按 maxBatchSize 和各方法的数量限制分批(保持顺序;不受限的方法照常合并) */
|
|
274
|
+
#split(items: readonly Pending[]): Pending[][] {
|
|
275
|
+
const chunks: Pending[][] = []
|
|
276
|
+
let current: Pending[] = []
|
|
277
|
+
let counts = new Map<string, number>()
|
|
278
|
+
for (const item of items) {
|
|
279
|
+
const limit = this.#methodLimits.get(item.method)
|
|
280
|
+
const count = counts.get(item.method) ?? 0
|
|
281
|
+
if (current.length >= this.#maxBatchSize || (limit !== undefined && count >= limit)) {
|
|
282
|
+
chunks.push(current)
|
|
283
|
+
current = []
|
|
284
|
+
counts = new Map()
|
|
285
|
+
}
|
|
286
|
+
current.push(item)
|
|
287
|
+
counts.set(item.method, (counts.get(item.method) ?? 0) + 1)
|
|
175
288
|
}
|
|
289
|
+
if (current.length) {
|
|
290
|
+
chunks.push(current)
|
|
291
|
+
}
|
|
292
|
+
return chunks
|
|
176
293
|
}
|
|
177
294
|
|
|
178
|
-
|
|
295
|
+
/**
|
|
296
|
+
* 报错是 “某方法在批量里的数量超限” 时(如 “Maximum number of 'getMultipleAccounts' calls in a batch request is 1”),
|
|
297
|
+
* 记下限制并按限制重新分批,返回 true;限制解析不出或这批本来就没超限(避免无限重发)时返回 false
|
|
298
|
+
*/
|
|
299
|
+
async #retryWithMethodLimit(items: Pending[], message: string | undefined): Promise<boolean> {
|
|
300
|
+
const match = METHOD_LIMIT.exec(message ?? '')
|
|
301
|
+
const method = match?.[1]
|
|
302
|
+
const limit = Number(match?.[2])
|
|
303
|
+
if (!method || !Number.isFinite(limit) || limit < 1 || items.filter((item) => item.method === method).length <= limit) {
|
|
304
|
+
return false
|
|
305
|
+
}
|
|
306
|
+
this.#methodLimits.set(method, limit)
|
|
307
|
+
await Promise.all(this.#split(items).map((chunk) => this.#dispatch(chunk)))
|
|
308
|
+
return true
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** retriedServerError:这一批已经因 5xx + JSON-RPC 错误重试过一次 */
|
|
312
|
+
async #dispatch(items: Pending[], retriedServerError = false): Promise<void> {
|
|
179
313
|
if (items.length === 1 || !this.#batch) {
|
|
180
314
|
await Promise.all(items.map((item) => this.#single(item)))
|
|
181
315
|
return
|
|
@@ -198,23 +332,53 @@ export class HttpRpc implements RpcTransport {
|
|
|
198
332
|
await Promise.all(items.map((item) => this.#single(item)))
|
|
199
333
|
return
|
|
200
334
|
}
|
|
335
|
+
// 5xx + -32603(含义不明确:临时故障,或节点用 5xx 拒绝批量):先原样重试一次批量(偶发故障这次就成功了,不降级);
|
|
336
|
+
// 仍然失败时只用第一条单独试探——成功说明是批量本身被拒,暂停批量(10 分钟后重试)并逐条发送其余的;
|
|
337
|
+
// 失败则是临时故障,其余的直接报错(换节点),不给出问题的节点加量
|
|
338
|
+
if (err instanceof HttpError && err.rpcError && items.length > 1) {
|
|
339
|
+
if (!retriedServerError) {
|
|
340
|
+
await this.#dispatch(items, true)
|
|
341
|
+
return
|
|
342
|
+
}
|
|
343
|
+
const [first, ...rest] = items as [Pending, ...Pending[]]
|
|
344
|
+
if (await this.#single(first)) {
|
|
345
|
+
this.#disableBatch()
|
|
346
|
+
await Promise.all(rest.map((item) => this.#single(item)))
|
|
347
|
+
} else {
|
|
348
|
+
rest.forEach((item) => item.reject(err))
|
|
349
|
+
}
|
|
350
|
+
return
|
|
351
|
+
}
|
|
201
352
|
items.forEach((item) => item.reject(err))
|
|
202
353
|
return
|
|
203
354
|
}
|
|
204
355
|
if (!Array.isArray(body)) {
|
|
205
356
|
const error = (body as RpcResponse)?.error
|
|
357
|
+
// 单个方法在批量里的数量超限(整批被拒):按限制重新分批,不当成节点问题
|
|
358
|
+
if (await this.#retryWithMethodLimit(items, error?.message)) {
|
|
359
|
+
return
|
|
360
|
+
}
|
|
206
361
|
const rpcError = new RpcError(error?.message ?? 'Invalid JSON-RPC batch response', error?.code, error?.data)
|
|
207
362
|
// 限频 / 鉴权等节点问题:原样报错,不降级(降级成逐条请求只会让限频更严重)
|
|
208
363
|
if (rpcError.nodeFault) {
|
|
209
364
|
items.forEach((item) => item.reject(rpcError))
|
|
210
365
|
return
|
|
211
366
|
}
|
|
212
|
-
//
|
|
213
|
-
|
|
367
|
+
// 整批被拒(不是账户数超限):节点多半不支持批量(各家错误码不同:-32600 / -32601 / -32700 …),
|
|
368
|
+
// 一段时间内改为逐条请求(10 分钟后重新尝试批量,偶发的错误不会永久关闭);不关的话每批都要先失败一次再逐条重发。
|
|
369
|
+
// 账户数超限只把这一批逐条重发(由 HTTP 层按节点缩小单次读取数量)
|
|
370
|
+
if (!TOO_MANY_ACCOUNTS.test(error?.message ?? '')) {
|
|
371
|
+
this.#disableBatch()
|
|
372
|
+
}
|
|
214
373
|
await Promise.all(items.map((item) => this.#single(item)))
|
|
215
374
|
return
|
|
216
375
|
}
|
|
217
376
|
const byId = new Map((body as RpcResponse[]).map((res) => [res.id, res]))
|
|
377
|
+
// 有的节点把 “方法数量超限” 放在批量结果的某一项里
|
|
378
|
+
const methodLimited = (body as RpcResponse[]).find((res) => METHOD_LIMIT.test(res.error?.message ?? ''))
|
|
379
|
+
if (methodLimited && (await this.#retryWithMethodLimit(items, methodLimited.error?.message))) {
|
|
380
|
+
return
|
|
381
|
+
}
|
|
218
382
|
const limited = (body as RpcResponse[]).find((res) => isBatchLimitError(res.error?.message))
|
|
219
383
|
if (limited) {
|
|
220
384
|
// 如 “Batch of more than 3 requests are not allowed”:按限制缩小批量后重发;
|
|
@@ -229,11 +393,13 @@ export class HttpRpc implements RpcTransport {
|
|
|
229
393
|
}
|
|
230
394
|
return
|
|
231
395
|
}
|
|
232
|
-
// 批量里个别调用被限频(JSON-RPC 429
|
|
396
|
+
// 批量里个别调用被限频(JSON-RPC 429):只把这些调用退避后单独重试一次(不再叠加 #post 的重试);
|
|
397
|
+
// 整批都是 429 时 #post 已经退避重试过整批,不再逐条重试——否则请求数会成倍放大
|
|
398
|
+
const allLimited = isRateLimited(body)
|
|
233
399
|
const limitedItems: Pending[] = []
|
|
234
400
|
items.forEach((item, i) => {
|
|
235
401
|
const response = byId.get(ids[i])
|
|
236
|
-
if (response?.error?.code === 429 && this.#retries > 0) {
|
|
402
|
+
if (response?.error?.code === 429 && this.#retries > 0 && !allLimited) {
|
|
237
403
|
limitedItems.push(item)
|
|
238
404
|
} else {
|
|
239
405
|
settle(item, response)
|
|
@@ -241,34 +407,33 @@ export class HttpRpc implements RpcTransport {
|
|
|
241
407
|
})
|
|
242
408
|
if (limitedItems.length) {
|
|
243
409
|
await new Promise((r) => setTimeout(r, 500))
|
|
244
|
-
await Promise.all(limitedItems.map((item) => this.#single(item)))
|
|
410
|
+
await Promise.all(limitedItems.map((item) => this.#single(item, 0)))
|
|
245
411
|
}
|
|
246
412
|
}
|
|
247
413
|
|
|
248
414
|
/** 按当前的 maxBatchSize 重新分批发送(每批都比原来小,不会无限循环) */
|
|
249
415
|
async #redispatch(items: Pending[]): Promise<void> {
|
|
250
|
-
|
|
251
|
-
const chunks: Pending[][] = []
|
|
252
|
-
for (let i = 0; i < items.length; i += size) {
|
|
253
|
-
chunks.push(items.slice(i, i + size))
|
|
254
|
-
}
|
|
255
|
-
await Promise.all(chunks.map((chunk) => this.#dispatch(chunk)))
|
|
416
|
+
await Promise.all(this.#split(items).map((chunk) => this.#dispatch(chunk)))
|
|
256
417
|
}
|
|
257
418
|
|
|
258
|
-
|
|
419
|
+
/** 逐条发送一个调用;返回是否拿到了结果(没有出错) */
|
|
420
|
+
async #single(item: Pending, retries = this.#retries): Promise<boolean> {
|
|
259
421
|
try {
|
|
260
|
-
const body = (await this.#post({ jsonrpc: '2.0', id: nextId++, method: item.method, params: item.params })) as RpcResponse
|
|
422
|
+
const body = (await this.#post({ jsonrpc: '2.0', id: nextId++, method: item.method, params: item.params }, retries)) as RpcResponse
|
|
261
423
|
settle(item, body)
|
|
424
|
+
return !body?.error
|
|
262
425
|
} catch (err) {
|
|
263
426
|
item.reject(err)
|
|
427
|
+
return false
|
|
264
428
|
}
|
|
265
429
|
}
|
|
266
430
|
|
|
431
|
+
/** 节点明确表示不支持批量:10 分钟内逐条请求,之后重新尝试批量(节点可能只是临时出错或已经升级) */
|
|
267
432
|
#disableBatch(): void {
|
|
268
|
-
this.#
|
|
433
|
+
this.#batchDisabledUntil = Date.now() + BATCH_RETRY_AFTER
|
|
269
434
|
}
|
|
270
435
|
|
|
271
|
-
async #post(payload: unknown): Promise<unknown> {
|
|
436
|
+
async #post(payload: unknown, retries = this.#retries): Promise<unknown> {
|
|
272
437
|
for (let attempt = 0; ; attempt++) {
|
|
273
438
|
let response: Response
|
|
274
439
|
try {
|
|
@@ -283,11 +448,18 @@ export class HttpRpc implements RpcTransport {
|
|
|
283
448
|
const timeout = (err as { name?: unknown } | null)?.name === 'TimeoutError'
|
|
284
449
|
throw new HttpError(`Request to ${this.#safeUrl} failed: ${(err as Error)?.message ?? err}`, undefined, timeout)
|
|
285
450
|
}
|
|
286
|
-
if (response.status === 429 && attempt <
|
|
451
|
+
if (response.status === 429 && attempt < retries) {
|
|
287
452
|
await new Promise((r) => setTimeout(r, 500 * 2 ** attempt))
|
|
288
453
|
continue
|
|
289
454
|
}
|
|
290
|
-
|
|
455
|
+
let text: string
|
|
456
|
+
try {
|
|
457
|
+
text = await response.text()
|
|
458
|
+
} catch (err) {
|
|
459
|
+
// 读响应体时超时或连接中断:同样包装成 HttpError(原生 DOMException / TypeError 不会被当成可恢复的节点问题)
|
|
460
|
+
const timeout = (err as { name?: unknown } | null)?.name === 'TimeoutError'
|
|
461
|
+
throw new HttpError(`Reading response from ${this.#safeUrl} failed: ${(err as Error)?.message ?? err}`, response.status, timeout)
|
|
462
|
+
}
|
|
291
463
|
let body: unknown
|
|
292
464
|
try {
|
|
293
465
|
body = parseJson(text)
|
|
@@ -298,8 +470,18 @@ export class HttpRpc implements RpcTransport {
|
|
|
298
470
|
if (!response.ok && !Array.isArray(body) && !(body as RpcResponse)?.error) {
|
|
299
471
|
throw new HttpError(`HTTP ${response.status} from ${this.#safeUrl}`, response.status)
|
|
300
472
|
}
|
|
473
|
+
// 5xx + 单个 JSON-RPC 错误:
|
|
474
|
+
// - 繁忙、过载等明确的临时故障:按 HTTP 错误处理,直接换节点(不重试、不逐条,不给过载的节点加量)
|
|
475
|
+
// - -32603 internal error:含义不明确,标记 rpcError,由 #dispatch 判断是临时故障还是节点用 5xx 拒绝批量
|
|
476
|
+
// 其他错误(方法数量超限、账户数超限、参数错误、minContextSlot、限频等)照常交给上层按类型处理
|
|
477
|
+
const bodyError = response.status >= 500 && !Array.isArray(body) ? (body as RpcResponse)?.error : undefined
|
|
478
|
+
const kind = bodyError ? classifyRpcError(bodyError.code, bodyError.message ?? '') : undefined
|
|
479
|
+
if (kind === 'transient' || kind === 'internal') {
|
|
480
|
+
const message = bodyError?.message
|
|
481
|
+
throw new HttpError(`HTTP ${response.status} from ${this.#safeUrl}${message ? `: ${message}` : ''}`, response.status, false, kind === 'internal')
|
|
482
|
+
}
|
|
301
483
|
// 有的节点限频时返回 HTTP 200 + JSON-RPC 错误码 429:同样退避重试
|
|
302
|
-
if (attempt <
|
|
484
|
+
if (attempt < retries && isRateLimited(body)) {
|
|
303
485
|
await new Promise((r) => setTimeout(r, 500 * 2 ** attempt))
|
|
304
486
|
continue
|
|
305
487
|
}
|
|
@@ -326,7 +508,17 @@ function parseJson(text: string): unknown {
|
|
|
326
508
|
typeof value === 'number' && !Number.isSafeInteger(value) && Number.isInteger(value) && context?.source ? context.source : value,
|
|
327
509
|
)
|
|
328
510
|
}
|
|
329
|
-
return JSON.parse(text
|
|
511
|
+
return JSON.parse(quoteBigIntegers(text))
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* 把 JSON 文本里数值位置上 16 位以上的整数改成字符串(不支持 source text access 的环境用)。
|
|
516
|
+
* 跳过字符串字面量:错误信息等字符串里的长数字不会被改动
|
|
517
|
+
*/
|
|
518
|
+
export function quoteBigIntegers(text: string): string {
|
|
519
|
+
return text.replace(/"(?:[^"\\]|\\.)*"|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/g, (token) =>
|
|
520
|
+
token[0] !== '"' && /^-?\d{16,}$/.test(token) ? `"${token}"` : token,
|
|
521
|
+
)
|
|
330
522
|
}
|
|
331
523
|
|
|
332
524
|
function safeOrigin(url: string): string {
|
package/src/shortcuts.ts
CHANGED
|
@@ -38,11 +38,24 @@ function setBounded<K, V>(map: Map<K, V>, key: K, value: V): void {
|
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
// 配置里的函数(如自定义 fetch)按对象编号参与缓存键:同一个函数对象复用同一个客户端(WeakMap 不阻止回收)
|
|
42
|
+
const functionIds = new WeakMap<object, number>()
|
|
43
|
+
let nextFunctionId = 0
|
|
44
|
+
|
|
45
|
+
/** 配置的缓存键:对象的键按字母排序(写法顺序不同视为同一份配置),函数按对象编号;无法序列化时返回 null(不缓存) */
|
|
41
46
|
function configKey(config: ClientConfig): string | null {
|
|
42
47
|
try {
|
|
43
48
|
return JSON.stringify(config, (_key, value: unknown) => {
|
|
44
49
|
if (typeof value === 'function') {
|
|
45
|
-
|
|
50
|
+
let id = functionIds.get(value)
|
|
51
|
+
if (id === undefined) {
|
|
52
|
+
id = ++nextFunctionId
|
|
53
|
+
functionIds.set(value, id)
|
|
54
|
+
}
|
|
55
|
+
return `function#${id}`
|
|
56
|
+
}
|
|
57
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
58
|
+
return Object.fromEntries(Object.entries(value).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)))
|
|
46
59
|
}
|
|
47
60
|
return value
|
|
48
61
|
})
|