mirror of
https://github.com/stablyai/orca.git
synced 2026-11-01 09:37:33 +00:00
* fix(browser): retry a screencast frame a viewer's socket refused An idle page sends no next frame, so a refused last frame left the viewer on a stale picture. The desktop's relay leg to a server uses legacy E2EE, which drops on a full socket where a direct v2 phone queues; retrying the newest pending frame restores parity. Retries never count toward ghost eviction. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(e2ee): queue legacy binary replies with text instead of dropping them Legacy E2EE (every direct phone, the desktop's server link) sent binary straight to the socket and silently dropped it once the outbound budget was full, so terminals lost output and binary could overtake queued text. Binary now shares the ordered text queue and closes on overflow like v2; the lossy screencast opts into dropWhenBacklogged and its retry delivers the newest frame after a drain. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(e2ee): keep a backlogged screencast viewer instead of evicting it as a ghost A lossy frame skipped on a busy socket returned false like a dead socket, so a long backlog evicted a live viewer. Skips now return 'backlogged', which retries without counting toward eviction; legacy judges idleness on the sealed size before encrypting, so skipped frames cost no encryption. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(browser): expect the ready gate to forward screencast send options The pre-ready gate now passes the per-frame send options through to the socket sender, so the replayed startup frame arrives with an options slot. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(browser): share the streamed-guest double across screencast tests Main's renderer throttle lease now calls setBackgroundThrottling on the guest, so the bare { isDestroyed } guest in two branch tests threw. One shared double keeps all three screencast suites on the shape the lease needs. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
177 lines
5.4 KiB
TypeScript
177 lines
5.4 KiB
TypeScript
import type { BrowserScreencastResult } from '../../shared/runtime-types'
|
|
import type { RuntimeBrowserCommands, RuntimeBrowserCommandHost } from './orca-runtime-browser'
|
|
import { RuntimeEmulatorCommands } from './orca-runtime-emulator'
|
|
import { RuntimeBrowserScreencastController } from './runtime-browser-screencast-controller'
|
|
import { createRuntimeBrowserCommands } from './runtime-browser-commands-factory'
|
|
import { RuntimeJiraCommands } from './runtime-jira-commands'
|
|
import type { RpcBinarySender } from './rpc/rpc-binary-sender'
|
|
|
|
type PublicMethods<T> = Pick<T, keyof T>
|
|
type BrowserSurface = Omit<PublicMethods<RuntimeBrowserCommands>, 'browserScreencast'> & {
|
|
browserScreencast(
|
|
params: Parameters<RuntimeBrowserCommands['browserScreencast']>[0],
|
|
options: {
|
|
connectionId?: string
|
|
pairedDeviceId?: string
|
|
clientKind?: 'mobile' | 'runtime'
|
|
sendBinary?: RpcBinarySender
|
|
signal?: AbortSignal
|
|
emit: (result: BrowserScreencastResult) => void
|
|
}
|
|
): Promise<void>
|
|
}
|
|
|
|
export type RuntimeEdgeCommandSurface = BrowserSurface &
|
|
PublicMethods<RuntimeJiraCommands> &
|
|
PublicMethods<RuntimeEmulatorCommands>
|
|
|
|
type ScreencastDependencies = ConstructorParameters<typeof RuntimeBrowserScreencastController>[0]
|
|
type EmulatorHost = ConstructorParameters<typeof RuntimeEmulatorCommands>[0]
|
|
|
|
const BROWSER_COMMAND_NAMES = [
|
|
'browserSnapshot',
|
|
'browserClick',
|
|
'browserGoto',
|
|
'browserFill',
|
|
'browserType',
|
|
'browserSelect',
|
|
'browserScroll',
|
|
'browserBack',
|
|
'browserReload',
|
|
'browserScreenshot',
|
|
'browserScreencast',
|
|
'browserEval',
|
|
'browserTabList',
|
|
'browserProceedCertificate',
|
|
'browserTabShow',
|
|
'browserTabCurrent',
|
|
'browserTabSwitch',
|
|
'browserHover',
|
|
'browserDrag',
|
|
'browserUpload',
|
|
'browserWait',
|
|
'browserCheck',
|
|
'browserFocus',
|
|
'browserClear',
|
|
'browserSelectAll',
|
|
'browserKeypress',
|
|
'browserPdf',
|
|
'browserFullScreenshot',
|
|
'browserCookieGet',
|
|
'browserCookieSet',
|
|
'browserCookieDelete',
|
|
'browserSetViewport',
|
|
'browserSetGeolocation',
|
|
'browserInterceptEnable',
|
|
'browserInterceptDisable',
|
|
'browserInterceptList',
|
|
'browserCaptureStart',
|
|
'browserCaptureStop',
|
|
'browserConsoleLog',
|
|
'browserNetworkLog',
|
|
'browserDblclick',
|
|
'browserForward',
|
|
'browserScrollIntoView',
|
|
'browserGet',
|
|
'browserIs',
|
|
'browserKeyboardInsertText',
|
|
'browserMouseMove',
|
|
'browserMouseDown',
|
|
'browserMouseClick',
|
|
'browserMouseUp',
|
|
'browserMouseWheel',
|
|
'browserFind',
|
|
'browserSetDevice',
|
|
'browserSetOffline',
|
|
'browserSetHeaders',
|
|
'browserSetCredentials',
|
|
'browserSetMedia',
|
|
'browserClipboardRead',
|
|
'browserClipboardWrite',
|
|
'browserDialogAccept',
|
|
'browserDialogDismiss',
|
|
'browserStorageLocalGet',
|
|
'browserStorageLocalSet',
|
|
'browserStorageLocalClear',
|
|
'browserStorageSessionGet',
|
|
'browserStorageSessionSet',
|
|
'browserStorageSessionClear',
|
|
'browserDownload',
|
|
'browserHighlight',
|
|
'browserExec',
|
|
'browserTabCreate',
|
|
'browserOpenUrlOnClient',
|
|
'browserTabSetProfile',
|
|
'browserTabProfileShow',
|
|
'browserTabProfileClone',
|
|
'browserProfileList',
|
|
'browserProfileCreate',
|
|
'browserProfileDelete',
|
|
'browserProfileDetectBrowsers',
|
|
'browserProfileImportFromBrowser',
|
|
'browserProfileClearDefaultCookies',
|
|
'browserTabClose'
|
|
] as const satisfies readonly (keyof RuntimeBrowserCommands)[]
|
|
|
|
function bindPrefixedMethods<T extends object>(
|
|
instance: T,
|
|
prefix: string
|
|
): Partial<PublicMethods<T>> {
|
|
const bound: Record<string, unknown> = {}
|
|
for (const name of Object.getOwnPropertyNames(Object.getPrototypeOf(instance))) {
|
|
if (name.startsWith(prefix)) {
|
|
bound[name] = (instance[name as keyof T] as (...args: unknown[]) => unknown).bind(instance)
|
|
}
|
|
}
|
|
return bound as Partial<PublicMethods<T>>
|
|
}
|
|
|
|
function bindNamedMethods<T extends object>(
|
|
instance: T,
|
|
names: readonly (keyof T)[]
|
|
): Partial<PublicMethods<T>> {
|
|
return Object.fromEntries(
|
|
names.map((name) => [name, (instance[name] as (...args: unknown[]) => unknown).bind(instance)])
|
|
) as Partial<PublicMethods<T>>
|
|
}
|
|
|
|
export class RuntimeEdgeCommandController {
|
|
private readonly jira = new RuntimeJiraCommands()
|
|
private readonly browser: RuntimeBrowserCommands
|
|
private readonly screencasts: RuntimeBrowserScreencastController
|
|
private readonly emulator: RuntimeEmulatorCommands
|
|
readonly surface: RuntimeEdgeCommandSurface
|
|
|
|
constructor(args: {
|
|
browserHost: RuntimeBrowserCommandHost
|
|
screencast: Omit<ScreencastDependencies, 'getCommands'>
|
|
emulatorHost: EmulatorHost
|
|
getBrowserCommands?: () => RuntimeBrowserCommands
|
|
}) {
|
|
this.browser = createRuntimeBrowserCommands(args.browserHost)
|
|
this.screencasts = new RuntimeBrowserScreencastController({
|
|
...args.screencast,
|
|
getCommands: () => args.getBrowserCommands?.() ?? this.browser
|
|
})
|
|
this.emulator = new RuntimeEmulatorCommands(args.emulatorHost)
|
|
this.surface = {
|
|
...bindPrefixedMethods(this.jira, 'jira'),
|
|
...bindNamedMethods(this.browser, BROWSER_COMMAND_NAMES),
|
|
...bindPrefixedMethods(this.emulator, 'emulator'),
|
|
browserScreencast: (params, options) => this.screencasts.start(params, options)
|
|
} as RuntimeEdgeCommandSurface
|
|
}
|
|
|
|
cancelScreencast(browserPageId: string): void {
|
|
this.screencasts.cancelMobilePage(browserPageId, true)
|
|
}
|
|
|
|
getBrowserRemoteViewerPages(): string[] {
|
|
return this.screencasts.getRemoteViewerPages()
|
|
}
|
|
|
|
getBrowserCommands(): RuntimeBrowserCommands {
|
|
return this.browser
|
|
}
|
|
}
|