Skip to content

Commit 4559790

Browse files
committed
fix(hub-ui): scope action cleanup to its owning connection
Track the active inspected-page action across sessions and release ownership when page selection changes. Ignore stale deactivation and disconnect requests while preserving serialized teardown for in-flight activations.
1 parent ed7e957 commit 4559790

2 files changed

Lines changed: 56 additions & 13 deletions

File tree

‎packages/hub-ui/src/client/state/inspected-page.test.ts‎

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ function setup(options: { adapter?: boolean, activate?: (id: string) => Promise<
3434
// The browser adapter binds this port to its known viewer and inspected
3535
// document. Production adapter identity checks live in packages/webext.
3636
if (event.source !== viewer.window || event.origin !== viewer.win.location.origin
37-
|| event.data?.session !== 'session-a') {
37+
|| event.data?.session !== new URLSearchParams(viewer.win.location.search).get('devframe-inspected-page')) {
3838
return
3939
}
4040
page.win.dispatch('message', {
@@ -69,14 +69,16 @@ describe('inspected page bridge', () => {
6969
listener(null)
7070
await vi.waitFor(() => expect(changed).toHaveBeenCalledExactlyOnceWith(null))
7171
unsubscribe()
72-
await expect(target.deactivate('tracer')).resolves.toBe(true)
73-
expect(s.host.deactivate).toHaveBeenCalledExactlyOnceWith('tracer')
72+
await expect(target.deactivate('tracer')).resolves.toBe(false)
73+
expect(s.host.deactivate).not.toHaveBeenCalled()
7474
})
7575

7676
it('carries an existing panel channel through the dedicated bridge port', async () => {
7777
interface Protocol extends InPageChannelProtocol {
78-
pageScript: { route: () => string }
79-
panel: Record<string, never>
78+
functions: {
79+
pageScript: { route: () => string }
80+
panel: Record<string, never>
81+
}
8082
}
8183
const s = setup()
8284
const pageScript = createPageScriptChannel<Protocol>({
@@ -169,6 +171,39 @@ describe('inspected page bridge', () => {
169171
expect(s.host.deactivate).toHaveBeenCalledExactlyOnceWith('tracer')
170172
})
171173

174+
it.each(['disconnect', 'deactivate'])('preserves a newer session activation when the old session requests %s', async (method) => {
175+
const s = setup()
176+
const first = await connect(s)
177+
await first.activate('tracer')
178+
s.viewer.win.location.search = s.viewer.win.location.search.replace('session-a', 'session-b')
179+
const second = await connect(s)
180+
await second.activate('tracer')
181+
182+
if (method === 'disconnect')
183+
first.close()
184+
else
185+
await expect(first.deactivate('tracer')).resolves.toBe(false)
186+
// A later request is a barrier for the host's shared operation queue.
187+
await second.prepare('a11y')
188+
expect(s.host.deactivate).not.toHaveBeenCalled()
189+
second.close()
190+
await vi.waitFor(() => expect(s.host.deactivate).toHaveBeenCalledExactlyOnceWith('tracer'))
191+
})
192+
193+
it('releases ownership when inspection ends before an embedded activation', async () => {
194+
const s = setup()
195+
const target = await connect(s)
196+
await target.activate('tracer')
197+
for (const id of [null, 'tracer']) {
198+
for (const listener of s.selectionListeners)
199+
listener(id)
200+
}
201+
target.close()
202+
const replacement = await connect(s)
203+
await replacement.prepare('a11y')
204+
expect(s.host.deactivate).not.toHaveBeenCalled()
205+
})
206+
172207
it('closes a timed-out activation and tears down when the pending action finishes', async () => {
173208
let finish!: (value: boolean) => void
174209
let started!: () => void

‎packages/hub-ui/src/client/state/inspected-page.ts‎

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ function channelTransport(port: MessagePort) {
4444
*/
4545
export function installInspectedPageHost(host: InspectedPageHost, win: Window = window): () => void {
4646
const sessions = new Map<string, () => void>()
47+
let activeAction: { port: MessagePort, entryId: string } | undefined
4748
// Replacement connections share the action queue, including teardown. An
4849
// older pending activation must finish and deactivate before its successor.
4950
let operations = Promise.resolve()
@@ -55,10 +56,19 @@ export function installInspectedPageHost(host: InspectedPageHost, win: Window =
5556
return
5657
sessions.get(data.session)?.()
5758
const port = event.ports[0]
58-
let activeEntry: string | undefined
5959
let closed = false
6060
const stopRelay = createInPageChannelRelay({ role: 'page', window: win, transport: channelTransport(port) })
61-
const stopSelection = host.onSelection(entryId => port.postMessage({ type: 'selection', entryId }))
61+
const stopSelection = host.onSelection((entryId) => {
62+
if (activeAction && activeAction.entryId !== entryId)
63+
activeAction = undefined
64+
port.postMessage({ type: 'selection', entryId })
65+
})
66+
async function deactivate(entryId: string): Promise<boolean> {
67+
if (activeAction?.port !== port || activeAction.entryId !== entryId)
68+
return false
69+
activeAction = undefined
70+
return host.deactivate(entryId)
71+
}
6272
function close() {
6373
if (closed)
6474
return
@@ -72,8 +82,8 @@ export function installInspectedPageHost(host: InspectedPageHost, win: Window =
7282
// Queue teardown after in-flight activation so closing the extension
7383
// cannot leave an inspector enabled after its asynchronous script loads.
7484
operations = operations.then(async () => {
75-
if (activeEntry)
76-
await host.deactivate(activeEntry)
85+
if (activeAction?.port === port)
86+
await deactivate(activeAction.entryId)
7787
}).catch(() => {})
7888
}
7989
function receive(message: MessageEvent) {
@@ -91,11 +101,9 @@ export function installInspectedPageHost(host: InspectedPageHost, win: Window =
91101
if (closed)
92102
return
93103
try {
94-
const result = await host[method](request.entryId)
104+
const result = await (method === 'deactivate' ? deactivate(request.entryId) : host[method](request.entryId))
95105
if (method === 'activate' && result)
96-
activeEntry = request.entryId
97-
if (method === 'deactivate' && activeEntry === request.entryId)
98-
activeEntry = undefined
106+
activeAction = { port, entryId: request.entryId }
99107
if (!closed)
100108
port.postMessage({ type: 'response', id: request.id, result })
101109
}

0 commit comments

Comments
 (0)