Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,26 @@
// @ts-nocheck -- mechanically split from OrcaRuntimeService; behavior is covered by AST equivalence and characterization tests.
import { OrcaRuntimeWithCollectMobileVisibleGraphChangedWorktrees } from './orca-runtime-collect-mobile-visible-graph-changed-worktrees'

// Why bounded: an old client is withheld until the renderer publishes (it would prune tabs from
// an unpublished list), but a host whose graph never publishes must refuse, not hang the call.
export const SESSION_TABS_INVENTORY_PUBLICATION_WAIT_MS = 30_000

export class OrcaRuntimeWithWaitForSessionTabsInventoryPublication extends OrcaRuntimeWithCollectMobileVisibleGraphChangedWorktrees {
protected waitForSessionTabsInventoryPublication(signal?: AbortSignal): Promise<void> {
protected waitForSessionTabsInventoryPublication(
signal?: AbortSignal,
timeoutMs = SESSION_TABS_INVENTORY_PUBLICATION_WAIT_MS
): Promise<void> {
if (this.getAuthoritativeSessionTabsInventoryEpoch() !== null) {
return Promise.resolve()
}
return new Promise<void>((resolve, reject) => {
const timer = setTimeout(() => {
cleanup()
reject(new Error('runtime_unavailable'))
}, timeoutMs)
timer.unref?.()
const cleanup = (): void => {
clearTimeout(timer)
this.sessionTabsInventoryWaiters.delete(onPublished)
signal?.removeEventListener('abort', onAbort)
}
Expand Down
47 changes: 47 additions & 0 deletions src/main/runtime/session-tabs-inventory-publication.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { describe, expect, it, vi } from 'vitest'
import { OrcaRuntimeService } from './orca-runtime'
import { SESSION_TABS_INVENTORY_PUBLICATION_WAIT_MS } from './orca-runtime-wait-for-session-tabs-inventory-publication'
import { RpcDispatcher } from './rpc/dispatcher'
import { SESSION_TAB_METHODS } from './rpc/methods/session-tabs'

type InventoryInternals = {
sessionTabsInventoryWaiters: Set<() => void>
Expand Down Expand Up @@ -368,6 +371,50 @@ describe('authoritative session tab inventory publication', () => {
expect((runtime as unknown as InventoryInternals).sessionTabsInventoryWaiters.size).toBe(0)
})

it('refuses with runtime_unavailable instead of hanging when no graph ever publishes', async () => {
vi.useFakeTimers()
try {
const runtime = createInventoryRuntime()
runtime.attachWindow(1)
const pending = runtime.listAllMobileSessionTabsInventory()
const settled = pending.catch((error: unknown) => error)
await waitForInventoryWaiter(runtime)

await vi.advanceTimersByTimeAsync(SESSION_TABS_INVENTORY_PUBLICATION_WAIT_MS)

expect(await settled).toEqual(new Error('runtime_unavailable'))
expect((runtime as unknown as InventoryInternals).sessionTabsInventoryWaiters.size).toBe(0)
} finally {
vi.useRealTimers()
}
})

it('answers an old client over RPC with runtime_unavailable when no graph publishes', async () => {
vi.useFakeTimers()
try {
const runtime = createInventoryRuntime()
runtime.attachWindow(1)
const dispatcher = new RpcDispatcher({ runtime, methods: SESSION_TAB_METHODS })
const messages: string[] = []
// No clientCapabilities: the pre-capability client shape.
const pending = dispatcher.dispatchStreaming(
{ id: 'req-1', authToken: 'tok', method: 'session.tabs.listAll' },
(message) => messages.push(message),
{ clientKind: 'runtime' }
)
await waitForInventoryWaiter(runtime)
await vi.advanceTimersByTimeAsync(SESSION_TABS_INVENTORY_PUBLICATION_WAIT_MS)
await pending

expect(JSON.parse(messages[0]!)).toMatchObject({
ok: false,
error: { code: 'runtime_unavailable' }
})
} finally {
vi.useRealTimers()
}
})

it('does not scan when the inventory request is already cancelled', async () => {
const runtime = createInventoryRuntime()
const collect = vi.spyOn(
Expand Down
Loading