@formo/analytics 1.38.0 → 1.38.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.
Files changed (34) hide show
  1. package/dist/cjs/src/FormoAnalytics.d.ts +15 -4
  2. package/dist/cjs/src/FormoAnalytics.js +37 -6
  3. package/dist/cjs/src/FormoAnalyticsProvider.d.ts +8 -0
  4. package/dist/cjs/src/FormoAnalyticsProvider.js +41 -32
  5. package/dist/cjs/src/evm/EvmEventTracker.d.ts +100 -9
  6. package/dist/cjs/src/evm/EvmEventTracker.js +357 -19
  7. package/dist/cjs/src/evm/EvmProviderRegistry.d.ts +34 -2
  8. package/dist/cjs/src/evm/EvmProviderRegistry.js +61 -4
  9. package/dist/cjs/src/evm/EvmRequestTracker.d.ts +21 -4
  10. package/dist/cjs/src/evm/EvmRequestTracker.js +54 -28
  11. package/dist/cjs/src/version.d.ts +1 -1
  12. package/dist/cjs/src/version.js +1 -1
  13. package/dist/cjs/src/wagmi/WagmiEventHandler.d.ts +67 -1
  14. package/dist/cjs/src/wagmi/WagmiEventHandler.js +286 -39
  15. package/dist/cjs/src/wagmi/types.d.ts +6 -0
  16. package/dist/cjs/src/wagmi/utils.js +9 -1
  17. package/dist/esm/src/FormoAnalytics.d.ts +15 -4
  18. package/dist/esm/src/FormoAnalytics.js +37 -6
  19. package/dist/esm/src/FormoAnalyticsProvider.d.ts +8 -0
  20. package/dist/esm/src/FormoAnalyticsProvider.js +39 -31
  21. package/dist/esm/src/evm/EvmEventTracker.d.ts +100 -9
  22. package/dist/esm/src/evm/EvmEventTracker.js +357 -19
  23. package/dist/esm/src/evm/EvmProviderRegistry.d.ts +34 -2
  24. package/dist/esm/src/evm/EvmProviderRegistry.js +61 -4
  25. package/dist/esm/src/evm/EvmRequestTracker.d.ts +21 -4
  26. package/dist/esm/src/evm/EvmRequestTracker.js +54 -28
  27. package/dist/esm/src/version.d.ts +1 -1
  28. package/dist/esm/src/version.js +1 -1
  29. package/dist/esm/src/wagmi/WagmiEventHandler.d.ts +67 -1
  30. package/dist/esm/src/wagmi/WagmiEventHandler.js +286 -39
  31. package/dist/esm/src/wagmi/types.d.ts +6 -0
  32. package/dist/esm/src/wagmi/utils.js +9 -1
  33. package/dist/index.umd.min.js +1 -1
  34. package/package.json +2 -2
@@ -136,10 +136,54 @@ var EvmEventTracker = /** @class */ (function () {
136
136
  * Announcement-driven cleanup must not touch them: they are never in an
137
137
  * announcement list, so "missing from the announcement" is their normal
138
138
  * state, not evidence of removal. A Set rather than a WeakSet because
139
- * suppressed adoptions retry from it; the registry holds these providers
140
- * strongly anyway, and untrack removes them.
139
+ * the corrected-detect pass iterates it; the registry's detail list
140
+ * holds these providers strongly for the instance's life anyway.
141
141
  */
142
142
  this.externallyRegistered = new Set();
143
+ /**
144
+ * Registered providers whose session this SDK has NOT yet learned.
145
+ *
146
+ * Only these are retried on page hits and opt-in. Re-running adoption for
147
+ * every registered provider looked idempotent but was not: adoption is
148
+ * the same path a live `accountsChanged` takes, and that path treats a
149
+ * provider with a different address from the active one as a wallet
150
+ * switch. With two registered providers, or one registered next to a
151
+ * discovered wallet, every page hit emitted a disconnect and a connect
152
+ * that no user action caused.
153
+ *
154
+ * A provider enters at registration (it may have no session yet, or be
155
+ * refused because tracking is suppressed), again whenever a handler
156
+ * refuses its signal while suppressed (noted on entry AND at the
157
+ * suppressed commit, since the handlers gate on the active provider and
158
+ * re-check suppression after an await), and again for all of them when
159
+ * an opt-out purges identity. It leaves the first time a handler commits
160
+ * its session unsuppressed, or when it is untracked. Retrying reads the
161
+ * provider's SYNCHRONOUS accounts only - no RPC - so a registered
162
+ * provider whose session never fires `accountsChanged` (a wallet that
163
+ * signals `connect` alone while `autocapture.connect` is off, which
164
+ * installs a chain-only observer) is still adopted on the next hit. "No
165
+ * RPC" holds for the pending provider itself; the handler's switch
166
+ * arbitration may still ask the ACTIVE provider for its accounts, as it
167
+ * does for any live signal.
168
+ *
169
+ * Replay goes through the accounts handler, which has live wallet-switch
170
+ * semantics: a different provider with a different address is a switch.
171
+ * So a pending provider is replayed only while no OTHER wallet is active
172
+ * and known - it waits, at no cost, until that wallet is gone - unless
173
+ * its own signal was refused (`latestRefused`): then the replay does
174
+ * exactly what the live signal would have done, switch included.
175
+ */
176
+ this.pendingAdoptions = new Set();
177
+ /**
178
+ * Registered providers whose session ended and have not signalled a
179
+ * new one. Some providers keep stale synchronous `accounts` after a
180
+ * disconnect; replaying those would re-install a session that is over.
181
+ * Any later session signal (connect, accountsChanged) lifts this.
182
+ */
183
+ this.awaitingNewSession = new Set();
184
+ /** One retry scan at a time; a call during a scan queues one more. */
185
+ this.retryInFlight = false;
186
+ this.retryRequested = false;
143
187
  /**
144
188
  * The connect this SDK has already reported for a provider.
145
189
  *
@@ -148,10 +192,89 @@ var EvmEventTracker = /** @class */ (function () {
148
192
  * which is when a record must lapse.
149
193
  */
150
194
  this._announcedConnect = new WeakMap();
195
+ /** Set by `cleanup()`; every awaited continuation checks it. */
196
+ this.disposed = false;
197
+ /** Bumped when a registered provider's session ends. See reopenAdoption. */
198
+ this.sessionGenerations = new WeakMap();
151
199
  }
200
+ EvmEventTracker.prototype.dropRefusal = function (provider) {
201
+ if (this.latestRefused === provider)
202
+ this.latestRefused = undefined;
203
+ };
204
+ /**
205
+ * An opt-out purges wallet identity. Every registered session is then
206
+ * unknown again (pending; no refusal is ADDED, so the opt-in replay of a
207
+ * merely connected wallet does not switch, while a refusal already
208
+ * standing from an excluded route keeps its meaning), and the
209
+ * opt-in retry must re-learn all of them: with the active wallet's
210
+ * address gone, the accounts handler cannot even tell a second wallet's
211
+ * accounts apart from the active one's, and would ignore them.
212
+ */
213
+ EvmEventTracker.prototype.markRegisteredAdoptionsPending = function () {
214
+ var _this = this;
215
+ this.externallyRegistered.forEach(function (provider) {
216
+ return _this.pendingAdoptions.add(provider);
217
+ });
218
+ };
219
+ /**
220
+ * Remember a registered provider's signal that suppression refuses.
221
+ *
222
+ * Replay privilege (`latestRefused`) goes only to a provider whose
223
+ * session the replay can actually read - synchronous accounts - so an
224
+ * accountless `connect` (a chain-only observation, or a session still
225
+ * pairing) cannot displace an earlier refusal that is adoptable.
226
+ */
227
+ EvmEventTracker.prototype.noteRefusalIfSuppressed = function (provider) {
228
+ if (this.deps.isTrackingSuppressed() &&
229
+ this.externallyRegistered.has(provider)) {
230
+ this.pendingAdoptions.add(provider);
231
+ var adoptable = false;
232
+ try {
233
+ adoptable = readProviderAccounts(provider).length > 0;
234
+ }
235
+ catch (_a) {
236
+ /* unreadable state: pending, not privileged */
237
+ }
238
+ if (adoptable)
239
+ this.latestRefused = provider;
240
+ }
241
+ };
242
+ /** A handler has learned this provider's session; nothing is pending. */
243
+ EvmEventTracker.prototype.settleAdoption = function (provider) {
244
+ this.pendingAdoptions.delete(provider);
245
+ this.awaitingNewSession.delete(provider);
246
+ this.dropRefusal(provider);
247
+ };
248
+ /**
249
+ * A registered provider's session has ended. Its NEXT session is not
250
+ * yet learned, and may announce itself in a way no handler adopts (a
251
+ * `connect` alone while `autocapture.connect` is off installs a
252
+ * chain-only observer), so it is pending again for the page-hit retry.
253
+ */
254
+ EvmEventTracker.prototype.reopenAdoption = function (provider) {
255
+ var _a;
256
+ if (this.externallyRegistered.has(provider)) {
257
+ this.pendingAdoptions.add(provider);
258
+ this.awaitingNewSession.add(provider);
259
+ // A lookup still in flight for the session that just ended must not
260
+ // record a refusal for it when it resolves.
261
+ this.sessionGenerations.set(provider, ((_a = this.sessionGenerations.get(provider)) !== null && _a !== void 0 ? _a : 0) + 1);
262
+ // The refusal marker described the session that just ended. Left
263
+ // standing, a later connect-only session would inherit it and be
264
+ // replayed as a switch away from another active wallet.
265
+ this.dropRefusal(provider);
266
+ }
267
+ };
152
268
  /** Stop listening for wallet announcements. Called from SDK teardown. */
153
269
  EvmEventTracker.prototype.cleanup = function () {
154
270
  var _a;
271
+ // A replay in flight (awaiting the active wallet's accounts) must not
272
+ // resume into a torn-down instance and commit a session there.
273
+ this.disposed = true;
274
+ this.pendingAdoptions.clear();
275
+ this.awaitingNewSession.clear();
276
+ this.latestRefused = undefined;
277
+ this.retryRequested = false;
155
278
  try {
156
279
  (_a = this.unsubscribeDiscovery) === null || _a === void 0 ? void 0 : _a.call(this);
157
280
  }
@@ -160,6 +283,10 @@ var EvmEventTracker = /** @class */ (function () {
160
283
  }
161
284
  this.unsubscribeDiscovery = undefined;
162
285
  };
286
+ EvmEventTracker.prototype.sessionGeneration = function (provider) {
287
+ var _a;
288
+ return (_a = this.sessionGenerations.get(provider)) !== null && _a !== void 0 ? _a : 0;
289
+ };
163
290
  /** Drop a provider's reported connect. Called when it stops being active. */
164
291
  EvmEventTracker.prototype.forgetAnnouncedConnect = function (provider) {
165
292
  this._announcedConnect.delete(provider);
@@ -321,6 +448,9 @@ var EvmEventTracker = /** @class */ (function () {
321
448
  void this.detectWallets([
322
449
  __assign(__assign({}, detail), { info: __assign(__assign({}, detail.info), this.registry.infoFor(provider)) }),
323
450
  ]);
451
+ // Pending until a handler commits its session unsuppressed: the
452
+ // session may not exist yet, or adoption may be refused right below.
453
+ this.pendingAdoptions.add(provider);
324
454
  var accounts = readProviderAccounts(provider);
325
455
  if (accounts.length > 0) {
326
456
  void this.onAccountsChanged(provider, accounts);
@@ -328,14 +458,19 @@ var EvmEventTracker = /** @class */ (function () {
328
458
  return true;
329
459
  };
330
460
  /**
331
- * Re-run session adoption for every registered external provider.
461
+ * Finish what registration could not.
462
+ *
463
+ * A registered provider's session may not have existed at registration,
464
+ * or its adoption was refused because tracking was suppressed (opt-out,
465
+ * excluded route) - and an existing session may never emit another
466
+ * accountsChanged, so nothing else would ever retry. Called when
467
+ * suppression can have ended (opt-in, page navigation).
332
468
  *
333
- * Registration while tracking was suppressed (opt-out, excluded route)
334
- * reached the adoption path and was refused - and a provider whose
335
- * session already exists may never emit another accountsChanged, so
336
- * nothing would ever retry. Called when suppression can have ended
337
- * (opt-in, page navigation). Idempotent: an already-adopted wallet is
338
- * deduplicated by the same state and markers as any repeated signal.
469
+ * Adoption is retried ONLY for providers not yet adopted
470
+ * (`pendingAdoptions`), only from their synchronous accounts, and only
471
+ * once suppression has actually ended. The corrected-detect pass below
472
+ * runs for every registered provider: it is deduplicated per session by
473
+ * rdns, so repeating it is free.
339
474
  */
340
475
  EvmEventTracker.prototype.retryExternalAdoptions = function () {
341
476
  var _this = this;
@@ -358,11 +493,97 @@ var EvmEventTracker = /** @class */ (function () {
358
493
  },
359
494
  ]);
360
495
  }
361
- var accounts = readProviderAccounts(provider);
362
- if (accounts.length > 0) {
363
- void _this.onAccountsChanged(provider, accounts);
364
- }
365
496
  });
497
+ if (this.disposed ||
498
+ this.pendingAdoptions.size === 0 ||
499
+ this.deps.isTrackingSuppressed()) {
500
+ // Nothing to finish, or still suppressed: adopting now would be
501
+ // refused again, and the entry must survive for the next chance.
502
+ return;
503
+ }
504
+ // ONE scan at a time, providers awaited in turn, and the entry is NOT
505
+ // removed here: only a handler that commits the session removes it.
506
+ // Fired concurrently - two overlapping scans from repeated page hits,
507
+ // or two providers within one - the later call made the earlier
508
+ // one's observation stale; a stale return dropped a provider whose
509
+ // session was never learned, and a second scan replayed a provider
510
+ // the first was still switching to, emitting the old wallet's
511
+ // disconnect twice. A call that lands during a scan queues exactly
512
+ // one more scan, so nothing that changed meanwhile is missed.
513
+ if (this.retryInFlight) {
514
+ this.retryRequested = true;
515
+ return;
516
+ }
517
+ this.retryInFlight = true;
518
+ void (function () { return __awaiter(_this, void 0, void 0, function () {
519
+ var first, ordered, _i, ordered_1, provider, accounts, otherWalletActive, _a;
520
+ var _this = this;
521
+ return __generator(this, function (_b) {
522
+ switch (_b.label) {
523
+ case 0:
524
+ _b.trys.push([0, , 9, 10]);
525
+ _b.label = 1;
526
+ case 1:
527
+ this.retryRequested = false;
528
+ first = [this.wallet.provider, this.latestRefused].filter(function (p) { return !!p && _this.pendingAdoptions.has(p); });
529
+ ordered = Array.from(new Set(__spreadArray(__spreadArray([], first, true), Array.from(this.pendingAdoptions), true)));
530
+ _i = 0, ordered_1 = ordered;
531
+ _b.label = 2;
532
+ case 2:
533
+ if (!(_i < ordered_1.length)) return [3 /*break*/, 7];
534
+ provider = ordered_1[_i];
535
+ if (this.disposed || this.deps.isTrackingSuppressed())
536
+ return [3 /*break*/, 7];
537
+ accounts = void 0;
538
+ try {
539
+ accounts = readProviderAccounts(provider);
540
+ }
541
+ catch (_c) {
542
+ // A wallet whose state getters throw (transport disposed)
543
+ // stays pending; it must not abort the scan for the others,
544
+ // nor surface as an unhandled rejection from this task.
545
+ return [3 /*break*/, 6];
546
+ }
547
+ if (accounts.length === 0 || this.awaitingNewSession.has(provider)) {
548
+ // No session yet, or none since the last one ended: stays
549
+ // pending, at no cost.
550
+ return [3 /*break*/, 6];
551
+ }
552
+ otherWalletActive = this.wallet.provider !== undefined &&
553
+ this.wallet.provider !== provider;
554
+ if (otherWalletActive && this.latestRefused !== provider) {
555
+ // Never signalled, merely connected: replaying it now would
556
+ // be reported as a wallet switch nobody made. Waits for
557
+ // that wallet to go.
558
+ return [3 /*break*/, 6];
559
+ }
560
+ _b.label = 3;
561
+ case 3:
562
+ _b.trys.push([3, 5, , 6]);
563
+ return [4 /*yield*/, this.onAccountsChanged(provider, accounts)];
564
+ case 4:
565
+ _b.sent();
566
+ return [3 /*break*/, 6];
567
+ case 5:
568
+ _a = _b.sent();
569
+ return [3 /*break*/, 6];
570
+ case 6:
571
+ _i++;
572
+ return [3 /*break*/, 2];
573
+ case 7:
574
+ if (this.retryRequested &&
575
+ !this.disposed &&
576
+ !this.deps.isTrackingSuppressed()) return [3 /*break*/, 1];
577
+ _b.label = 8;
578
+ case 8: return [3 /*break*/, 10];
579
+ case 9:
580
+ this.retryInFlight = false;
581
+ this.retryRequested = false;
582
+ return [7 /*endfinally*/];
583
+ case 10: return [2 /*return*/];
584
+ }
585
+ });
586
+ }); })();
366
587
  };
367
588
  EvmEventTracker.prototype.registerAccountsChangedListener = function (provider) {
368
589
  var _this = this;
@@ -383,6 +604,11 @@ var EvmEventTracker = /** @class */ (function () {
383
604
  return __generator(this, function (_a) {
384
605
  switch (_a.label) {
385
606
  case 0:
607
+ // A listener that could not be removed at cleanup (its provider's
608
+ // removeListener threw) still fires; it must not touch a torn-down
609
+ // instance.
610
+ if (this.disposed)
611
+ return [2 /*return*/];
386
612
  logger.info("onAccountsChanged", accounts);
387
613
  claims = accounts.length > 0 || this.wallet.provider === provider;
388
614
  observation = claims
@@ -418,11 +644,12 @@ var EvmEventTracker = /** @class */ (function () {
418
644
  */
419
645
  EvmEventTracker.prototype._handleAccountsChanged = function (provider, accounts, observation) {
420
646
  return __awaiter(this, void 0, void 0, function () {
421
- var error_1, address, currentStoredAddress, newProviderAddress, activeProviderAccounts, error_2, nextChainId, wasDisconnected, providerInfo, effectiveChainId;
647
+ var error_1, address, session, currentStoredAddress, newProviderAddress, activeProviderAccounts, error_2, nextChainId, wasDisconnected, providerInfo, effectiveChainId;
422
648
  return __generator(this, function (_a) {
423
649
  switch (_a.label) {
424
650
  case 0:
425
651
  if (!(accounts.length === 0)) return [3 /*break*/, 9];
652
+ this.reopenAdoption(provider);
426
653
  if (!(this.wallet.provider === provider)) return [3 /*break*/, 7];
427
654
  logger.info("OnAccountsChanged: Detecting disconnect, current state:", {
428
655
  evmAddress: this.wallet.evmAddress,
@@ -445,6 +672,9 @@ var EvmEventTracker = /** @class */ (function () {
445
672
  case 2:
446
673
  // Pass EVM state explicitly to ensure we have the data for the disconnect event
447
674
  _a.sent();
675
+ // Torn down during the emission: nothing below may run.
676
+ if (this.disposed)
677
+ return [2 /*return*/];
448
678
  return [3 /*break*/, 4];
449
679
  case 3:
450
680
  error_1 = _a.sent();
@@ -469,6 +699,12 @@ var EvmEventTracker = /** @class */ (function () {
469
699
  logger.warn("onAccountsChanged: Invalid address received", accounts[0]);
470
700
  return [2 /*return*/];
471
701
  }
702
+ // Before the active-provider gates below: a registered provider that
703
+ // is not the active one can return early without ever reaching the
704
+ // suppressed commit, and its session may never signal again.
705
+ this.awaitingNewSession.delete(provider);
706
+ this.noteRefusalIfSuppressed(provider);
707
+ session = this.sessionGeneration(provider);
472
708
  if (!(this.wallet.provider && this.wallet.provider !== provider)) return [3 /*break*/, 26];
473
709
  currentStoredAddress = this.wallet.evmAddress;
474
710
  newProviderAddress = validateAndChecksumAddress(address);
@@ -484,12 +720,24 @@ var EvmEventTracker = /** @class */ (function () {
484
720
  return [4 /*yield*/, this.registry.accountsOf(this.wallet.provider)];
485
721
  case 11:
486
722
  activeProviderAccounts = _a.sent();
723
+ if (this.disposed)
724
+ return [2 /*return*/];
725
+ if (this.sessionGeneration(provider) !== session) {
726
+ // This provider's session ended during the probe: the signal
727
+ // describes nothing that still exists, refusal included.
728
+ this.dropRefusal(provider);
729
+ return [2 /*return*/];
730
+ }
487
731
  // The probe is asynchronous too, so check before issuing anything.
488
732
  // Every branch below reads the CURRENT evm state, so a switch that
489
733
  // went stale during the probe would emit a false disconnect for
490
734
  // whoever claimed the namespace, and clear them.
491
- if (!this.wallet.isCurrent(observation))
735
+ if (!this.wallet.isCurrent(observation)) {
736
+ // Superseded: this signal's refusal, if noted, no longer describes
737
+ // a session the replay may switch to.
738
+ this.dropRefusal(provider);
492
739
  return [2 /*return*/];
740
+ }
493
741
  logger.info("OnAccountsChanged: Checking current provider accounts", {
494
742
  activeProvider: this.registry.infoFor(this.wallet.provider).name,
495
743
  accountsLength: activeProviderAccounts
@@ -515,6 +763,9 @@ var EvmEventTracker = /** @class */ (function () {
515
763
  })];
516
764
  case 12:
517
765
  _a.sent();
766
+ // Torn down during the emission: nothing below may run.
767
+ if (this.disposed)
768
+ return [2 /*return*/];
518
769
  return [3 /*break*/, 14];
519
770
  case 13:
520
771
  logger.debug("OnAccountsChanged: Disconnect event skipped during provider switch (autocapture.disconnect: false)");
@@ -522,12 +773,31 @@ var EvmEventTracker = /** @class */ (function () {
522
773
  this.wallet.clear('evm');
523
774
  _a.label = 14;
524
775
  case 14:
525
- if (!this.wallet.isCurrent(observation))
776
+ if (!this.wallet.isCurrent(observation)) {
777
+ // Superseded: this signal's refusal, if noted, no longer describes
778
+ // a session the replay may switch to.
779
+ this.dropRefusal(provider);
526
780
  return [2 /*return*/];
781
+ }
527
782
  // Clear state and let the new provider become active
528
783
  this.wallet.clearProvider();
529
784
  return [3 /*break*/, 16];
530
785
  case 15:
786
+ // Ignored: the active wallet has accounts and this signal's
787
+ // address is the same, or the active address is unknown (a
788
+ // discovered wallet whose identity an opt-out purged; a
789
+ // registered one is re-learned before this point). THIS
790
+ // provider's session is not adopted; it stays pending without
791
+ // its refusal marker, so it is not probed again on every page
792
+ // hit while the active wallet stands, and is adopted once that
793
+ // wallet is gone. A live signal is ignored here in exactly the
794
+ // same way, so the replay changes nothing about who is active.
795
+ // While SUPPRESSED this is the refusal itself, not a verdict:
796
+ // the marker stands for the replay that runs once suppression
797
+ // ends.
798
+ if (!this.deps.isTrackingSuppressed()) {
799
+ this.dropRefusal(provider);
800
+ }
531
801
  logger.info("OnAccountsChanged: Current provider still has accounts and same address, ignoring new provider", {
532
802
  activeProvider: this.registry.infoFor(this.wallet.provider).name,
533
803
  eventProvider: this.registry.infoFor(provider).name,
@@ -550,6 +820,9 @@ var EvmEventTracker = /** @class */ (function () {
550
820
  })];
551
821
  case 18:
552
822
  _a.sent();
823
+ // Torn down during the emission: nothing below may run.
824
+ if (this.disposed)
825
+ return [2 /*return*/];
553
826
  return [3 /*break*/, 20];
554
827
  case 19:
555
828
  logger.debug("OnAccountsChanged: Disconnect event skipped for old provider (autocapture.disconnect: false)");
@@ -557,8 +830,12 @@ var EvmEventTracker = /** @class */ (function () {
557
830
  this.wallet.clear('evm');
558
831
  _a.label = 20;
559
832
  case 20:
560
- if (!this.wallet.isCurrent(observation))
833
+ if (!this.wallet.isCurrent(observation)) {
834
+ // Superseded: this signal's refusal, if noted, no longer describes
835
+ // a session the replay may switch to.
836
+ this.dropRefusal(provider);
561
837
  return [2 /*return*/];
838
+ }
562
839
  _a.label = 21;
563
840
  case 21: return [3 /*break*/, 26];
564
841
  case 22:
@@ -579,6 +856,9 @@ var EvmEventTracker = /** @class */ (function () {
579
856
  })];
580
857
  case 23:
581
858
  _a.sent();
859
+ // Torn down during the emission: nothing below may run.
860
+ if (this.disposed)
861
+ return [2 /*return*/];
582
862
  return [3 /*break*/, 25];
583
863
  case 24:
584
864
  logger.debug("OnAccountsChanged: Disconnect event skipped for failed provider check (autocapture.disconnect: false)");
@@ -586,8 +866,12 @@ var EvmEventTracker = /** @class */ (function () {
586
866
  this.wallet.clear('evm');
587
867
  _a.label = 25;
588
868
  case 25:
589
- if (!this.wallet.isCurrent(observation))
869
+ if (!this.wallet.isCurrent(observation)) {
870
+ // Superseded: this signal's refusal, if noted, no longer describes
871
+ // a session the replay may switch to.
872
+ this.dropRefusal(provider);
590
873
  return [2 /*return*/];
874
+ }
591
875
  return [3 /*break*/, 26];
592
876
  case 26:
593
877
  // Set provider if none exists (first connection)
@@ -596,6 +880,8 @@ var EvmEventTracker = /** @class */ (function () {
596
880
  }
597
881
  // If both the provider and address are the same, no-op
598
882
  if (this.wallet.provider === provider && address === this.wallet.evmAddress) {
883
+ // This session is the one already known; nothing is pending for it.
884
+ this.settleAdoption(provider);
599
885
  return [2 /*return*/];
600
886
  }
601
887
  nextChainId = this.registry.resolveChainId(provider);
@@ -606,9 +892,14 @@ var EvmEventTracker = /** @class */ (function () {
606
892
  // events.)
607
893
  if (this.deps.isTrackingSuppressed()) {
608
894
  this.wallet.clearStaleEvmWalletOnSwitchWhileSuppressed(address);
895
+ // Again after the await: suppression may have begun mid-flight.
896
+ this.noteRefusalIfSuppressed(provider);
609
897
  }
610
898
  else {
611
899
  this.wallet.set('evm', { address: address, chainId: nextChainId });
900
+ // Adopted unsuppressed, whichever path got here first: a retry for
901
+ // this provider would now be a repeat, not a completion.
902
+ this.settleAdoption(provider);
612
903
  }
613
904
  providerInfo = this.registry.infoFor(provider);
614
905
  effectiveChainId = nextChainId || 0;
@@ -667,6 +958,11 @@ var EvmEventTracker = /** @class */ (function () {
667
958
  return __generator(this, function (_a) {
668
959
  switch (_a.label) {
669
960
  case 0:
961
+ // A listener that could not be removed at cleanup (its provider's
962
+ // removeListener threw) still fires; it must not touch a torn-down
963
+ // instance.
964
+ if (this.disposed)
965
+ return [2 /*return*/];
670
966
  logger.info("onChainChanged", chainIdHex);
671
967
  nextChainId = parseChainId(chainIdHex);
672
968
  // Record it for THIS provider regardless of which one is active. This is
@@ -767,9 +1063,16 @@ var EvmEventTracker = /** @class */ (function () {
767
1063
  args[_i] = arguments[_i];
768
1064
  }
769
1065
  var connection = args[0];
1066
+ if (_this.disposed)
1067
+ return;
770
1068
  if (typeof (connection === null || connection === void 0 ? void 0 : connection.chainId) !== "string")
771
1069
  return;
772
1070
  _this.registry.rememberChain(provider, parseChainId(connection.chainId));
1071
+ _this.awaitingNewSession.delete(provider);
1072
+ // This observer adopts nothing, but a registered provider's connect
1073
+ // refused by suppression is still a refused signal: once suppression
1074
+ // ends, its replay may switch, as the full handler's would.
1075
+ _this.noteRefusalIfSuppressed(provider);
773
1076
  };
774
1077
  provider.on("connect", listener);
775
1078
  this.registry.addListener(provider, "connect", listener);
@@ -828,6 +1131,9 @@ var EvmEventTracker = /** @class */ (function () {
828
1131
  return __generator(this, function (_a) {
829
1132
  switch (_a.label) {
830
1133
  case 0:
1134
+ if (this.disposed)
1135
+ return [2 /*return*/];
1136
+ this.reopenAdoption(provider);
831
1137
  if (this.wallet.provider !== provider)
832
1138
  return [2 /*return*/];
833
1139
  // As in the accountsChanged disconnect path: the reported connect ends
@@ -849,6 +1155,9 @@ var EvmEventTracker = /** @class */ (function () {
849
1155
  case 2:
850
1156
  // Pass current state explicitly to ensure we have the data for the disconnect event
851
1157
  _a.sent();
1158
+ // Torn down during the emission: nothing below may run.
1159
+ if (this.disposed)
1160
+ return [2 /*return*/];
852
1161
  return [3 /*break*/, 4];
853
1162
  case 3:
854
1163
  e_1 = _a.sent();
@@ -870,11 +1179,20 @@ var EvmEventTracker = /** @class */ (function () {
870
1179
  };
871
1180
  EvmEventTracker.prototype.onConnected = function (provider, connection) {
872
1181
  return __awaiter(this, void 0, void 0, function () {
873
- var disconnectsBefore, stateTicket, chainId, address, committed, movedElsewhere, wasDisconnected, isActiveProvider, providerInfo, effectiveChainId, providerInfo, e_2;
1182
+ var disconnectsBefore, stateTicket, chainId, session, address, committed, movedElsewhere, wasDisconnected, isActiveProvider, providerInfo, effectiveChainId, providerInfo, e_2;
874
1183
  return __generator(this, function (_a) {
875
1184
  switch (_a.label) {
876
1185
  case 0:
1186
+ // A listener that could not be removed at cleanup (its provider's
1187
+ // removeListener threw) still fires; it must not touch a torn-down
1188
+ // instance.
1189
+ if (this.disposed)
1190
+ return [2 /*return*/];
877
1191
  logger.info("onConnected", connection);
1192
+ // A session signal, and BEFORE the account lookup below: suppression
1193
+ // that ends while the lookup is in flight must not lose the refusal.
1194
+ this.awaitingNewSession.delete(provider);
1195
+ this.noteRefusalIfSuppressed(provider);
878
1196
  disconnectsBefore = this.wallet.disconnectsSoFar("evm");
879
1197
  stateTicket = this.wallet.currentObservation("evm");
880
1198
  _a.label = 1;
@@ -885,9 +1203,18 @@ var EvmEventTracker = /** @class */ (function () {
885
1203
  chainId = parseChainId(connection.chainId);
886
1204
  // Record it for this provider before anything can bail out below.
887
1205
  this.registry.rememberChain(provider, chainId);
1206
+ session = this.sessionGeneration(provider);
888
1207
  return [4 /*yield*/, this.registry.addressOf(provider)];
889
1208
  case 2:
890
1209
  address = _a.sent();
1210
+ if (this.disposed)
1211
+ return [2 /*return*/];
1212
+ if (this.sessionGeneration(provider) !== session) {
1213
+ // Disconnected while the lookup was in flight: a former account
1214
+ // arriving now must not record a refusal for an ended session.
1215
+ this.dropRefusal(provider);
1216
+ return [2 /*return*/];
1217
+ }
891
1218
  // A newer signal arrived while we were resolving the address. Claiming
892
1219
  // the namespace now would write this stale view over it, and would make
893
1220
  // a disconnect still in flight look stale so it skipped its cleanup. A
@@ -895,6 +1222,7 @@ var EvmEventTracker = /** @class */ (function () {
895
1222
  // is reported normally.
896
1223
  if (this.wallet.disconnectsSoFar("evm") !== disconnectsBefore) {
897
1224
  logger.info("onConnected: The wallet disconnected after this observation began; dropping it");
1225
+ this.dropRefusal(provider);
898
1226
  return [2 /*return*/];
899
1227
  }
900
1228
  committed = this.wallet.evmAddress;
@@ -905,10 +1233,15 @@ var EvmEventTracker = /** @class */ (function () {
905
1233
  !this.wallet.isCurrent(stateTicket) &&
906
1234
  movedElsewhere) {
907
1235
  logger.info("onConnected: A newer signal from this provider has overtaken this connect observation; dropping it");
1236
+ this.dropRefusal(provider);
908
1237
  return [2 /*return*/];
909
1238
  }
910
1239
  if (chainId && address) {
911
1240
  wasDisconnected = !this.wallet.evmAddress;
1241
+ // Same refusal as the accounts path, and before the active-provider
1242
+ // gate: a registered provider that connects while another one is
1243
+ // active is not adopted here, and may never signal again.
1244
+ this.noteRefusalIfSuppressed(provider);
912
1245
  // Set provider if none exists
913
1246
  if (!this.wallet.provider) {
914
1247
  this.wallet.provider = provider;
@@ -920,12 +1253,15 @@ var EvmEventTracker = /** @class */ (function () {
920
1253
  if (isActiveProvider) {
921
1254
  if (this.deps.isTrackingSuppressed()) {
922
1255
  this.wallet.clearStaleEvmWalletOnSwitchWhileSuppressed(address);
1256
+ // Again after the await: suppression may have begun mid-flight.
1257
+ this.noteRefusalIfSuppressed(provider);
923
1258
  }
924
1259
  else {
925
1260
  this.wallet.set('evm', {
926
1261
  chainId: chainId,
927
1262
  address: validateAndChecksumAddress(address) || undefined,
928
1263
  });
1264
+ this.settleAdoption(provider);
929
1265
  }
930
1266
  }
931
1267
  // Conditionally emit connect event based on tracking configuration.
@@ -1150,6 +1486,8 @@ var EvmEventTracker = /** @class */ (function () {
1150
1486
  this.registry.rememberChain(provider, chainId);
1151
1487
  };
1152
1488
  EvmEventTracker.prototype.untrackProvider = function (provider) {
1489
+ // An untracked provider has nothing left to finish.
1490
+ this.settleAdoption(provider);
1153
1491
  try {
1154
1492
  this.registry.removeListeners(provider);
1155
1493
  // Only stop tracking it if the listeners actually came off. "Tracked"