fix(mobile): unsubscribe session tabs by request on the direct connection (#22943)

* fix(mobile): unsubscribe session tabs by request on the direct connection

* fix(mobile): hold a direct session tabs unsubscribe until the first snapshot

The desktop registers a tab-list stream only as it emits the first snapshot. A direct
unsubscribe sent before that found nothing, and with per-request addressing no later
worktree-wide sweep collects the late stream, so it kept its desktop listener until the
socket closed. Hold the unsubscribe until the snapshot arrives, as the relay connection does.

* test(mobile): cover a held session tabs unsubscribe whose subscribe fails

* fix(mobile): keep the session tabs hold within the registry line budget after merging main

Move the pre-snapshot hold into the session tabs stream module, note that only older hosts need it, and give the unsubscribe test the real registration version now that a worktree-wide unsubscribe spares later streams.
This commit is contained in:
Brennan Benson
2026-09-28 21:15:39 -07:00
committed by GitHub
parent 2ae7c00e84
commit e899809ff8
6 changed files with 211 additions and 6 deletions
@@ -1,5 +1,6 @@
import { describe, expect, it, vi } from 'vitest'
import type { OrcaRuntimeService } from '../../orca-runtime'
import { RuntimeSubscriptionRegistry } from '../../runtime-subscription-registry'
import { RpcDispatcher } from '../dispatcher'
import { SESSION_TAB_METHODS } from './session-tabs'
@@ -61,11 +62,61 @@ describe('session tab unsubscribe RPC methods', () => {
expect(cleanupSubscription).toHaveBeenCalledWith('session.tabs:conn-1:*:sub-all-1')
expect(cleanupSubscriptionsByPrefix).not.toHaveBeenCalled()
})
it('ends only the named stream when a newer one watches the same worktree', async () => {
const { dispatcher, ends } = await subscribeTwiceToOneWorktree()
await dispatcher.dispatchStreaming(
request('session.tabs.unsubscribe', { worktree: 'id:wt-1', subscriptionId: 'sub-old' }),
vi.fn(),
{ connectionId: 'conn-1' }
)
await vi.waitFor(() => expect(ends['sub-old']).toBe(1))
expect(ends['sub-new']).toBe(0)
})
it('ends every stream for the worktree when the unsubscribe names no request', async () => {
const { dispatcher, ends } = await subscribeTwiceToOneWorktree()
await dispatcher.dispatchStreaming(
request('session.tabs.unsubscribe', { worktree: 'id:wt-1' }),
vi.fn(),
{ connectionId: 'conn-1' }
)
await vi.waitFor(() => expect(ends).toEqual({ 'sub-old': 1, 'sub-new': 1 }))
})
})
async function subscribeTwiceToOneWorktree() {
const registry = new RuntimeSubscriptionRegistry()
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Partial runtime backed by the real subscription registry; it supplies every member session.tabs subscribe/unsubscribe call.
const runtime = {
...runtimeWithCleanup(registry.cleanup.bind(registry), registry.cleanupByPrefix.bind(registry)),
registerSubscriptionCleanup: registry.register.bind(registry),
getSubscriptionRegistrationVersion: registry.getRegistrationVersion.bind(registry),
onMobileSessionTabsChanged: () => () => {}
} as unknown as OrcaRuntimeService
const dispatcher = new RpcDispatcher({ runtime, methods: SESSION_TAB_METHODS })
const ends: Record<string, number> = { 'sub-old': 0, 'sub-new': 0 }
for (const id of ['sub-old', 'sub-new']) {
await dispatcher.dispatchStreaming(
{ ...request('session.tabs.subscribe', { worktree: 'id:wt-1' }), id },
(message) => {
if (JSON.parse(message).result?.type === 'end') {
ends[id]! += 1
}
},
{ connectionId: 'conn-1' }
)
}
return { dispatcher, ends }
}
function runtimeWithCleanup(
cleanupSubscription: ReturnType<typeof vi.fn>,
cleanupSubscriptionsByPrefix = vi.fn()
cleanupSubscription: (id: string) => void,
cleanupSubscriptionsByPrefix: (prefix: string, throughVersion?: number) => void = vi.fn()
): OrcaRuntimeService {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the unsubscribe methods reach only these members; a missing one throws and fails the test.
return {