@agentunion/fastaun 0.5.36 → 0.5.37

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/dist/client.js CHANGED
@@ -231,9 +231,32 @@ function clampReconnectDelayMs(value, fallback, upper = RECONNECT_MAX_BASE_DELAY
231
231
  return Math.min(Math.max(ms, RECONNECT_MIN_BASE_DELAY_MS), upper);
232
232
  }
233
233
  function reconnectSleepDelayMs(baseDelay, maxBaseDelay) {
234
- // 抖动只随当前退避基数增长,并限制在 maxBaseDelay 内。
235
- const jitterLimit = Math.min(Math.max(baseDelay, 0), Math.max(maxBaseDelay - baseDelay, 0));
236
- return baseDelay + Math.random() * jitterLimit;
234
+ return Math.max(1, Math.random() * Math.min(baseDelay, maxBaseDelay));
235
+ }
236
+ const reconnectSlots = new Map();
237
+ async function acquireReconnectSlot(aid, wait, pause) {
238
+ const issuer = aid.slice(aid.indexOf('.') + 1).toLowerCase();
239
+ let slots = reconnectSlots.get(issuer);
240
+ if (!slots) {
241
+ slots = { active: new Set(), next: 0 };
242
+ reconnectSlots.set(issuer, slots);
243
+ }
244
+ while (slots.active.size >= 2)
245
+ await wait(Promise.race(slots.active));
246
+ let done;
247
+ const ticket = new Promise(resolve => { done = resolve; });
248
+ slots.active.add(ticket);
249
+ const release = () => { slots.active.delete(ticket); done(); };
250
+ const delay = Math.min(500, Math.max(0, slots.next - Date.now()));
251
+ slots.next = Date.now() + delay + 250 + Math.random() * 250;
252
+ try {
253
+ await pause(delay);
254
+ return release;
255
+ }
256
+ catch (error) {
257
+ release();
258
+ throw error;
259
+ }
237
260
  }
238
261
  /** peer 证书缓存 TTL(1 小时) */
239
262
  const PEER_CERT_CACHE_TTL = 3600;
@@ -743,7 +766,7 @@ export class AUNClient {
743
766
  if (current())
744
767
  this._credentialRenewalTimer = setTimeout(() => { void scan(); }, 60_000);
745
768
  };
746
- void scan();
769
+ this._credentialRenewalTimer = setTimeout(() => { void scan(); }, 30_000 + Math.random() * 30_000);
747
770
  }
748
771
  _stopCredentialRenewal() {
749
772
  this._credentialRenewalAbort?.abort();
@@ -2475,28 +2498,43 @@ export class AUNClient {
2475
2498
  tokens.saveMetadata(orgAid, { org_identity_creation: null });
2476
2499
  return checkpoint.result;
2477
2500
  }
2478
- _schedulePostConnectRecovery() {
2501
+ _schedulePostConnectRecovery(backgroundSync = this._sessionOptions.background_sync !== false) {
2479
2502
  this._postConnectRecoveryAbort?.abort();
2480
2503
  if (this._postConnectRecoveryTimer !== null)
2481
2504
  clearTimeout(this._postConnectRecoveryTimer);
2482
2505
  this._postConnectRecoveryTimer = null;
2483
2506
  const store = this._aidStore;
2484
- if (this._sessionOptions.background_sync === false)
2485
- return;
2486
2507
  const abort = new AbortController();
2487
2508
  this._postConnectRecoveryAbort = abort;
2488
2509
  const aid = this._aid, version = this._serviceEventIdentityVersion, connection = this._entityOwnershipConnection;
2489
2510
  const current = () => !abort.signal.aborted && aid === this._aid && version === this._serviceEventIdentityVersion && connection === this._entityOwnershipConnection && ['ready', 'connected'].includes(this._state) && !this._closing && !this._disconnecting;
2511
+ const tasks = [];
2512
+ if (!this._ownershipOperation)
2513
+ tasks.push(() => this._checkIdentityAdmissionAfterConnect());
2514
+ tasks.push(() => this._reconcileReadStates());
2515
+ if (backgroundSync) {
2516
+ tasks.push(() => this._recoverGroupInvites());
2517
+ if (this._v2Session && this._sessionOptions.connection_kind !== 'short')
2518
+ tasks.push(() => this._v2AutoConfirmPendingProposals());
2519
+ tasks.push(() => this._delivery.restoreMessageClassPulls(), () => this._fillP2pGap());
2520
+ if (store && !this._ownershipOperation)
2521
+ tasks.push(() => this._resumeOrgIdentityAfterConnect(store, current));
2522
+ }
2523
+ const run = async () => {
2524
+ for (let index = 0; index < tasks.length; index++) {
2525
+ if (!current())
2526
+ return;
2527
+ const pending = tasks[index]();
2528
+ this._safeAsync(pending);
2529
+ await pending.catch(() => { });
2530
+ if (index + 1 < tasks.length)
2531
+ await this._sleep(1000 + Math.random() * 4000, abort.signal);
2532
+ }
2533
+ };
2490
2534
  this._postConnectRecoveryTimer = setTimeout(() => {
2491
2535
  this._postConnectRecoveryTimer = null;
2492
- if (!current())
2493
- return;
2494
- this._safeAsync(this._delivery.restoreMessageClassPulls());
2495
- void this._fillP2pGap().catch((exc) => {
2496
- this._clientLog.warn(`schedule post-connect P2P gap fill failed: ${formatCaughtError(exc)}`);
2497
- });
2498
- if (store && !this._ownershipOperation)
2499
- this._safeAsync(this._resumeOrgIdentityAfterConnect(store, current));
2536
+ if (current())
2537
+ this._safeAsync(run());
2500
2538
  }, 30_000 + Math.random() * 30_000);
2501
2539
  this._postConnectRecoveryTimer.unref?.();
2502
2540
  }
@@ -4990,18 +5028,11 @@ export class AUNClient {
4990
5028
  }
4991
5029
  this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
4992
5030
  this._startBackgroundTasks();
4993
- this._safeAsync(this._reconcileReadStates());
4994
5031
  const connectionKind = String(params.connection_kind ?? 'long');
4995
5032
  const isShortConnection = connectionKind === 'short';
4996
5033
  const hasExplicitBackgroundSync = Object.prototype.hasOwnProperty.call(params, 'background_sync');
4997
5034
  const backgroundSyncEnabled = this._sessionOptions.background_sync !== false
4998
5035
  && (!isShortConnection || hasExplicitBackgroundSync);
4999
- if (backgroundSyncEnabled) {
5000
- // 连接建立/恢复后先从事实列表恢复定向邀请提示;失败不影响连接本身。
5001
- this._safeAsync(this._recoverGroupInvites().catch((exc) => {
5002
- this._clientLog.debug(`group invite recovery failed: ${formatCaughtError(exc)}`);
5003
- }));
5004
- }
5005
5036
  if (!isShortConnection) {
5006
5037
  await this._v2E2EE.onConnected({ backgroundSync: backgroundSyncEnabled });
5007
5038
  this._assertReconnectOwner(reconnectOwner);
@@ -5016,13 +5047,7 @@ export class AUNClient {
5016
5047
  // 群消息按惰性触发,不在此处主动 pull
5017
5048
  this._entityOwnershipConnection++;
5018
5049
  this._entityOwnershipChecks.clear();
5019
- if (!this._ownershipOperation)
5020
- this._safeAsync(this._checkIdentityAdmissionAfterConnect());
5021
- if (backgroundSyncEnabled)
5022
- this._schedulePostConnectRecovery();
5023
- if (!isShortConnection && backgroundSyncEnabled && this._v2Session) {
5024
- this._safeAsync(this._v2AutoConfirmPendingProposals());
5025
- }
5050
+ this._schedulePostConnectRecovery(backgroundSyncEnabled);
5026
5051
  this._activateUdpStandby();
5027
5052
  this._clientLog.debug(`_connectOnce exit: elapsed=${Date.now() - tStart}ms gateway=${gatewayUrl}, aid=${this._aid ?? ''}`);
5028
5053
  }
@@ -6463,8 +6488,17 @@ export class AUNClient {
6463
6488
  reconnect: async () => {
6464
6489
  if (this._closing || this._disconnecting)
6465
6490
  throw new ConnectionError('client closed');
6466
- this._state = 'standby';
6467
- await this._lifecycle.connect(this._udpStandbyConnectOptions);
6491
+ await this._sleep(reconnectSleepDelayMs(1000, RECONNECT_MAX_BASE_DELAY_MS));
6492
+ const release = await acquireReconnectSlot(this._aid ?? '', task => task, ms => this._sleep(ms));
6493
+ try {
6494
+ if (this._closing || this._disconnecting || this._udpStandby?.state === 'offline')
6495
+ throw new ConnectionError('client closed');
6496
+ this._state = 'standby';
6497
+ await this._lifecycle.connect(this._udpStandbyConnectOptions);
6498
+ }
6499
+ finally {
6500
+ release();
6501
+ }
6468
6502
  },
6469
6503
  });
6470
6504
  this._udpStandby.activate(this._udpStandbyPolicy);
@@ -6592,9 +6626,10 @@ export class AUNClient {
6592
6626
  next_retry_at: this._nextRetryAt,
6593
6627
  }))
6594
6628
  return;
6629
+ let releaseReconnectSlot;
6595
6630
  try {
6596
- // 抖动随当前 base 增长且不超过 max_base,避免首次重连被放大到几十秒。
6597
6631
  await this._sleep(Math.max(0, this._nextRetryAt - Date.now()), reconnectAbort?.signal);
6632
+ releaseReconnectSlot = await acquireReconnectSlot(this._aid ?? '', task => this._lifecycle.waitForAttempt(task, undefined, reconnectAbort ?? undefined), ms => this._sleep(ms, reconnectAbort?.signal));
6598
6633
  if (!this._ownsReconnect(reconnectAbort))
6599
6634
  return;
6600
6635
  this._state = 'reconnecting';
@@ -6603,17 +6638,6 @@ export class AUNClient {
6603
6638
  attempt,
6604
6639
  }))
6605
6640
  return;
6606
- // /health 仅作旁路诊断,不能阻止实际 WebSocket 重连。
6607
- if (this._gatewayUrl) {
6608
- void this._discovery.checkHealth(this._gatewayUrl, 5_000)
6609
- .then((healthy) => {
6610
- if (!healthy)
6611
- this._clientLog.warn(`reconnect health advisory failed: gateway=${this._gatewayUrl}`);
6612
- })
6613
- .catch((exc) => {
6614
- this._clientLog.debug(`reconnect health advisory error: ${formatCaughtError(exc)}`);
6615
- });
6616
- }
6617
6641
  await this._transport.close();
6618
6642
  if (!this._ownsReconnect(reconnectAbort))
6619
6643
  return;
@@ -6650,7 +6674,7 @@ export class AUNClient {
6650
6674
  this._sessionParams.access_token = '';
6651
6675
  }
6652
6676
  }
6653
- const candidates = await this._lifecycle.waitForAttempt(this._resolveGatewayCandidatesForAid(this._aid, true, reconnectAbort ?? undefined), undefined, reconnectAbort ?? undefined);
6677
+ const candidates = await this._lifecycle.waitForAttempt(this._resolveGatewayCandidatesForAid(this._aid, this._migrationPending || this._migrationRevision.size > 0 || attempt % 3 === 2, reconnectAbort ?? undefined), undefined, reconnectAbort ?? undefined);
6654
6678
  if (!this._ownsReconnect(reconnectAbort))
6655
6679
  return;
6656
6680
  this._sessionParams.gateways = [...candidates];
@@ -6710,6 +6734,9 @@ export class AUNClient {
6710
6734
  }
6711
6735
  delay = Math.min(delay * 2, maxBaseDelay);
6712
6736
  }
6737
+ finally {
6738
+ releaseReconnectSlot?.();
6739
+ }
6713
6740
  }
6714
6741
  }
6715
6742
  /** 每轮重连从当前地址之后开始轮转,避免永远黏在失效的首个 Gateway。 */