@formo/analytics 1.38.0 → 1.38.1

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.
@@ -402,9 +402,9 @@ export declare class FormoAnalytics implements IFormoAnalytics {
402
402
  *
403
403
  * With several live SDK instances (multi write-key pages) registering
404
404
  * the SAME provider, request-derived events (signatures, transactions)
405
- * go to the most recently registered live instance - the same
406
- * single-observer semantics discovery has always had for the request
407
- * wrapper. Lifecycle events (connect, chain, disconnect) reach every
405
+ * go to the most recently CREATED live instance, regardless of the
406
+ * order registrations happen to land in - the same single-observer
407
+ * semantics discovery has always had for the request wrapper. Lifecycle events (connect, chain, disconnect) reach every
408
408
  * instance. Fanning request observations out to all instances is a
409
409
  * separate feature.
410
410
  *
@@ -429,7 +429,9 @@ export declare class FormoAnalytics implements IFormoAnalytics {
429
429
  * request wrapper installs here. Double counting is prevented in the
430
430
  * wrapper via `shouldSkipRequestCapture`.
431
431
  */
432
- _wrapWagmiProvider(provider: EIP1193Provider): void;
432
+ _wrapWagmiProvider(provider: EIP1193Provider, chainId?: number): boolean;
433
+ /** INTERNAL. Chain updates for the fallback-wrapped provider. */
434
+ _rememberWagmiProviderChain(provider: EIP1193Provider, chainId: number | undefined): void;
433
435
  registerProvider(provider: EIP1193Provider, info?: {
434
436
  name?: string;
435
437
  rdns?: string;
@@ -1334,9 +1334,9 @@ var FormoAnalytics = /** @class */ (function () {
1334
1334
  *
1335
1335
  * With several live SDK instances (multi write-key pages) registering
1336
1336
  * the SAME provider, request-derived events (signatures, transactions)
1337
- * go to the most recently registered live instance - the same
1338
- * single-observer semantics discovery has always had for the request
1339
- * wrapper. Lifecycle events (connect, chain, disconnect) reach every
1337
+ * go to the most recently CREATED live instance, regardless of the
1338
+ * order registrations happen to land in - the same single-observer
1339
+ * semantics discovery has always had for the request wrapper. Lifecycle events (connect, chain, disconnect) reach every
1340
1340
  * instance. Fanning request observations out to all instances is a
1341
1341
  * separate feature.
1342
1342
  *
@@ -1361,14 +1361,35 @@ var FormoAnalytics = /** @class */ (function () {
1361
1361
  * request wrapper installs here. Double counting is prevented in the
1362
1362
  * wrapper via `shouldSkipRequestCapture`.
1363
1363
  */
1364
- FormoAnalytics.prototype._wrapWagmiProvider = function (provider) {
1364
+ FormoAnalytics.prototype._wrapWagmiProvider = function (provider, chainId) {
1365
1365
  if (this.isCleanedUp || !isValidProvider(provider))
1366
- return;
1366
+ return false;
1367
1367
  try {
1368
- this.evmRequests.registerRequestListeners(provider);
1368
+ // The tracker reports refusals (frozen provider, unrebindable
1369
+ // wrapper) by returning false rather than throwing; treat those as
1370
+ // failures too so the caller can retry later.
1371
+ if (!this.evmRequests.registerRequestListeners(provider)) {
1372
+ return false;
1373
+ }
1374
+ if (chainId !== undefined) {
1375
+ this.evm.rememberChain(provider, chainId);
1376
+ }
1377
+ return true;
1369
1378
  }
1370
1379
  catch (e) {
1371
1380
  logger.warn("Failed to wrap wagmi provider for hybrid capture", e);
1381
+ return false;
1382
+ }
1383
+ };
1384
+ /** INTERNAL. Chain updates for the fallback-wrapped provider. */
1385
+ FormoAnalytics.prototype._rememberWagmiProviderChain = function (provider, chainId) {
1386
+ if (this.isCleanedUp)
1387
+ return;
1388
+ try {
1389
+ this.evm.rememberChain(provider, chainId);
1390
+ }
1391
+ catch (_a) {
1392
+ /* bookkeeping only */
1372
1393
  }
1373
1394
  };
1374
1395
  FormoAnalytics.prototype.registerProvider = function (provider, info) {
@@ -7,6 +7,14 @@ export interface FormoAnalyticsProviderProps {
7
7
  children: ReactNode;
8
8
  }
9
9
  export declare const FormoAnalyticsContext: import("react").Context<IFormoAnalytics>;
10
+ /**
11
+ * A stable key over the serializable parts of Options. The provider effect
12
+ * re-initialises the SDK when this key changes; anything that alters SDK
13
+ * behaviour must be represented here, or a runtime change to it is silently
14
+ * ignored. Complex objects are tracked by presence, plus the flags that
15
+ * change what the SDK does with them.
16
+ */
17
+ export declare const computeOptionsKey: (options?: Options) => string;
10
18
  export declare const FormoAnalyticsProvider: FC<FormoAnalyticsProviderProps>;
11
19
  export declare const useFormo: () => IFormoAnalytics;
12
20
  //# sourceMappingURL=FormoAnalyticsProvider.d.ts.map
@@ -69,6 +69,44 @@ var defaultContext = {
69
69
  hasOptedOutTracking: function () { return false; },
70
70
  };
71
71
  export var FormoAnalyticsContext = createContext(defaultContext);
72
+ /**
73
+ * A stable key over the serializable parts of Options. The provider effect
74
+ * re-initialises the SDK when this key changes; anything that alters SDK
75
+ * behaviour must be represented here, or a runtime change to it is silently
76
+ * ignored. Complex objects are tracked by presence, plus the flags that
77
+ * change what the SDK does with them.
78
+ */
79
+ export var computeOptionsKey = function (options) {
80
+ var _a;
81
+ if (!options)
82
+ return 'undefined';
83
+ var serializableOptions = {
84
+ tracking: options.tracking,
85
+ autocapture: options.autocapture,
86
+ crossSubdomainCookies: options.crossSubdomainCookies,
87
+ apiHost: options.apiHost,
88
+ flushAt: options.flushAt,
89
+ flushInterval: options.flushInterval,
90
+ retryCount: options.retryCount,
91
+ maxQueueSize: options.maxQueueSize,
92
+ logger: options.logger,
93
+ referral: options.referral,
94
+ evm: options.evm,
95
+ // For complex objects, just track their presence, not their content
96
+ hasProvider: !!options.provider,
97
+ hasWagmi: !!options.wagmi,
98
+ wagmiEip1193Fallback: !!((_a = options.wagmi) === null || _a === void 0 ? void 0 : _a.eip1193Fallback),
99
+ hasReady: !!options.ready,
100
+ };
101
+ try {
102
+ return JSON.stringify(serializableOptions);
103
+ }
104
+ catch (error) {
105
+ // Fallback to timestamp if serialization fails
106
+ logger.warn('Failed to serialize options, using timestamp', error);
107
+ return Date.now().toString();
108
+ }
109
+ };
72
110
  export var FormoAnalyticsProvider = function (props) {
73
111
  var writeKey = props.writeKey, _a = props.disabled, disabled = _a === void 0 ? false : _a, children = props.children;
74
112
  // Keep the app running without analytics if no Write Key is provided or disabled
@@ -87,37 +125,7 @@ var InitializedAnalytics = function (_a) {
87
125
  var _b = useState(defaultContext), sdk = _b[0], setSdk = _b[1];
88
126
  var sdkRef = useRef(defaultContext);
89
127
  initStorageManager(writeKey);
90
- // Create a stable key from options that ignores complex objects and functions
91
- // We only care about serializable config values that would affect SDK behavior
92
- var optionsKey = useMemo(function () {
93
- if (!options)
94
- return 'undefined';
95
- // Extract only the serializable parts of options
96
- var serializableOptions = {
97
- tracking: options.tracking,
98
- autocapture: options.autocapture,
99
- crossSubdomainCookies: options.crossSubdomainCookies,
100
- apiHost: options.apiHost,
101
- flushAt: options.flushAt,
102
- flushInterval: options.flushInterval,
103
- retryCount: options.retryCount,
104
- maxQueueSize: options.maxQueueSize,
105
- logger: options.logger,
106
- referral: options.referral,
107
- // For complex objects, just track their presence, not their content
108
- hasProvider: !!options.provider,
109
- hasWagmi: !!options.wagmi,
110
- hasReady: !!options.ready,
111
- };
112
- try {
113
- return JSON.stringify(serializableOptions);
114
- }
115
- catch (error) {
116
- // Fallback to timestamp if serialization fails
117
- logger.warn('Failed to serialize options, using timestamp', error);
118
- return Date.now().toString();
119
- }
120
- }, [options]);
128
+ var optionsKey = useMemo(function () { return computeOptionsKey(options); }, [options]);
121
129
  useEffect(function () {
122
130
  var isCleanedUp = false;
123
131
  var initialize = function () { return __awaiter(void 0, void 0, void 0, function () {
@@ -57,6 +57,18 @@ export declare class EvmRequestTracker {
57
57
  * never retry it, and every signature and transaction from that wallet
58
58
  * would be missed for the rest of the session.
59
59
  */
60
+ /** Monotonic creation stamp; owner precedence is decided by it. */
61
+ private static nextCreationSeq;
62
+ readonly creationSeq: number;
63
+ /**
64
+ * Put this tracker into an owner list at its CREATION-ORDER position.
65
+ * Registration can arrive out of order - an older instance's async wrap
66
+ * kick may resolve after a newer instance's - and dispatch picks from
67
+ * the END of the list, so append-on-registration would let resolution
68
+ * order decide which instance owns capture. Creation order is what the
69
+ * newest-live contract promises.
70
+ */
71
+ private insertBySeniority;
60
72
  registerRequestListeners(provider: EIP1193Provider): boolean;
61
73
  /** Install the wrapper function onto the provider; separated so the
62
74
  * routing shim above stays small. */
@@ -137,6 +137,7 @@ var EvmRequestTracker = /** @class */ (function () {
137
137
  this.disposed = false;
138
138
  /** Providers whose owner list includes this instance; pruned on cleanup. */
139
139
  this.wrappedProviders = new Set();
140
+ this.creationSeq = EvmRequestTracker.nextCreationSeq++;
140
141
  }
141
142
  /** Stop every poll in flight. Terminal, like the event queue's close(). */
142
143
  EvmRequestTracker.prototype.cleanup = function () {
@@ -170,11 +171,24 @@ var EvmRequestTracker = /** @class */ (function () {
170
171
  this.polls.add(timer);
171
172
  };
172
173
  /**
173
- * Wrap a provider's `request`. Returns whether the wrapper is installed:
174
- * a caller that marks the provider as tracked on a false return would
175
- * never retry it, and every signature and transaction from that wallet
176
- * would be missed for the rest of the session.
174
+ * Put this tracker into an owner list at its CREATION-ORDER position.
175
+ * Registration can arrive out of order - an older instance's async wrap
176
+ * kick may resolve after a newer instance's - and dispatch picks from
177
+ * the END of the list, so append-on-registration would let resolution
178
+ * order decide which instance owns capture. Creation order is what the
179
+ * newest-live contract promises.
177
180
  */
181
+ EvmRequestTracker.prototype.insertBySeniority = function (owners) {
182
+ var _this = this;
183
+ var idx = owners.indexOf(this);
184
+ if (idx !== -1)
185
+ owners.splice(idx, 1);
186
+ var insertAt = owners.findIndex(function (o) { return o.creationSeq > _this.creationSeq; });
187
+ if (insertAt === -1)
188
+ owners.push(this);
189
+ else
190
+ owners.splice(insertAt, 0, this);
191
+ };
178
192
  EvmRequestTracker.prototype.registerRequestListeners = function (provider) {
179
193
  var _this = this;
180
194
  logger.info("registerRequestListeners");
@@ -196,10 +210,7 @@ var EvmRequestTracker = /** @class */ (function () {
196
210
  logger.warn("wrapped without owner list; cannot rebind");
197
211
  return false;
198
212
  }
199
- var idx = owners.indexOf(this);
200
- if (idx !== -1)
201
- owners.splice(idx, 1);
202
- owners.push(this);
213
+ this.insertBySeniority(owners);
203
214
  this.wrappedProviders.add(provider);
204
215
  logger.info("Provider already wrapped; rebinding the wrapper to this instance.");
205
216
  return true;
@@ -240,10 +251,7 @@ var EvmRequestTracker = /** @class */ (function () {
240
251
  var slot = provider;
241
252
  var prior = slot[WRAPPED_REQUEST_OWNER_SYMBOL];
242
253
  var owners = Array.isArray(prior) ? prior : [];
243
- var idx = owners.indexOf(this);
244
- if (idx !== -1)
245
- owners.splice(idx, 1);
246
- owners.push(this);
254
+ this.insertBySeniority(owners);
247
255
  if (!Array.isArray(prior)) {
248
256
  slot[WRAPPED_REQUEST_OWNER_SYMBOL] = owners;
249
257
  }
@@ -848,6 +856,14 @@ var EvmRequestTracker = /** @class */ (function () {
848
856
  });
849
857
  });
850
858
  };
859
+ /**
860
+ * Wrap a provider's `request`. Returns whether the wrapper is installed:
861
+ * a caller that marks the provider as tracked on a false return would
862
+ * never retry it, and every signature and transaction from that wallet
863
+ * would be missed for the rest of the session.
864
+ */
865
+ /** Monotonic creation stamp; owner precedence is decided by it. */
866
+ EvmRequestTracker.nextCreationSeq = 0;
851
867
  return EvmRequestTracker;
852
868
  }());
853
869
  export { EvmRequestTracker };
@@ -1,2 +1,2 @@
1
- export declare const version = "1.38.0";
1
+ export declare const version = "1.38.1";
2
2
  //# sourceMappingURL=version.d.ts.map
@@ -1,4 +1,4 @@
1
1
  // This file is auto-generated by scripts/update-version.js during npm version
2
2
  // Do not edit manually - it will be overwritten
3
- export var version = '1.38.0';
3
+ export var version = '1.38.1';
4
4
  //# sourceMappingURL=version.js.map
@@ -362,6 +362,43 @@ export declare class WagmiEventHandler {
362
362
  * dedup. Fire-and-forget per connection; a provider that cannot be
363
363
  * produced simply keeps mutation-only capture.
364
364
  */
365
+ /**
366
+ * Connections THIS instance has wrapped. Deliberately per-instance, not
367
+ * module-level: a module-level guard let a rebuilt SDK instance skip
368
+ * re-wrapping, so ownership stayed with the torn-down instance and every
369
+ * capture died in its closed queue - and it also stopped a second
370
+ * write-key instance from ever registering. Re-wrapping is safe and
371
+ * cheap: the request tracker rebinds ownership of an intact wrapper.
372
+ */
373
+ /**
374
+ * Latest wrap attempt per connector. Every kick re-resolves
375
+ * getProvider() - a connector can hand out a REPLACEMENT provider after
376
+ * a reconnect, so a once-only guard would leave the new session's
377
+ * provider unwrapped forever. Re-wrapping is idempotent: the request
378
+ * tracker rebinds ownership of an intact wrapper. The epoch lets a slow
379
+ * resolution from a superseded kick (an earlier session, an earlier
380
+ * chain) be discarded instead of overwriting fresher state.
381
+ */
382
+ private wrapEpochs;
383
+ /** Consecutive automatic wrap retries per connector; see below. */
384
+ private wrapRetryCounts;
385
+ /** Pending retry timers, cancelled by cleanup(). */
386
+ private wrapRetryTimers;
387
+ /**
388
+ * Bumped on every observed disconnect. Every wrap attempt captures it
389
+ * at kick time and its resolution stands down on a mismatch, so a
390
+ * disconnect invalidates ALL pending wraps at once - including ones
391
+ * whose getProvider() had not resolved yet, which no per-connector
392
+ * bookkeeping can reach because nothing has been recorded for them.
393
+ */
394
+ private wrapSessionGeneration;
395
+ /** The active connector this instance wrapped, and its provider, for
396
+ * chain updates. Kept as a pair so a chain report is only ever applied
397
+ * to the provider of the connector it describes. Keyed by connector,
398
+ * not by connection record: wagmi REPLACES the connection object on
399
+ * every account or chain update, so record identity is not stable. */
400
+ private fallbackConnector?;
401
+ private fallbackProvider?;
365
402
  private wrapActiveConnectorProvider;
366
403
  private kickWalletConnectPeerLookup;
367
404
  /**
@@ -238,8 +238,6 @@ var walletConnectPeerLookups = new WeakSet();
238
238
  * newest kicked one, so a slow resolution from a PREVIOUS session cannot
239
239
  * land after the current session's and overwrite it. */
240
240
  var walletConnectPeerLatest = new WeakMap();
241
- /** Connections whose provider already has the hybrid-capture wrapper. */
242
- var wagmiWrappedConnections = new WeakSet();
243
241
  /**
244
242
  * Pending transactions, shared per destination rather than per handler.
245
243
  *
@@ -424,6 +422,57 @@ var WagmiEventHandler = /** @class */ (function () {
424
422
  * Key format: `${queryHash}:${status}`
425
423
  */
426
424
  this.processedQueries = new Set();
425
+ /**
426
+ * Start resolving the wallet behind a WalletConnect connection.
427
+ *
428
+ * Fire-and-forget on purpose: emission paths are synchronous by design
429
+ * and must never wait (see the connect marker comment). Called at the
430
+ * START of the status/address flows rather than only at read time - the
431
+ * lookup is one microtask for an initialised connector, and the emission
432
+ * sits behind several awaits, so kicking early usually means even the
433
+ * FIRST connect names the real wallet. When the race is lost the event
434
+ * honestly says "WalletConnect" and every later event names the peer.
435
+ */
436
+ /**
437
+ * Install the request wrapper on the active connector's provider.
438
+ *
439
+ * This is what lets wagmi mode capture IMPERATIVE viem calls
440
+ * (walletClient.sendTransaction / .signMessage / .writeContract / raw
441
+ * request), which create no mutation and were silently lost. Hook-driven
442
+ * calls stay owned by the mutation handlers via the pending-mutation
443
+ * dedup. Fire-and-forget per connection; a provider that cannot be
444
+ * produced simply keeps mutation-only capture.
445
+ */
446
+ /**
447
+ * Connections THIS instance has wrapped. Deliberately per-instance, not
448
+ * module-level: a module-level guard let a rebuilt SDK instance skip
449
+ * re-wrapping, so ownership stayed with the torn-down instance and every
450
+ * capture died in its closed queue - and it also stopped a second
451
+ * write-key instance from ever registering. Re-wrapping is safe and
452
+ * cheap: the request tracker rebinds ownership of an intact wrapper.
453
+ */
454
+ /**
455
+ * Latest wrap attempt per connector. Every kick re-resolves
456
+ * getProvider() - a connector can hand out a REPLACEMENT provider after
457
+ * a reconnect, so a once-only guard would leave the new session's
458
+ * provider unwrapped forever. Re-wrapping is idempotent: the request
459
+ * tracker rebinds ownership of an intact wrapper. The epoch lets a slow
460
+ * resolution from a superseded kick (an earlier session, an earlier
461
+ * chain) be discarded instead of overwriting fresher state.
462
+ */
463
+ this.wrapEpochs = new WeakMap();
464
+ /** Consecutive automatic wrap retries per connector; see below. */
465
+ this.wrapRetryCounts = new WeakMap();
466
+ /** Pending retry timers, cancelled by cleanup(). */
467
+ this.wrapRetryTimers = new Set();
468
+ /**
469
+ * Bumped on every observed disconnect. Every wrap attempt captures it
470
+ * at kick time and its resolution stands down on a mismatch, so a
471
+ * disconnect invalidates ALL pending wraps at once - including ones
472
+ * whose getProvider() had not resolved yet, which no per-connector
473
+ * bookkeeping can reach because nothing has been recorded for them.
474
+ */
475
+ this.wrapSessionGeneration = 0;
427
476
  this.formo = formoAnalytics;
428
477
  this.wagmiConfig = wagmiConfig;
429
478
  this.queryClient = queryClient;
@@ -598,6 +647,10 @@ var WagmiEventHandler = /** @class */ (function () {
598
647
  var connectionUnsubscribe = this.wagmiConfig.subscribe(function (state) { var _a, _b; return "".concat((_a = state.current) !== null && _a !== void 0 ? _a : "", "|").concat((_b = _this.getConnectedAddress(state)) !== null && _b !== void 0 ? _b : ""); }, function (key, prevKey) {
599
648
  var _a = key.split("|"), address = _a[1];
600
649
  var _b = (prevKey !== null && prevKey !== void 0 ? prevKey : "|").split("|"), prevAddress = _b[1];
650
+ // A connector switch keeps status "connected", so the status
651
+ // subscription never re-wraps; the NEW active connection's provider
652
+ // must be instrumented from here or its imperative calls are lost.
653
+ _this.wrapActiveConnectorProvider(_this.getState());
601
654
  _this.handleActiveAddressChange(address || undefined, prevAddress || undefined);
602
655
  });
603
656
  this.unsubscribers.push(connectionUnsubscribe);
@@ -903,6 +956,19 @@ var WagmiEventHandler = /** @class */ (function () {
903
956
  return __generator(this, function (_e) {
904
957
  switch (_e.label) {
905
958
  case 0:
959
+ if (status === "disconnected" || status === "reconnecting") {
960
+ // The wrapped session is over - or, for "reconnecting", about to be
961
+ // replaced without ever passing through "disconnected". This must
962
+ // run for EVERY such transition, before the processing lock and
963
+ // regardless of whether a wallet was being tracked, or a rapid
964
+ // same-connector reconnect retains the old provider and the chain
965
+ // callback writes the new session's chain onto it. The generation
966
+ // bump invalidates every wrap still in flight from the ended
967
+ // session, resolved or not. The "connected" that follows re-wraps.
968
+ this.wrapSessionGeneration += 1;
969
+ this.fallbackConnector = undefined;
970
+ this.fallbackProvider = undefined;
971
+ }
906
972
  // Prevent concurrent processing
907
973
  if (this.trackingState.isProcessing) {
908
974
  // Dropped, not queued - but remember that a disconnect went past.
@@ -1439,10 +1505,31 @@ var WagmiEventHandler = /** @class */ (function () {
1439
1505
  */
1440
1506
  WagmiEventHandler.prototype.handleChainChange = function (chainId, prevChainId) {
1441
1507
  return __awaiter(this, void 0, void 0, function () {
1442
- var state, address, hadAddress, wasAnnounced, announcedNow;
1443
- return __generator(this, function (_a) {
1444
- switch (_a.label) {
1508
+ var live, activeConnector, state, address, hadAddress, wasAnnounced, announcedNow;
1509
+ var _a, _b, _c;
1510
+ return __generator(this, function (_d) {
1511
+ switch (_d.label) {
1445
1512
  case 0:
1513
+ // Keep the fallback-wrapped provider's registry chain in step with the
1514
+ // store, so request-derived events are labelled correctly even when the
1515
+ // provider exposes no synchronous chainId. Only when the report is
1516
+ // about the connection that provider belongs to: after a connector
1517
+ // switch the store's chain describes the NEW connection, and writing it
1518
+ // to the old provider would mislabel that wallet's events.
1519
+ if (this.fallbackProvider !== undefined) {
1520
+ live = this.getState();
1521
+ activeConnector = live.current
1522
+ ? (_a = live.connections.get(live.current)) === null || _a === void 0 ? void 0 : _a.connector
1523
+ : undefined;
1524
+ if (activeConnector === this.fallbackConnector) {
1525
+ try {
1526
+ (_c = (_b = this.formo)._rememberWagmiProviderChain) === null || _c === void 0 ? void 0 : _c.call(_b, this.fallbackProvider, chainId);
1527
+ }
1528
+ catch (_e) {
1529
+ /* never let bookkeeping break the chain flow */
1530
+ }
1531
+ }
1532
+ }
1446
1533
  if (chainId === prevChainId || chainId === undefined) {
1447
1534
  return [2 /*return*/];
1448
1535
  }
@@ -1488,7 +1575,7 @@ var WagmiEventHandler = /** @class */ (function () {
1488
1575
  });
1489
1576
  return [4 /*yield*/, this.applyChainForTrackedWallet(address, chainId)];
1490
1577
  case 1:
1491
- _a.sent();
1578
+ _d.sent();
1492
1579
  return [2 /*return*/];
1493
1580
  }
1494
1581
  });
@@ -2267,30 +2354,10 @@ var WagmiEventHandler = /** @class */ (function () {
2267
2354
  this.kickWalletConnectPeerLookup(state);
2268
2355
  return connector.name;
2269
2356
  };
2270
- /**
2271
- * Start resolving the wallet behind a WalletConnect connection.
2272
- *
2273
- * Fire-and-forget on purpose: emission paths are synchronous by design
2274
- * and must never wait (see the connect marker comment). Called at the
2275
- * START of the status/address flows rather than only at read time - the
2276
- * lookup is one microtask for an initialised connector, and the emission
2277
- * sits behind several awaits, so kicking early usually means even the
2278
- * FIRST connect names the real wallet. When the race is lost the event
2279
- * honestly says "WalletConnect" and every later event names the peer.
2280
- */
2281
- /**
2282
- * Install the request wrapper on the active connector's provider.
2283
- *
2284
- * This is what lets wagmi mode capture IMPERATIVE viem calls
2285
- * (walletClient.sendTransaction / .signMessage / .writeContract / raw
2286
- * request), which create no mutation and were silently lost. Hook-driven
2287
- * calls stay owned by the mutation handlers via the pending-mutation
2288
- * dedup. Fire-and-forget per connection; a provider that cannot be
2289
- * produced simply keeps mutation-only capture.
2290
- */
2291
- WagmiEventHandler.prototype.wrapActiveConnectorProvider = function (state) {
2357
+ WagmiEventHandler.prototype.wrapActiveConnectorProvider = function (state, isRetry) {
2292
2358
  var _this = this;
2293
- var _a, _b;
2359
+ var _a, _b, _c;
2360
+ if (isRetry === void 0) { isRetry = false; }
2294
2361
  // OPT-IN only. Wagmi mode's baseline never touches the signing
2295
2362
  // transport; instrumenting the provider is an explicit integrator
2296
2363
  // decision (options.wagmi.eip1193Fallback), made auditable in
@@ -2299,25 +2366,124 @@ var WagmiEventHandler = /** @class */ (function () {
2299
2366
  if (!optedIn) {
2300
2367
  return;
2301
2368
  }
2369
+ if (state.status !== "connected") {
2370
+ // Status kicks run for every transition; only a connected snapshot
2371
+ // describes a session worth wrapping.
2372
+ return;
2373
+ }
2302
2374
  var connection = state.current
2303
2375
  ? state.connections.get(state.current)
2304
2376
  : undefined;
2305
2377
  var connector = connection === null || connection === void 0 ? void 0 : connection.connector;
2306
- if (!connection ||
2307
- typeof (connector === null || connector === void 0 ? void 0 : connector.getProvider) !== "function" ||
2308
- wagmiWrappedConnections.has(connection)) {
2378
+ if (!connection || typeof (connector === null || connector === void 0 ? void 0 : connector.getProvider) !== "function") {
2309
2379
  return;
2310
2380
  }
2311
- wagmiWrappedConnections.add(connection);
2312
- connector
2313
- .getProvider()
2381
+ var epoch = ((_c = this.wrapEpochs.get(connector)) !== null && _c !== void 0 ? _c : 0) + 1;
2382
+ this.wrapEpochs.set(connector, epoch);
2383
+ var session = this.wrapSessionGeneration;
2384
+ // A store-driven kick resets the retry budget; automatic retries
2385
+ // (below) spend it.
2386
+ if (!isRetry) {
2387
+ this.wrapRetryCounts.delete(connector);
2388
+ }
2389
+ // When the NEWEST attempt fails, older in-flight resolutions have
2390
+ // been epoch-discarded (they may describe an earlier session) and
2391
+ // nothing else would wrap the provider until the next store update.
2392
+ // Recover by re-kicking fresh with live state, bounded so a
2393
+ // persistently failing connector cannot loop: after the budget is
2394
+ // spent, recovery waits for a real store update, which resets it.
2395
+ var retryAfterFailure = function () {
2396
+ var _a;
2397
+ if (_this.disposed)
2398
+ return;
2399
+ if (_this.wrapEpochs.get(connector) !== epoch) {
2400
+ // Superseded: the newer attempt owns recovery.
2401
+ return;
2402
+ }
2403
+ var failures = ((_a = _this.wrapRetryCounts.get(connector)) !== null && _a !== void 0 ? _a : 0) + 1;
2404
+ _this.wrapRetryCounts.set(connector, failures);
2405
+ if (failures > 3)
2406
+ return;
2407
+ var timer = setTimeout(function () {
2408
+ var _a;
2409
+ _this.wrapRetryTimers.delete(timer);
2410
+ if (_this.disposed)
2411
+ return;
2412
+ // Re-kick only while THIS connector is still the active one and
2413
+ // no newer attempt superseded this one. A retry for a connector
2414
+ // the user has left must not fire at the current connector: with
2415
+ // the retry flag it would skip the budget reset and, worse, its
2416
+ // fresh epoch would discard the current connector's own in-flight
2417
+ // resolution.
2418
+ var live = _this.getState();
2419
+ var stillCurrent = live.current !== undefined &&
2420
+ ((_a = live.connections.get(live.current)) === null || _a === void 0 ? void 0 : _a.connector) === connector;
2421
+ if (stillCurrent && _this.wrapEpochs.get(connector) === epoch) {
2422
+ _this.wrapActiveConnectorProvider(live, true);
2423
+ }
2424
+ }, 25 * failures);
2425
+ _this.wrapRetryTimers.add(timer);
2426
+ };
2427
+ var resolution;
2428
+ try {
2429
+ resolution = connector.getProvider();
2430
+ }
2431
+ catch (_d) {
2432
+ // A synchronous throw must not escape into the subscription
2433
+ // callback: address-change handling runs after this kick and a
2434
+ // propagated throw would silently skip it.
2435
+ retryAfterFailure();
2436
+ return;
2437
+ }
2438
+ resolution
2314
2439
  .then(function (provider) {
2315
- var _a, _b;
2316
- (_b = (_a = _this.formo)._wrapWagmiProvider) === null || _b === void 0 ? void 0 : _b.call(_a, provider);
2440
+ var _a, _b, _c, _d;
2441
+ if (_this.wrapEpochs.get(connector) !== epoch) {
2442
+ // A newer kick is in flight or already resolved; this answer may
2443
+ // describe an earlier session. Let the newest one win.
2444
+ return;
2445
+ }
2446
+ // The fallback installs only the request wrapper, so a provider
2447
+ // without a synchronous chainId property would never teach the
2448
+ // registry its chain and its events would carry chain 0. The wagmi
2449
+ // store knows; feed it here and on every later chain change. Read
2450
+ // the chain from LIVE state, not from a snapshot taken before this
2451
+ // asynchronous resolution: a switch that lands while getProvider is
2452
+ // in flight fires handleChainChange before fallbackProvider exists,
2453
+ // so a pre-resolution snapshot would stick as the recorded chain.
2454
+ // Activity is judged by CONNECTOR identity: wagmi replaces the
2455
+ // connection record itself on every account or chain update.
2456
+ var live = _this.getState();
2457
+ // A wrap only describes a CONNECTED session. Status changes kick
2458
+ // this path for every transition, and a resolution landing while
2459
+ // disconnected would re-point the fallback pair at a provider
2460
+ // whose session is over.
2461
+ var stillActive = !_this.disposed &&
2462
+ _this.wrapSessionGeneration === session &&
2463
+ live.status === "connected" &&
2464
+ live.current !== undefined &&
2465
+ ((_a = live.connections.get(live.current)) === null || _a === void 0 ? void 0 : _a.connector) === connector;
2466
+ if (!stillActive) {
2467
+ // The user moved on while getProvider was in flight. Wrapping
2468
+ // now would instrument a provider whose chain nobody feeds -
2469
+ // its requests would report chain 0. Switching back re-kicks.
2470
+ return;
2471
+ }
2472
+ var chainId = (_b = _this.getActiveConnectionChainId(live)) !== null && _b !== void 0 ? _b : live.chainId;
2473
+ var wrapped = (_d = (_c = _this.formo)._wrapWagmiProvider) === null || _d === void 0 ? void 0 : _d.call(_c, provider, typeof chainId === "number" ? chainId : undefined);
2474
+ if (wrapped !== true) {
2475
+ // The provider was refused (invalid shape, frozen provider,
2476
+ // torn-down SDK).
2477
+ retryAfterFailure();
2478
+ return;
2479
+ }
2480
+ _this.wrapRetryCounts.delete(connector);
2481
+ _this.fallbackConnector = connector;
2482
+ _this.fallbackProvider = provider;
2317
2483
  })
2318
2484
  .catch(function () {
2319
- // Mutation-only capture remains; retry on the next connection.
2320
- wagmiWrappedConnections.delete(connection);
2485
+ // Mutation-only capture remains.
2486
+ retryAfterFailure();
2321
2487
  });
2322
2488
  };
2323
2489
  WagmiEventHandler.prototype.kickWalletConnectPeerLookup = function (state) {
@@ -2407,6 +2573,8 @@ var WagmiEventHandler = /** @class */ (function () {
2407
2573
  * Clean up all subscriptions
2408
2574
  */
2409
2575
  WagmiEventHandler.prototype.cleanup = function () {
2576
+ this.wrapRetryTimers.forEach(function (t) { return clearTimeout(t); });
2577
+ this.wrapRetryTimers.clear();
2410
2578
  logger.debug("WagmiEventHandler: Cleaning up subscriptions");
2411
2579
  if (!this.disposed) {
2412
2580
  this.disposed = true;