@meshconnect/uwc-core 1.2.3-snapshot.bd9aae7 → 1.2.3-snapshot.dcfdbbf
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/events.d.ts +5 -42
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +0 -4
- package/dist/events.js.map +1 -1
- package/dist/observability/focus-tracker.d.ts +12 -25
- package/dist/observability/focus-tracker.d.ts.map +1 -1
- package/dist/observability/focus-tracker.js +19 -42
- package/dist/observability/focus-tracker.js.map +1 -1
- package/dist/observability/instrument-handoff.d.ts.map +1 -1
- package/dist/observability/instrument-handoff.js +6 -24
- package/dist/observability/instrument-handoff.js.map +1 -1
- package/dist/observability/lifecycle-bridge.d.ts +31 -13
- package/dist/observability/lifecycle-bridge.d.ts.map +1 -1
- package/dist/observability/lifecycle-bridge.js +167 -92
- package/dist/observability/lifecycle-bridge.js.map +1 -1
- package/dist/observability/telemetry.d.ts +0 -8
- package/dist/observability/telemetry.d.ts.map +1 -1
- package/dist/observability/telemetry.js +5 -49
- package/dist/observability/telemetry.js.map +1 -1
- package/dist/services/connection-service.d.ts +43 -0
- package/dist/services/connection-service.d.ts.map +1 -1
- package/dist/services/connection-service.js +163 -9
- package/dist/services/connection-service.js.map +1 -1
- package/dist/services/network-switch-service.d.ts.map +1 -1
- package/dist/services/network-switch-service.js +4 -1
- package/dist/services/network-switch-service.js.map +1 -1
- package/dist/universal-wallet-connector.d.ts.map +1 -1
- package/dist/universal-wallet-connector.js +1 -7
- package/dist/universal-wallet-connector.js.map +1 -1
- package/package.json +5 -5
- package/src/events.ts +6 -50
- package/src/observability/focus-tracker.test.ts +8 -60
- package/src/observability/focus-tracker.ts +23 -59
- package/src/observability/instrument-handoff.test.ts +0 -42
- package/src/observability/instrument-handoff.ts +6 -26
- package/src/observability/lifecycle-bridge.test.ts +197 -0
- package/src/observability/lifecycle-bridge.ts +211 -99
- package/src/observability/telemetry.test.ts +0 -35
- package/src/observability/telemetry.ts +6 -58
- package/src/services/connection-service.test.ts +272 -0
- package/src/services/connection-service.ts +190 -10
- package/src/services/network-switch-service.test.ts +33 -0
- package/src/services/network-switch-service.ts +4 -1
- package/src/universal-wallet-connector.ts +1 -11
|
@@ -88,57 +88,17 @@ describe('trackFocusDuringHandoff', () => {
|
|
|
88
88
|
|
|
89
89
|
const { doc, fire } = makeFakeDoc()
|
|
90
90
|
const { win } = makeFakeWin()
|
|
91
|
-
|
|
92
|
-
trackFocusDuringHandoff(em, 'h-1', 'mobile', doc, win, () => t)
|
|
91
|
+
trackFocusDuringHandoff(em, 'h-1', 'mobile', doc, win)
|
|
93
92
|
|
|
94
93
|
doc.visibilityState = 'hidden'
|
|
95
94
|
doc.hidden = true
|
|
96
|
-
fire('visibilitychange') // hidden at t=1000
|
|
97
|
-
t = 1700
|
|
98
|
-
doc.visibilityState = 'visible'
|
|
99
|
-
doc.hidden = false
|
|
100
|
-
fire('visibilitychange') // visible at t=1700 → 700ms hidden
|
|
101
|
-
|
|
102
|
-
expect(hidden).toEqual([{ handoffId: 'h-1', platform: 'mobile' }])
|
|
103
|
-
expect(visible).toEqual([
|
|
104
|
-
{ handoffId: 'h-1', platform: 'mobile', hiddenMs: 700 }
|
|
105
|
-
])
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
it('summary() reports background count + cumulative hidden time, including an open segment', () => {
|
|
109
|
-
const em = new EventManager()
|
|
110
|
-
const { doc, fire } = makeFakeDoc()
|
|
111
|
-
let t = 0
|
|
112
|
-
const tracker = trackFocusDuringHandoff(
|
|
113
|
-
em,
|
|
114
|
-
'h-sum',
|
|
115
|
-
'mobile',
|
|
116
|
-
doc,
|
|
117
|
-
undefined,
|
|
118
|
-
() => t
|
|
119
|
-
)
|
|
120
|
-
|
|
121
|
-
// First hide→show: 100 → 600 = 500ms hidden.
|
|
122
|
-
t = 100
|
|
123
|
-
doc.visibilityState = 'hidden'
|
|
124
95
|
fire('visibilitychange')
|
|
125
|
-
t = 600
|
|
126
96
|
doc.visibilityState = 'visible'
|
|
97
|
+
doc.hidden = false
|
|
127
98
|
fire('visibilitychange')
|
|
128
|
-
expect(tracker.summary()).toEqual({
|
|
129
|
-
backgroundCount: 1,
|
|
130
|
-
backgroundedMs: 500
|
|
131
|
-
})
|
|
132
99
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
doc.visibilityState = 'hidden'
|
|
136
|
-
fire('visibilitychange')
|
|
137
|
-
t = 1000
|
|
138
|
-
expect(tracker.summary()).toEqual({
|
|
139
|
-
backgroundCount: 2,
|
|
140
|
-
backgroundedMs: 700
|
|
141
|
-
})
|
|
100
|
+
expect(hidden).toEqual([{ handoffId: 'h-1', platform: 'mobile' }])
|
|
101
|
+
expect(visible).toEqual([{ handoffId: 'h-1', platform: 'mobile' }])
|
|
142
102
|
})
|
|
143
103
|
|
|
144
104
|
it('treats a bfcache pageshow ON THE WINDOW as a "returned" signal', () => {
|
|
@@ -177,13 +137,7 @@ describe('trackFocusDuringHandoff', () => {
|
|
|
177
137
|
em.on('pageVisibleDuringHandoff', e => visible.push(e))
|
|
178
138
|
|
|
179
139
|
const { win, fire } = makeFakeWin()
|
|
180
|
-
const
|
|
181
|
-
em,
|
|
182
|
-
'h-2c',
|
|
183
|
-
'mobile',
|
|
184
|
-
undefined,
|
|
185
|
-
win
|
|
186
|
-
)
|
|
140
|
+
const stop = trackFocusDuringHandoff(em, 'h-2c', 'mobile', undefined, win)
|
|
187
141
|
fire('pageshow')
|
|
188
142
|
|
|
189
143
|
expect(visible).toEqual([{ handoffId: 'h-2c', platform: 'mobile' }])
|
|
@@ -196,13 +150,7 @@ describe('trackFocusDuringHandoff', () => {
|
|
|
196
150
|
em.on('pageHiddenDuringHandoff', e => hidden.push(e))
|
|
197
151
|
|
|
198
152
|
const { doc, fire } = makeFakeDoc()
|
|
199
|
-
const
|
|
200
|
-
em,
|
|
201
|
-
'h-2d',
|
|
202
|
-
'desktop',
|
|
203
|
-
doc,
|
|
204
|
-
undefined
|
|
205
|
-
)
|
|
153
|
+
const stop = trackFocusDuringHandoff(em, 'h-2d', 'desktop', doc, undefined)
|
|
206
154
|
doc.visibilityState = 'hidden'
|
|
207
155
|
fire('visibilitychange')
|
|
208
156
|
|
|
@@ -218,7 +166,7 @@ describe('trackFocusDuringHandoff', () => {
|
|
|
218
166
|
|
|
219
167
|
const { doc, fire: fireDoc } = makeFakeDoc()
|
|
220
168
|
const { win, listeners: winListeners, fire: fireWin } = makeFakeWin()
|
|
221
|
-
const
|
|
169
|
+
const stop = trackFocusDuringHandoff(em, 'h-3', 'desktop', doc, win)
|
|
222
170
|
stop()
|
|
223
171
|
|
|
224
172
|
doc.visibilityState = 'hidden'
|
|
@@ -231,7 +179,7 @@ describe('trackFocusDuringHandoff', () => {
|
|
|
231
179
|
|
|
232
180
|
it('is a no-op (no throw) when there is neither document nor window (SSR)', () => {
|
|
233
181
|
const em = new EventManager()
|
|
234
|
-
const
|
|
182
|
+
const stop = trackFocusDuringHandoff(
|
|
235
183
|
em,
|
|
236
184
|
'h-4',
|
|
237
185
|
'unknown',
|
|
@@ -35,52 +35,30 @@ function resolveWindow(): FocusWindowLike | undefined {
|
|
|
35
35
|
return win && typeof win.addEventListener === 'function' ? win : undefined
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
/** Handle returned by `trackFocusDuringHandoff`. */
|
|
39
|
-
export interface FocusHandoffTracker {
|
|
40
|
-
/** Remove the visibility listeners. Always call on settle (no-op if none attached). */
|
|
41
|
-
stop: () => void
|
|
42
|
-
/**
|
|
43
|
-
* Backgrounding summary at call time: how many times the page was hidden and the
|
|
44
|
-
* cumulative hidden duration (ms) during the handoff — including an in-progress
|
|
45
|
-
* hidden segment if the handoff settles while still backgrounded.
|
|
46
|
-
*/
|
|
47
|
-
summary: () => { backgroundCount: number; backgroundedMs: number }
|
|
48
|
-
}
|
|
49
|
-
|
|
50
38
|
/**
|
|
51
39
|
* While a wallet handoff is pending, emit a FACT on every tab-visibility
|
|
52
40
|
* transition — `pageHiddenDuringHandoff` when the page is backgrounded (the user
|
|
53
|
-
* likely left for the wallet app), `pageVisibleDuringHandoff`
|
|
54
|
-
*
|
|
55
|
-
*
|
|
41
|
+
* likely left for the wallet app), `pageVisibleDuringHandoff` when it returns
|
|
42
|
+
* (including a bfcache `pageshow`, which iOS uses when coming back from a wallet).
|
|
43
|
+
*
|
|
44
|
+
* This is the raw signal behind "did the user actually app-switch to the wallet?"
|
|
45
|
+
* for deep-link flows — it does NOT interpret abandonment. The listeners are
|
|
46
|
+
* scoped to the single handoff: attach right after `awaitingWallet`, and the
|
|
47
|
+
* returned teardown (called the instant the op settles) removes them, so the
|
|
48
|
+
* page's visibility is only observed during the blind window. SSR-safe (no-op
|
|
49
|
+
* when there is neither `document` nor `window`).
|
|
56
50
|
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* listeners are scoped to the single handoff: attach right after `awaitingWallet`,
|
|
60
|
-
* and `stop()` (called the instant the op settles) removes them, so the page's
|
|
61
|
-
* visibility is only observed during the blind window. SSR-safe (no-op when there
|
|
62
|
-
* is neither `document` nor `window`). `summary()` lets the handoff stamp the
|
|
63
|
-
* cumulative backgrounding onto its terminal.
|
|
51
|
+
* @returns a teardown that removes the listeners. Always call it (even when
|
|
52
|
+
* nothing was attached — it's a no-op then) to keep the call site symmetric.
|
|
64
53
|
*/
|
|
65
54
|
export function trackFocusDuringHandoff(
|
|
66
55
|
eventManager: EventManager,
|
|
67
56
|
handoffId: string,
|
|
68
57
|
platform: Platform,
|
|
69
58
|
doc: FocusDocumentLike | undefined = resolveDocument(),
|
|
70
|
-
win: FocusWindowLike | undefined = resolveWindow()
|
|
71
|
-
|
|
72
|
-
)
|
|
73
|
-
let backgroundCount = 0
|
|
74
|
-
let totalHiddenMs = 0
|
|
75
|
-
let hiddenSince: number | undefined
|
|
76
|
-
|
|
77
|
-
const summary = (): { backgroundCount: number; backgroundedMs: number } => ({
|
|
78
|
-
backgroundCount,
|
|
79
|
-
backgroundedMs:
|
|
80
|
-
totalHiddenMs + (hiddenSince !== undefined ? now() - hiddenSince : 0)
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
if (!doc && !win) return { stop: () => {}, summary }
|
|
59
|
+
win: FocusWindowLike | undefined = resolveWindow()
|
|
60
|
+
): () => void {
|
|
61
|
+
if (!doc && !win) return () => {}
|
|
84
62
|
|
|
85
63
|
// One physical return can fire BOTH `pageshow` (window) and a
|
|
86
64
|
// `visibilitychange`→visible (document) — an iOS bfcache restore does. Dedupe
|
|
@@ -89,23 +67,12 @@ export function trackFocusDuringHandoff(
|
|
|
89
67
|
const emitState = (state: 'hidden' | 'visible') => {
|
|
90
68
|
if (state === lastState) return
|
|
91
69
|
lastState = state
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
let hiddenMs: number | undefined
|
|
99
|
-
if (hiddenSince !== undefined) {
|
|
100
|
-
hiddenMs = now() - hiddenSince
|
|
101
|
-
totalHiddenMs += hiddenMs
|
|
102
|
-
hiddenSince = undefined
|
|
103
|
-
}
|
|
104
|
-
eventManager.emit('pageVisibleDuringHandoff', {
|
|
105
|
-
handoffId,
|
|
106
|
-
platform,
|
|
107
|
-
...(hiddenMs !== undefined ? { hiddenMs } : {})
|
|
108
|
-
})
|
|
70
|
+
eventManager.emit(
|
|
71
|
+
state === 'hidden'
|
|
72
|
+
? 'pageHiddenDuringHandoff'
|
|
73
|
+
: 'pageVisibleDuringHandoff',
|
|
74
|
+
{ handoffId, platform }
|
|
75
|
+
)
|
|
109
76
|
}
|
|
110
77
|
|
|
111
78
|
const onVisibility = () => {
|
|
@@ -122,11 +89,8 @@ export function trackFocusDuringHandoff(
|
|
|
122
89
|
doc?.addEventListener('visibilitychange', onVisibility)
|
|
123
90
|
win?.addEventListener('pageshow', onPageShow)
|
|
124
91
|
|
|
125
|
-
return {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
win?.removeEventListener('pageshow', onPageShow)
|
|
129
|
-
},
|
|
130
|
-
summary
|
|
92
|
+
return () => {
|
|
93
|
+
doc?.removeEventListener('visibilitychange', onVisibility)
|
|
94
|
+
win?.removeEventListener('pageshow', onPageShow)
|
|
131
95
|
}
|
|
132
96
|
}
|
|
@@ -47,48 +47,6 @@ describe('instrumentHandoff', () => {
|
|
|
47
47
|
expect(end.timedOut).toBe(false)
|
|
48
48
|
})
|
|
49
49
|
|
|
50
|
-
it('counts relay-socket drops seen during the handoff onto the terminal', async () => {
|
|
51
|
-
const em = new EventManager()
|
|
52
|
-
const seen = record(em)
|
|
53
|
-
const wcCtx: HandoffContext = { ...CTX, connectionMode: 'walletConnect' }
|
|
54
|
-
await instrumentHandoff(
|
|
55
|
-
{ eventManager: em, timeoutMs: 0 },
|
|
56
|
-
wcCtx,
|
|
57
|
-
async () => {
|
|
58
|
-
// The relay socket drops (and recovers) while the sign request is pending.
|
|
59
|
-
em.emit('relayDisconnected', {})
|
|
60
|
-
em.emit('relayReconnected', undefined)
|
|
61
|
-
em.emit('relayDisconnected', {})
|
|
62
|
-
return 'sig'
|
|
63
|
-
}
|
|
64
|
-
)
|
|
65
|
-
const end = seen.find(s => s.name === 'walletSucceeded')?.data as {
|
|
66
|
-
relayDropCount: number
|
|
67
|
-
backgroundCount: number
|
|
68
|
-
backgroundedMs: number
|
|
69
|
-
}
|
|
70
|
-
expect(end.relayDropCount).toBe(2)
|
|
71
|
-
// No backgrounding occurred in this test runtime.
|
|
72
|
-
expect(end.backgroundCount).toBe(0)
|
|
73
|
-
expect(end.backgroundedMs).toBe(0)
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
it('stops counting relay drops once the handoff settles', async () => {
|
|
77
|
-
const em = new EventManager()
|
|
78
|
-
const seen = record(em)
|
|
79
|
-
await instrumentHandoff(
|
|
80
|
-
{ eventManager: em, timeoutMs: 0 },
|
|
81
|
-
CTX,
|
|
82
|
-
async () => 'sig'
|
|
83
|
-
)
|
|
84
|
-
// A drop after settle must be ignored (listener removed) and never throw.
|
|
85
|
-
expect(() => em.emit('relayDisconnected', {})).not.toThrow()
|
|
86
|
-
const end = seen.find(s => s.name === 'walletSucceeded')?.data as {
|
|
87
|
-
relayDropCount: number
|
|
88
|
-
}
|
|
89
|
-
expect(end.relayDropCount).toBe(0)
|
|
90
|
-
})
|
|
91
|
-
|
|
92
50
|
it('carries targetAlreadyActive through awaitingWallet and the terminal', async () => {
|
|
93
51
|
const em = new EventManager()
|
|
94
52
|
const seen = record(em)
|
|
@@ -141,22 +141,7 @@ export async function instrumentHandoff<T>(
|
|
|
141
141
|
// Observe page-visibility transitions ONLY while this handoff is pending — the
|
|
142
142
|
// app-switch signal for deep-link flows (left for the wallet / came back). The
|
|
143
143
|
// teardown in `finally` removes the listeners the instant the op settles.
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
// Count relay-socket drops seen while this handoff is pending — the WC relay
|
|
147
|
-
// emits `relayDisconnected` on the same bus. Lets a terminal say "recovered
|
|
148
|
-
// after N drops" (the 88s suspend→reconnect case) without a cross-event join.
|
|
149
|
-
let relayDropCount = 0
|
|
150
|
-
const offRelayDrop = eventManager.on('relayDisconnected', () => {
|
|
151
|
-
relayDropCount++
|
|
152
|
-
})
|
|
153
|
-
|
|
154
|
-
// Backgrounding + relay-drop summary, snapshotted onto each terminal so ONE
|
|
155
|
-
// event answers "backgrounded? how long? socket dropped? recovered?".
|
|
156
|
-
const handoffMetrics = () => {
|
|
157
|
-
const { backgroundCount, backgroundedMs } = focus.summary()
|
|
158
|
-
return { backgroundedMs, backgroundCount, relayDropCount }
|
|
159
|
-
}
|
|
144
|
+
const stopFocus = trackFocusDuringHandoff(eventManager, handoffId, platform)
|
|
160
145
|
|
|
161
146
|
// Same routing facets as `awaitingWallet` so every terminal is self-describing
|
|
162
147
|
// (no handoffId self-join needed to know which wallet / chain / mode it closed).
|
|
@@ -179,8 +164,7 @@ export async function instrumentHandoff<T>(
|
|
|
179
164
|
operation: context.operation,
|
|
180
165
|
durationMs: now() - start,
|
|
181
166
|
timeoutMs,
|
|
182
|
-
...routing
|
|
183
|
-
...handoffMetrics()
|
|
167
|
+
...routing
|
|
184
168
|
})
|
|
185
169
|
}, timeoutMs)
|
|
186
170
|
: undefined
|
|
@@ -193,8 +177,7 @@ export async function instrumentHandoff<T>(
|
|
|
193
177
|
durationMs: now() - start,
|
|
194
178
|
timedOut,
|
|
195
179
|
timeoutMs: timedOut ? timeoutMs : undefined,
|
|
196
|
-
...routing
|
|
197
|
-
...handoffMetrics()
|
|
180
|
+
...routing
|
|
198
181
|
})
|
|
199
182
|
return result
|
|
200
183
|
} catch (error) {
|
|
@@ -224,8 +207,7 @@ export async function instrumentHandoff<T>(
|
|
|
224
207
|
errorCode,
|
|
225
208
|
failureKind,
|
|
226
209
|
source,
|
|
227
|
-
...routing
|
|
228
|
-
...handoffMetrics()
|
|
210
|
+
...routing
|
|
229
211
|
})
|
|
230
212
|
} else {
|
|
231
213
|
eventManager.emit('walletFailed', {
|
|
@@ -238,14 +220,12 @@ export async function instrumentHandoff<T>(
|
|
|
238
220
|
errorCode,
|
|
239
221
|
failureKind,
|
|
240
222
|
source,
|
|
241
|
-
...routing
|
|
242
|
-
...handoffMetrics()
|
|
223
|
+
...routing
|
|
243
224
|
})
|
|
244
225
|
}
|
|
245
226
|
throw error
|
|
246
227
|
} finally {
|
|
247
228
|
if (timer) clearTimeout(timer)
|
|
248
|
-
|
|
249
|
-
focus.stop()
|
|
229
|
+
stopFocus()
|
|
250
230
|
}
|
|
251
231
|
}
|
|
@@ -55,6 +55,82 @@ describe('attachLifecycle', () => {
|
|
|
55
55
|
expect(got.walletSessionExpired).toEqual([routing])
|
|
56
56
|
})
|
|
57
57
|
|
|
58
|
+
it('forwards real accounts from subscribeAccounts to onAccountsChanged (a separate stream from telemetry)', () => {
|
|
59
|
+
const em = new EventManager()
|
|
60
|
+
let emitAccounts!: (accounts: string[]) => void
|
|
61
|
+
const seen: string[][] = []
|
|
62
|
+
|
|
63
|
+
attachLifecycle({
|
|
64
|
+
eventManager: em,
|
|
65
|
+
subscribe: () => () => {},
|
|
66
|
+
subscribeAccounts: l => {
|
|
67
|
+
emitAccounts = l
|
|
68
|
+
return () => {}
|
|
69
|
+
},
|
|
70
|
+
onAccountsChanged: accounts => seen.push(accounts),
|
|
71
|
+
getRouting: () => routing,
|
|
72
|
+
connectionMode: 'injected',
|
|
73
|
+
namespace: 'eip155'
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
emitAccounts(['0xNEW'])
|
|
77
|
+
expect(seen).toEqual([['0xNEW']])
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('attaches subscribeAccounts even when the connector has NO telemetry subscribe (the Tron shape today)', () => {
|
|
81
|
+
// TronConnector implements onAccountsChanged but not onLifecycleEvent —
|
|
82
|
+
// the two streams must be independently attachable.
|
|
83
|
+
const em = new EventManager()
|
|
84
|
+
const unavail: unknown[] = []
|
|
85
|
+
em.on('lifecycleTelemetryUnavailable', d => unavail.push(d))
|
|
86
|
+
let emitAccounts!: (accounts: string[]) => void
|
|
87
|
+
const seen: string[][] = []
|
|
88
|
+
|
|
89
|
+
attachLifecycle({
|
|
90
|
+
eventManager: em,
|
|
91
|
+
subscribe: undefined,
|
|
92
|
+
subscribeAccounts: l => {
|
|
93
|
+
emitAccounts = l
|
|
94
|
+
return () => {}
|
|
95
|
+
},
|
|
96
|
+
onAccountsChanged: accounts => seen.push(accounts),
|
|
97
|
+
getRouting: () => routing,
|
|
98
|
+
connectionMode: 'injected',
|
|
99
|
+
namespace: 'tron'
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
emitAccounts(['TSwitched'])
|
|
103
|
+
expect(seen).toEqual([['TSwitched']])
|
|
104
|
+
// No telemetry surface exists for this connector, but that's a SEPARATE
|
|
105
|
+
// concern from account-sync succeeding — no unavailable signal fires
|
|
106
|
+
// just because the account stream alone is present.
|
|
107
|
+
expect(unavail).toHaveLength(0)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('teardown unsubscribes both streams', () => {
|
|
111
|
+
const em = new EventManager()
|
|
112
|
+
let lifecycleUnsubbed = false
|
|
113
|
+
let accountsUnsubbed = false
|
|
114
|
+
|
|
115
|
+
const teardown = attachLifecycle({
|
|
116
|
+
eventManager: em,
|
|
117
|
+
subscribe: () => () => {
|
|
118
|
+
lifecycleUnsubbed = true
|
|
119
|
+
},
|
|
120
|
+
subscribeAccounts: () => () => {
|
|
121
|
+
accountsUnsubbed = true
|
|
122
|
+
},
|
|
123
|
+
onAccountsChanged: () => {},
|
|
124
|
+
getRouting: () => routing,
|
|
125
|
+
connectionMode: 'injected',
|
|
126
|
+
namespace: 'eip155'
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
teardown()
|
|
130
|
+
expect(lifecycleUnsubbed).toBe(true)
|
|
131
|
+
expect(accountsUnsubbed).toBe(true)
|
|
132
|
+
})
|
|
133
|
+
|
|
58
134
|
it('a throwing getRouting never escapes into the wallet emitter dispatch', () => {
|
|
59
135
|
// Wallet SDKs dispatch listeners unguarded (e.g. TonConnect's bare
|
|
60
136
|
// forEach) — a throw from our listener would abort delivery to the
|
|
@@ -152,6 +228,91 @@ describe('attachLifecycle', () => {
|
|
|
152
228
|
expect(seen).toHaveLength(1)
|
|
153
229
|
})
|
|
154
230
|
|
|
231
|
+
it('suppresses an accountsChanged echo that matches the address a UWC switch just applied', () => {
|
|
232
|
+
let t = 1000
|
|
233
|
+
const em = new EventManager()
|
|
234
|
+
const seen: string[][] = []
|
|
235
|
+
let emitAccounts!: (accounts: string[]) => void
|
|
236
|
+
|
|
237
|
+
attachLifecycle({
|
|
238
|
+
eventManager: em,
|
|
239
|
+
subscribe: () => () => {},
|
|
240
|
+
subscribeAccounts: l => {
|
|
241
|
+
emitAccounts = l
|
|
242
|
+
return () => {}
|
|
243
|
+
},
|
|
244
|
+
onAccountsChanged: accounts => seen.push(accounts),
|
|
245
|
+
getRouting: () => routing,
|
|
246
|
+
connectionMode: 'injected',
|
|
247
|
+
namespace: 'eip155',
|
|
248
|
+
now: () => t
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
em.emit('networkSwitched', {
|
|
252
|
+
network: { id: 'eip155:8453' } as any,
|
|
253
|
+
address: '0xNEW'
|
|
254
|
+
})
|
|
255
|
+
emitAccounts(['0xNEW'])
|
|
256
|
+
expect(seen).toHaveLength(0)
|
|
257
|
+
|
|
258
|
+
// Past the echo window, the same address is a genuine wallet-side switch.
|
|
259
|
+
t = 1000 + 5000
|
|
260
|
+
emitAccounts(['0xNEW'])
|
|
261
|
+
expect(seen).toHaveLength(1)
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
it('forwards an accountsChanged inside the echo window whose address does NOT match the switch (a real, different, user-initiated change)', () => {
|
|
265
|
+
const em = new EventManager()
|
|
266
|
+
const seen: string[][] = []
|
|
267
|
+
let emitAccounts!: (accounts: string[]) => void
|
|
268
|
+
|
|
269
|
+
attachLifecycle({
|
|
270
|
+
eventManager: em,
|
|
271
|
+
subscribe: () => () => {},
|
|
272
|
+
subscribeAccounts: l => {
|
|
273
|
+
emitAccounts = l
|
|
274
|
+
return () => {}
|
|
275
|
+
},
|
|
276
|
+
onAccountsChanged: accounts => seen.push(accounts),
|
|
277
|
+
getRouting: () => routing,
|
|
278
|
+
connectionMode: 'injected',
|
|
279
|
+
namespace: 'eip155'
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
em.emit('networkSwitched', {
|
|
283
|
+
network: { id: 'eip155:8453' } as any,
|
|
284
|
+
address: '0xNEW'
|
|
285
|
+
})
|
|
286
|
+
emitAccounts(['0xSOMETHING_ELSE'])
|
|
287
|
+
expect(seen).toEqual([['0xSOMETHING_ELSE']])
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
it('never suppresses an empty accounts list (the disconnect signal), even inside the echo window', () => {
|
|
291
|
+
const em = new EventManager()
|
|
292
|
+
const seen: string[][] = []
|
|
293
|
+
let emitAccounts!: (accounts: string[]) => void
|
|
294
|
+
|
|
295
|
+
attachLifecycle({
|
|
296
|
+
eventManager: em,
|
|
297
|
+
subscribe: () => () => {},
|
|
298
|
+
subscribeAccounts: l => {
|
|
299
|
+
emitAccounts = l
|
|
300
|
+
return () => {}
|
|
301
|
+
},
|
|
302
|
+
onAccountsChanged: accounts => seen.push(accounts),
|
|
303
|
+
getRouting: () => routing,
|
|
304
|
+
connectionMode: 'injected',
|
|
305
|
+
namespace: 'eip155'
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
em.emit('networkSwitched', {
|
|
309
|
+
network: { id: 'eip155:8453' } as any,
|
|
310
|
+
address: '0xNEW'
|
|
311
|
+
})
|
|
312
|
+
emitAccounts([])
|
|
313
|
+
expect(seen).toEqual([[]])
|
|
314
|
+
})
|
|
315
|
+
|
|
155
316
|
it('caps lifecycle emissions per attachment (a storming provider cannot drive unbounded egress)', () => {
|
|
156
317
|
// Every other telemetry sink added by this layer is bounded (relay / bridge /
|
|
157
318
|
// observer-error all cap at 100); a wallet looping chainChanged must not be
|
|
@@ -192,6 +353,42 @@ describe('attachLifecycle', () => {
|
|
|
192
353
|
expect(unavail[0]?.reason).toBe('emission_cap')
|
|
193
354
|
})
|
|
194
355
|
|
|
356
|
+
it('does not drop a still-pending echo suppression when a SECOND switchNetwork arms before the FIRST wallet echo arrives', () => {
|
|
357
|
+
// Two rapid, back-to-back switches (A then B) each arm their OWN echo
|
|
358
|
+
// window. A's wallet echo can legitimately arrive AFTER B's switch has
|
|
359
|
+
// already completed — B arming/re-arming must not erase A's still-valid,
|
|
360
|
+
// not-yet-consumed suppression window.
|
|
361
|
+
let t = 1000
|
|
362
|
+
const em = new EventManager()
|
|
363
|
+
const seen: unknown[] = []
|
|
364
|
+
em.on('walletChainChanged', d => seen.push(d))
|
|
365
|
+
|
|
366
|
+
let emit!: (e: WalletLifecycleEvent) => void
|
|
367
|
+
attachLifecycle({
|
|
368
|
+
eventManager: em,
|
|
369
|
+
subscribe: l => {
|
|
370
|
+
emit = l
|
|
371
|
+
return () => {}
|
|
372
|
+
},
|
|
373
|
+
getRouting: () => routing,
|
|
374
|
+
connectionMode: 'injected',
|
|
375
|
+
namespace: 'eip155',
|
|
376
|
+
now: () => t
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
em.emit('networkSwitched', switchedTo('eip155:8453')) // switch to A
|
|
380
|
+
t = 1010
|
|
381
|
+
em.emit('networkSwitched', switchedTo('eip155:137')) // switch to B, A's echo still pending
|
|
382
|
+
|
|
383
|
+
t = 1020
|
|
384
|
+
emit({ type: 'chainChanged', chainId: 'eip155:8453' }) // A's delayed echo
|
|
385
|
+
expect(seen).toHaveLength(0)
|
|
386
|
+
|
|
387
|
+
t = 1030
|
|
388
|
+
emit({ type: 'chainChanged', chainId: 'eip155:137' }) // B's delayed echo
|
|
389
|
+
expect(seen).toHaveLength(0)
|
|
390
|
+
})
|
|
391
|
+
|
|
195
392
|
it('seeds echo suppression from initialSwitch (the reattach-after-switch path)', () => {
|
|
196
393
|
// A cross-namespace switchNetwork re-attaches the subscription AFTER the
|
|
197
394
|
// switch completed — the fresh closure never saw that switch's bus events,
|