mirror of
https://github.com/stablyai/orca.git
synced 2026-11-13 17:37:40 +00:00
perf(mobile): gzip large frames for phones that read them (#36801)
Every JSON reply and stream event reached the phone as base64 of sealed UTF-8, a third larger than the JSON itself, and terminal snapshots (up to 512 KB per attach) went out uncompressed. Opening a host on a real desktop moved ~2 MB, about 17 s on a 1 Mbps link. Phones now advertise transport.compressed-frames.v1. For those sockets the desktop gzips text frames of 1 KiB or more, and terminal frames of that size, and seals them as binary frames tagged with a new kind byte, the payload kind and the inflated length. The phone inflates them (native gunzip when available) back into the same text or binary frame before routing. Screencast frames are never compressed, and the relaying desktop never offers the capability to paired servers, whose frames it rewrites. On the same desktop, opening a host now moves ~180 KB. Desktop CPU is unchanged: gzip then seal of the 1.5 MB catalog takes the same time as seal then base64 of the plain one. Setting ORCA_DISABLE_MOBILE_COMPRESSED_FRAMES=1 on a desktop turns compression off without a release. Older phones never advertise the capability and older desktops ignore it, so both keep plain frames.
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
import { gunzipSync } from 'fflate'
|
||||
import { readCompressedFrame } from '../../../src/shared/paired-e2ee-compressed-frame'
|
||||
import { REMOTE_RUNTIME_MAX_OUTBOUND_JSON_BYTES } from '../../../src/shared/remote-runtime/remote-runtime-memory-limits'
|
||||
import { pairedE2EENativeCodec } from './paired-e2ee-native-codec'
|
||||
|
||||
export type InflatedFrame = { kind: 'text'; text: string } | { kind: 'binary'; bytes: Uint8Array }
|
||||
|
||||
/** What a compressed frame carries, or null for any other binary frame; throws on a corrupt one. */
|
||||
export function inflateCompressedFrame(frame: Uint8Array): InflatedFrame | null {
|
||||
const compressed = readCompressedFrame(frame)
|
||||
if (!compressed) {
|
||||
return null
|
||||
}
|
||||
const { payloadKind, inflatedByteLength, gzipped } = compressed
|
||||
// Why: the desktop never sends a frame above its outbound cap, so a larger claim is a corrupt frame.
|
||||
if (inflatedByteLength > REMOTE_RUNTIME_MAX_OUTBOUND_JSON_BYTES) {
|
||||
throw new Error(`Compressed frame claims ${inflatedByteLength} bytes`)
|
||||
}
|
||||
const native = pairedE2EENativeCodec()
|
||||
// One byte past the claim lets a member that inflates larger fail the length check, not overrun.
|
||||
const bytes = native
|
||||
? native.gunzip(gzipped, inflatedByteLength + 1)
|
||||
: gunzipSync(gzipped, { out: new Uint8Array(inflatedByteLength + 1) })
|
||||
if (bytes.byteLength !== inflatedByteLength) {
|
||||
throw new Error('Compressed frame inflated to an unexpected length')
|
||||
}
|
||||
if (payloadKind === 'binary') {
|
||||
return { kind: 'binary', bytes }
|
||||
}
|
||||
return { kind: 'text', text: native?.decodeUtf8(bytes) ?? new TextDecoder().decode(bytes) }
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import nacl from 'tweetnacl'
|
||||
import { setFlagsFromString } from 'node:v8'
|
||||
import { runInNewContext } from 'node:vm'
|
||||
import { gzipSync } from 'node:zlib'
|
||||
import {
|
||||
encodeMobileE2EEV2Transcript,
|
||||
validateMobileE2EEV2Handshake,
|
||||
@@ -11,6 +12,7 @@ import {
|
||||
openMobileE2EEV2Frame,
|
||||
sealMobileE2EEV2Frame
|
||||
} from '../../../src/shared/mobile-e2ee-v2-framing'
|
||||
import { encodeCompressedFrame } from '../../../src/shared/paired-e2ee-compressed-frame'
|
||||
|
||||
vi.mock('expo-crypto', () => ({
|
||||
getRandomBytes: (length: number) => new Uint8Array(length).fill(9)
|
||||
@@ -205,6 +207,32 @@ describe('mobile E2EE v2 physical channel', () => {
|
||||
expect(ctx.onError).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('delivers compressed frames as what they carry, in counter order with plain frames', async () => {
|
||||
const ctx = setup(async (raw) => (raw instanceof Uint8Array ? raw : null))
|
||||
await authenticate(ctx)
|
||||
ctx.events.length = 0
|
||||
|
||||
const reply = JSON.stringify({ id: 'rpc-1', ok: true, result: { note: 'é'.repeat(4096) } })
|
||||
const utf8 = Buffer.from(reply, 'utf8')
|
||||
const text = encodeCompressedFrame({
|
||||
payloadKind: 'text',
|
||||
inflatedByteLength: utf8.byteLength,
|
||||
gzipped: gzipSync(utf8)
|
||||
})
|
||||
const terminal = new Uint8Array(2048).fill(0x74)
|
||||
const binary = encodeCompressedFrame({
|
||||
payloadKind: 'binary',
|
||||
inflatedByteLength: terminal.byteLength,
|
||||
gzipped: gzipSync(terminal)
|
||||
})
|
||||
await ctx.channel.handleMessage(serverFrame(text, 'binary', 1n, ctx.schedule))
|
||||
await ctx.channel.handleMessage(serverFrame(binary, 'binary', 2n, ctx.schedule))
|
||||
await ctx.channel.handleMessage(serverFrame(new Uint8Array([7]), 'binary', 3n, ctx.schedule))
|
||||
|
||||
expect(ctx.events).toEqual([`text:${reply}`, 'binary:116', 'binary:7'])
|
||||
expect(ctx.onError).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('queues outbound text and binary in one counter order', async () => {
|
||||
const ctx = setup(async () => null)
|
||||
await authenticate(ctx)
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from '../../../src/shared/paired-e2ee-client-handshake'
|
||||
import type { MobileE2EEV2ClientSession } from './mobile-e2ee-v2-client-session'
|
||||
import { WsInboundMessageQueue } from '../../../src/shared/ws-inbound-message-queue'
|
||||
import { inflateCompressedFrame } from './compressed-frame-inflate'
|
||||
|
||||
type ChannelState = 'awaiting-ready' | 'awaiting-authenticated' | 'ready'
|
||||
type OutboundItem = { kind: 'text'; plaintext: string } | { kind: 'binary'; plaintext: Uint8Array }
|
||||
@@ -118,7 +119,12 @@ export class MobileE2EEV2PhysicalChannel {
|
||||
} else if (typeof plaintext === 'string') {
|
||||
this.args.onText(plaintext)
|
||||
} else {
|
||||
this.args.onBinary(plaintext)
|
||||
const inflated = inflateCompressedFrame(plaintext)
|
||||
if (inflated?.kind === 'text') {
|
||||
this.args.onText(inflated.text)
|
||||
} else {
|
||||
this.args.onBinary(inflated?.bytes ?? plaintext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
import { AGENT_SESSION_OPTIONAL_MODEL_CLIENT_CAPABILITY } from '../../../src/shared/agent-session-optional-model-capability'
|
||||
import { remoteRuntimeClientCapabilities } from '../../../src/shared/remote-runtime/remote-runtime-client-capabilities'
|
||||
import { AGENT_SESSION_CREATE_MESSAGE_RUNTIME_CAPABILITY } from '../../../src/shared/agent-session-create-capabilities'
|
||||
import { COMPRESSED_FRAMES_CLIENT_CAPABILITY } from '../../../src/shared/paired-e2ee-compressed-frame'
|
||||
|
||||
export const MOBILE_RUNTIME_CLIENT_CAPABILITIES = remoteRuntimeClientCapabilities([
|
||||
AGENT_SESSION_COMMANDS_ON_REQUEST_CAPABILITY,
|
||||
@@ -50,7 +51,9 @@ export const MOBILE_RUNTIME_CLIENT_CAPABILITIES = remoteRuntimeClientCapabilitie
|
||||
// Native chat offers no Stop for a turn the host restored but could not confirm.
|
||||
AGENT_STATUS_RESTORED_UNCONFIRMED_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_CREATE_MESSAGE_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_QUEUED_IMAGE_CARDS_CLIENT_CAPABILITY
|
||||
AGENT_SESSION_QUEUED_IMAGE_CARDS_CLIENT_CAPABILITY,
|
||||
// Every socket this list is sent on reads frames through MobileE2EEV2PhysicalChannel, which inflates them.
|
||||
COMPRESSED_FRAMES_CLIENT_CAPABILITY
|
||||
])
|
||||
|
||||
export const MOBILE_RUNTIME_CLIENT_CAPABILITY_UPDATE_METHOD =
|
||||
|
||||
@@ -13,8 +13,12 @@ import {
|
||||
BrowserScreencastOpcode,
|
||||
encodeBrowserScreencastFrame
|
||||
} from '../../../shared/browser-screencast-protocol'
|
||||
import { COMPRESSED_FRAMES_CLIENT_CAPABILITY } from '../../../shared/paired-e2ee-compressed-frame'
|
||||
import { BROWSER_CLIENT_HOST_RUNTIME_CAPABILITY } from '../../../shared/protocol-version'
|
||||
import {
|
||||
declareRelayedPhoneCapabilities,
|
||||
RelayedPhoneReplies,
|
||||
relayedPhoneCapabilities,
|
||||
rewriteRelayedPhoneRequest,
|
||||
withRelayedLaunchChatDefault
|
||||
} from './relayed-phone-frames'
|
||||
@@ -57,6 +61,26 @@ describe('rewriteRelayedPhoneRequest', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('relayed phone capabilities', () => {
|
||||
// The relaying desktop rewrites stream ids and reply bookkeeping, which compressed frames would hide.
|
||||
it('never offers a paired server compressed frames, at auth or on update', () => {
|
||||
const phone = ['agent-session.turn-item.v1', COMPRESSED_FRAMES_CLIENT_CAPABILITY]
|
||||
const update = JSON.stringify({
|
||||
id: 'r1',
|
||||
method: 'runtime.clientCapabilities.update',
|
||||
params: { clientCapabilities: phone }
|
||||
})
|
||||
|
||||
expect(relayedPhoneCapabilities(phone)).toEqual([
|
||||
'agent-session.turn-item.v1',
|
||||
BROWSER_CLIENT_HOST_RUNTIME_CAPABILITY
|
||||
])
|
||||
expect(JSON.parse(declareRelayedPhoneCapabilities(update)).params.clientCapabilities).toEqual(
|
||||
relayedPhoneCapabilities(phone)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('RelayedPhoneReplies', () => {
|
||||
function replies(firstDesktopId = 100) {
|
||||
let next = firstDesktopId
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { BROWSER_CLIENT_HOST_RUNTIME_CAPABILITY } from '../../../shared/protocol-version'
|
||||
import { COMPRESSED_FRAMES_CLIENT_CAPABILITY } from '../../../shared/paired-e2ee-compressed-frame'
|
||||
import {
|
||||
readTerminalStreamFrameId,
|
||||
writeTerminalStreamFrameId
|
||||
@@ -25,10 +26,14 @@ export function rewriteRelayedPhoneRequest(
|
||||
}
|
||||
|
||||
// Servers expose client-placed tabs only with this capability; mobile scope still cannot host them.
|
||||
// Withhold compressed frames: they would hide relayed replies and stream ids from the rewriting below.
|
||||
export function relayedPhoneCapabilities(capabilities: readonly string[]): readonly string[] {
|
||||
return capabilities.includes(BROWSER_CLIENT_HOST_RUNTIME_CAPABILITY)
|
||||
? capabilities
|
||||
: [...capabilities, BROWSER_CLIENT_HOST_RUNTIME_CAPABILITY]
|
||||
const forwarded = capabilities.filter(
|
||||
(capability) => capability !== COMPRESSED_FRAMES_CLIENT_CAPABILITY
|
||||
)
|
||||
return forwarded.includes(BROWSER_CLIENT_HOST_RUNTIME_CAPABILITY)
|
||||
? forwarded
|
||||
: [...forwarded, BROWSER_CLIENT_HOST_RUNTIME_CAPABILITY]
|
||||
}
|
||||
|
||||
export function declareRelayedPhoneCapabilities(frame: string): string {
|
||||
|
||||
@@ -13,7 +13,11 @@ import {
|
||||
describeMobileE2EEAuthFailure,
|
||||
type DeviceKeyResolution
|
||||
} from './mobile-e2ee-auth-validation'
|
||||
import { isMobileE2EEOutboundItemWithinLimit } from './mobile-e2ee-outbound-admission'
|
||||
import {
|
||||
isMobileE2EEOutboundItemWithinLimit,
|
||||
trackMobileE2EEOutboundBudgetClose,
|
||||
type MobileE2EEOutboundBudgetEmitter
|
||||
} from './mobile-e2ee-outbound-admission'
|
||||
import { parseRemoteRuntimeJsonText } from '../../../shared/remote-runtime/requests/remote-runtime-request-frames'
|
||||
import type { MobileE2EEOutboundMemoryBudget } from './mobile-e2ee-outbound-memory-budget'
|
||||
import { MobileE2EEDesktopOutboundOwner } from './mobile-e2ee-desktop-outbound-owner'
|
||||
@@ -24,12 +28,8 @@ import type {
|
||||
RpcBinarySendResult,
|
||||
RpcBinarySender
|
||||
} from './rpc-binary-sender'
|
||||
import type { EventProps } from '../../../shared/telemetry-events'
|
||||
import { track } from '../../telemetry/client'
|
||||
import type { DevicePrincipal } from '../device-principal'
|
||||
|
||||
type OutboundBudgetEmitter = EventProps<'remote_outbound_budget_close'>['emitter']
|
||||
|
||||
const HANDSHAKE_TIMEOUT_MS = 10_000
|
||||
const MAX_CONSECUTIVE_DECRYPT_FAILURES = 5
|
||||
|
||||
@@ -98,6 +98,7 @@ export class E2EEChannel {
|
||||
ws,
|
||||
() => Boolean(this.sharedKey),
|
||||
() => this.closeForOutboundBudget('queue'),
|
||||
this.capabilitySource,
|
||||
options.outboundMemoryBudget
|
||||
)
|
||||
|
||||
@@ -310,12 +311,8 @@ export class E2EEChannel {
|
||||
}
|
||||
|
||||
// Both budget closes kill the remote session: size identifies an oversized producer, queue a backed-up link.
|
||||
private closeForOutboundBudget(emitter: OutboundBudgetEmitter): void {
|
||||
try {
|
||||
track('remote_outbound_budget_close', { emitter })
|
||||
} catch {
|
||||
// Telemetry is best-effort; closing the unsafe socket remains authoritative.
|
||||
}
|
||||
private closeForOutboundBudget(emitter: MobileE2EEOutboundBudgetEmitter): void {
|
||||
trackMobileE2EEOutboundBudgetClose(emitter)
|
||||
this.onError(1013, 'Outbound reply buffer overflow')
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,12 @@ export function installMultiplexFrameDelivery(state: TerminalMultiplexConnection
|
||||
typeof seq === 'number' ? seq : opcode === TerminalStreamOpcode.Output ? 0 : state.cursor++
|
||||
let sent: RpcBinarySendResult
|
||||
try {
|
||||
sent = sendBinary(encodeTerminalStreamFrame({ opcode, streamId, seq: resolvedSeq, payload }))
|
||||
sent = sendBinary(
|
||||
encodeTerminalStreamFrame({ opcode, streamId, seq: resolvedSeq, payload }),
|
||||
{
|
||||
compressible: true
|
||||
}
|
||||
)
|
||||
} catch {
|
||||
state.closeMultiplex()
|
||||
return false
|
||||
|
||||
@@ -83,7 +83,9 @@ export async function runTerminalBinarySubscription(args: TerminalSubscriptionAr
|
||||
if (closed || !sendBinary) {
|
||||
return
|
||||
}
|
||||
sendBinary(encodeTerminalStreamFrame({ opcode, streamId, seq: frameSeq, payload }))
|
||||
sendBinary(encodeTerminalStreamFrame({ opcode, streamId, seq: frameSeq, payload }), {
|
||||
compressible: true
|
||||
})
|
||||
}
|
||||
outputBatcher = createTerminalOutputBatcher((data, meta) => {
|
||||
if (meta?.cwd !== undefined) {
|
||||
|
||||
@@ -14,6 +14,8 @@ import {
|
||||
type MobileE2EEOutboundSocketMemory
|
||||
} from './mobile-e2ee-outbound-memory-budget'
|
||||
import type { RpcBinarySendOptions } from './rpc-binary-sender'
|
||||
import type { RuntimeClientCapabilities } from './runtime-client-capabilities'
|
||||
import { compressMobileE2EEV2Item } from './mobile-e2ee-v2-compressed-frame'
|
||||
|
||||
export class MobileE2EEDesktopOutboundOwner {
|
||||
private readonly memoryBudget: MobileE2EEOutboundMemoryBudget
|
||||
@@ -26,6 +28,7 @@ export class MobileE2EEDesktopOutboundOwner {
|
||||
private readonly ws: WebSocket,
|
||||
private readonly isLegacyKeyed: () => boolean,
|
||||
private readonly onOverflow: () => void,
|
||||
private readonly clientCapabilities: RuntimeClientCapabilities,
|
||||
memoryBudget: MobileE2EEOutboundMemoryBudget = createMobileE2EEOutboundMemoryBudget()
|
||||
) {
|
||||
this.memoryBudget = memoryBudget
|
||||
@@ -62,10 +65,11 @@ export class MobileE2EEDesktopOutboundOwner {
|
||||
}
|
||||
|
||||
enqueueV2(
|
||||
item: DesktopMobileE2EEV2OutboundItem,
|
||||
plain: DesktopMobileE2EEV2OutboundItem,
|
||||
session: DesktopMobileE2EEV2Session,
|
||||
options?: RpcBinarySendOptions
|
||||
): boolean | 'backlogged' {
|
||||
const item = compressMobileE2EEV2Item(plain, this.clientCapabilities.current, options)
|
||||
if (!this.socketMemory) {
|
||||
this.onOverflow()
|
||||
return false
|
||||
|
||||
@@ -11,6 +11,18 @@ import type {
|
||||
MobileE2EEOutboundMemoryBudget,
|
||||
MobileE2EEOutboundSocketMemory
|
||||
} from './mobile-e2ee-outbound-memory-budget'
|
||||
import type { EventProps } from '../../../shared/telemetry-events'
|
||||
import { track } from '../../telemetry/client'
|
||||
|
||||
export type MobileE2EEOutboundBudgetEmitter = EventProps<'remote_outbound_budget_close'>['emitter']
|
||||
|
||||
export function trackMobileE2EEOutboundBudgetClose(emitter: MobileE2EEOutboundBudgetEmitter): void {
|
||||
try {
|
||||
track('remote_outbound_budget_close', { emitter })
|
||||
} catch {
|
||||
// Telemetry is best-effort; closing the unsafe socket remains authoritative.
|
||||
}
|
||||
}
|
||||
|
||||
export function mobileE2EETextPayloadAdmissionBytes(value: string): number {
|
||||
const bytes = Buffer.byteLength(value, 'utf8')
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { gunzipSync } from 'node:zlib'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
COMPRESSED_FRAMES_CLIENT_CAPABILITY,
|
||||
readCompressedFrame
|
||||
} from '../../../shared/paired-e2ee-compressed-frame'
|
||||
import { compressMobileE2EEV2Item } from './mobile-e2ee-v2-compressed-frame'
|
||||
|
||||
const capable = [COMPRESSED_FRAMES_CLIENT_CAPABILITY]
|
||||
|
||||
function inflate(item: ReturnType<typeof compressMobileE2EEV2Item>) {
|
||||
const frame = item.kind === 'binary' ? readCompressedFrame(item.plaintext) : null
|
||||
return frame && { payloadKind: frame.payloadKind, bytes: gunzipSync(frame.gzipped) }
|
||||
}
|
||||
|
||||
describe('compressMobileE2EEV2Item', () => {
|
||||
it('sends large text to a capable phone as a compressed frame that inflates to the same text', () => {
|
||||
const plaintext = JSON.stringify({ rows: Array.from({ length: 200 }, (_, i) => `ü-row-${i}`) })
|
||||
|
||||
const inflated = inflate(compressMobileE2EEV2Item({ kind: 'text', plaintext }, capable))
|
||||
|
||||
expect(inflated?.payloadKind).toBe('text')
|
||||
expect(inflated?.bytes.toString('utf8')).toBe(plaintext)
|
||||
})
|
||||
|
||||
it('compresses binary only when its sender marks it compressible', () => {
|
||||
const terminal = { kind: 'binary' as const, plaintext: new Uint8Array(4096).fill(0x74) }
|
||||
|
||||
expect(compressMobileE2EEV2Item(terminal, capable)).toBe(terminal)
|
||||
const inflated = inflate(compressMobileE2EEV2Item(terminal, capable, { compressible: true }))
|
||||
expect(inflated?.payloadKind).toBe('binary')
|
||||
expect(new Uint8Array(inflated!.bytes)).toEqual(terminal.plaintext)
|
||||
})
|
||||
|
||||
it('leaves frames alone for phones without the capability and for small frames', () => {
|
||||
const large = { kind: 'text' as const, plaintext: 'x'.repeat(4096) }
|
||||
const small = { kind: 'text' as const, plaintext: '{"id":"1"}' }
|
||||
|
||||
expect(compressMobileE2EEV2Item(large, [])).toBe(large)
|
||||
expect(compressMobileE2EEV2Item(small, capable)).toBe(small)
|
||||
})
|
||||
|
||||
it('sends plain frames to every phone while the rollback switch is set', () => {
|
||||
const large = { kind: 'text' as const, plaintext: 'x'.repeat(4096) }
|
||||
vi.stubEnv('ORCA_DISABLE_MOBILE_COMPRESSED_FRAMES', '1')
|
||||
try {
|
||||
expect(compressMobileE2EEV2Item(large, capable)).toBe(large)
|
||||
} finally {
|
||||
vi.unstubAllEnvs()
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,42 @@
|
||||
import { gzipSync } from 'node:zlib'
|
||||
import {
|
||||
COMPRESSED_FRAMES_CLIENT_CAPABILITY,
|
||||
COMPRESSED_FRAME_MIN_BYTES,
|
||||
encodeCompressedFrame
|
||||
} from '../../../shared/paired-e2ee-compressed-frame'
|
||||
import type { RuntimeCapability } from '../../../shared/protocol-version'
|
||||
import type { DesktopMobileE2EEV2OutboundItem } from './mobile-e2ee-v2-desktop-outbound'
|
||||
import type { RpcBinarySendOptions } from './rpc-binary-sender'
|
||||
|
||||
/** Gzips large text, and binary its sender marked compressible, for phones that read compressed frames. */
|
||||
export function compressMobileE2EEV2Item(
|
||||
item: DesktopMobileE2EEV2OutboundItem,
|
||||
clientCapabilities: readonly RuntimeCapability[],
|
||||
options?: RpcBinarySendOptions
|
||||
): DesktopMobileE2EEV2OutboundItem {
|
||||
// Why: screencast JPEGs never shrink, so binary is gzipped only when its sender knows it is text-like.
|
||||
const eligible = item.kind === 'text' || options?.compressible === true
|
||||
if (
|
||||
!eligible ||
|
||||
// Field rollback without a release: phones then read plain frames, as from older hosts.
|
||||
process.env.ORCA_DISABLE_MOBILE_COMPRESSED_FRAMES === '1' ||
|
||||
item.plaintext.length < COMPRESSED_FRAME_MIN_BYTES ||
|
||||
!clientCapabilities.includes(COMPRESSED_FRAMES_CLIENT_CAPABILITY)
|
||||
) {
|
||||
return item
|
||||
}
|
||||
const payload = item.kind === 'text' ? Buffer.from(item.plaintext, 'utf8') : item.plaintext
|
||||
const gzipped = gzipSync(payload)
|
||||
// Why: already-dense payloads can grow under gzip; the plain frame then costs less.
|
||||
if (gzipped.byteLength >= payload.byteLength) {
|
||||
return item
|
||||
}
|
||||
return {
|
||||
kind: 'binary',
|
||||
plaintext: encodeCompressedFrame({
|
||||
payloadKind: item.kind,
|
||||
inflatedByteLength: payload.byteLength,
|
||||
gzipped
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
export type RpcBinarySendOptions = {
|
||||
// Why: a lossy stream resends its newest frame later, so queueing stale frames behind a backlog only delays it.
|
||||
dropWhenBacklogged?: boolean
|
||||
// Text-like payloads (terminal output, snapshots) a phone that reads compressed frames receives gzipped.
|
||||
compressible?: boolean
|
||||
}
|
||||
|
||||
/** False: not sent or queued. 'backlogged': skipped under dropWhenBacklogged; the socket is alive. */
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* A text or binary frame the desktop gzipped and sealed as a binary frame: a 6-byte header (kind,
|
||||
* inflated payload kind, little-endian inflated byte length) and one gzip member. Binary carries it
|
||||
* without base64's third, and the kind byte keeps it apart from terminal (0x74) and screencast frames.
|
||||
*/
|
||||
export const COMPRESSED_FRAMES_CLIENT_CAPABILITY = 'transport.compressed-frames.v1' as const
|
||||
|
||||
const COMPRESSED_FRAME_KIND = 0x7a
|
||||
const HEADER_BYTES = 6
|
||||
const PAYLOAD_KIND_BYTES = { text: 1, binary: 2 } as const
|
||||
|
||||
export type CompressedFramePayloadKind = keyof typeof PAYLOAD_KIND_BYTES
|
||||
|
||||
// Why: below this a gzip member's fixed overhead and the inflate call outweigh the saved bytes.
|
||||
export const COMPRESSED_FRAME_MIN_BYTES = 1024
|
||||
|
||||
export function encodeCompressedFrame(args: {
|
||||
payloadKind: CompressedFramePayloadKind
|
||||
inflatedByteLength: number
|
||||
gzipped: Uint8Array
|
||||
}): Uint8Array {
|
||||
const frame = new Uint8Array(HEADER_BYTES + args.gzipped.byteLength)
|
||||
const view = new DataView(frame.buffer)
|
||||
view.setUint8(0, COMPRESSED_FRAME_KIND)
|
||||
view.setUint8(1, PAYLOAD_KIND_BYTES[args.payloadKind])
|
||||
view.setUint32(2, args.inflatedByteLength, true)
|
||||
frame.set(args.gzipped, HEADER_BYTES)
|
||||
return frame
|
||||
}
|
||||
|
||||
/** Null for any other binary frame, so callers pass it through unchanged. */
|
||||
export function readCompressedFrame(frame: Uint8Array): {
|
||||
payloadKind: CompressedFramePayloadKind
|
||||
inflatedByteLength: number
|
||||
gzipped: Uint8Array
|
||||
} | null {
|
||||
if (frame.byteLength < HEADER_BYTES || frame[0] !== COMPRESSED_FRAME_KIND) {
|
||||
return null
|
||||
}
|
||||
const payloadKind =
|
||||
frame[1] === PAYLOAD_KIND_BYTES.text
|
||||
? 'text'
|
||||
: frame[1] === PAYLOAD_KIND_BYTES.binary
|
||||
? 'binary'
|
||||
: null
|
||||
if (!payloadKind) {
|
||||
return null
|
||||
}
|
||||
const view = new DataView(frame.buffer, frame.byteOffset, frame.byteLength)
|
||||
return {
|
||||
payloadKind,
|
||||
inflatedByteLength: view.getUint32(2, true),
|
||||
gzipped: frame.subarray(HEADER_BYTES)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user