cat / goq
clone

turns a linux box into a private steam streaming appliance: plug in, tap your key, play from another computer

refactor(portal): extract pointer feedback runtime

Dami committed 2e92ea7
commit
2e92ea77423d87a3dd4bce406a0a288c5c4a9294
parent
0e5a5dcdb87c4b1b05c0ee9170600217f47ee8d5
author
Dami <dami.etoile@gmail.com>
3 files changed, +298 -44
Mportal/main.js59++------
Mportal/pointer-feedback.mjs76+++++++++++
Mportal/pointer-feedback.test.mjs207++++++++++++++++++++++++++++++-
M portal/main.js +16 -43
@@ -10,7 +10,10 @@ import { createInputRuntime } from './input-runtime.mjs';
1010 import { createControlRuntime } from './control-runtime.mjs';1111 import { formatVideoDiscardTelemetry } from './frame-stats.mjs';1212 import { networkDiagnosticsPresentation } from './network-diagnostics.mjs';13-import { newPointerSession, parsePointerFeedbackMessage } from './pointer-feedback.mjs';13+import {14+ createPointerFeedbackRuntime,15+ newPointerSession,16+} from './pointer-feedback.mjs';1417 import { newConnectionState } from './connection-state.mjs';1518 import { formatSignedMilliseconds } from './av-sync.mjs';1619 import { audioButtonPresentation } from './audio-ui.mjs';
@@ -55,49 +58,19 @@ let controllerActivationInProgress = false;
5558 let controlRuntime = null;5659 let controllerRuntime = null;5760 58-function applyPointerPositionFeedback(message, session) {59- if (session !== activePointerSession || !connectionState.inputCapabilities.pointerPositionFeedback) return;60- const surface = currentPointerSurfaceSize();61- if (surface === null) return;62- const feedback = validatePointerPositionFeedback(message, surface);63- session.remotePosition = feedback.position;64- session.remoteVisible = feedback.pointer_visible;65- renderRemotePointer();66-}67-68-function handlePointerPositionFeedback(message, session) {69- if (session !== activePointerSession || session.failed || session.closing) return;70- try {71- const envelope = parsePointerFeedbackMessage(message);72- if (envelope.type === 'terminal') {73- session.failed = true;74- session.failureDetail = envelope.reason === 'eof'75- ? 'Pointer feedback ended'76- : 'Pointer feedback was malformed';77- session.remotePosition = null;78- session.remoteVisible = false;79- renderRemotePointer();80- console.error(session.failureDetail);81- if (connectionState.connected) void disconnect();82- return;83- }84- const feedback = envelope.feedback;85- session.received = true;86- session.latest = feedback;87- if (connectionState.connected) applyPointerPositionFeedback(feedback, session);88- } catch (error) {89- session.failed = true;90- session.failureDetail = `Pointer feedback failed: ${error}`;91- session.remotePosition = null;92- session.remoteVisible = false;93- renderRemotePointer();94- console.error('invalid pointer-position feedback:', error);95- if (connectionState.connected) void disconnect();96- }97-}98-9961 const invoke = (...args) => window.__TAURI__.core.invoke(...args);10062 const listen = (...args) => window.__TAURI__.event.listen(...args);63+const pointerFeedbackRuntime = createPointerFeedbackRuntime({64+ getActiveSession: () => activePointerSession,65+ getCapabilities: () => ({66+ connected: connectionState.connected,67+ pointerPositionFeedback: connectionState.inputCapabilities.pointerPositionFeedback,68+ }),69+ getSurface: currentPointerSurfaceSize,70+ render: renderRemotePointer,71+ disconnect,72+ logger: console,73+});10174 const inputRuntime = createInputRuntime({10275 invokeCommand: invoke,10376 getCapabilities: () => connectionState.inputCapabilities,
@@ -414,7 +387,7 @@ async function createConnectionAttempt({ createChannel: Channel }) {
414387 (message) => streamRuntime.handleBinaryFrame(message, frameSession),415388 );416389 pointerSession.channel = new Channel(417- (message) => handlePointerPositionFeedback(message, pointerSession),390+ (message) => pointerFeedbackRuntime.handleMessage(message, pointerSession),418391 );419392 return {420393 audioSession: audioAttempt.session,
M portal/pointer-feedback.mjs +76 -0
@@ -28,3 +28,79 @@ export function parsePointerFeedbackMessage(value, surface = null) {
2828 }2929 throw new TypeError('invalid pointer feedback message');3030 }31+32+export function createPointerFeedbackRuntime({33+ getActiveSession,34+ getCapabilities,35+ getSurface,36+ render,37+ disconnect,38+ logger = console,39+} = {}) {40+ if (typeof getActiveSession !== 'function') {41+ throw new TypeError('getActiveSession must be a function');42+ }43+ if (typeof getCapabilities !== 'function') {44+ throw new TypeError('getCapabilities must be a function');45+ }46+ if (typeof getSurface !== 'function') throw new TypeError('getSurface must be a function');47+ if (typeof render !== 'function') throw new TypeError('render must be a function');48+ if (typeof disconnect !== 'function') throw new TypeError('disconnect must be a function');49+ if (typeof logger?.error !== 'function') throw new TypeError('logger must provide error');50+51+ function capabilities() {52+ return getCapabilities() ?? {};53+ }54+55+ function disconnectIfConnected(snapshot) {56+ if (snapshot.connected === true) void disconnect();57+ }58+59+ function applyPosition(message, session) {60+ const snapshot = capabilities();61+ if (session !== getActiveSession() || snapshot.pointerPositionFeedback !== true) return;62+ const surface = getSurface();63+ if (surface === null) return;64+ const feedback = validatePointerPositionFeedback(message, surface);65+ session.remotePosition = feedback.position;66+ session.remoteVisible = feedback.pointer_visible;67+ render();68+ }69+70+ function handleMessage(message, session) {71+ if (session !== getActiveSession() || session.failed || session.closing) return;72+ try {73+ const envelope = parsePointerFeedbackMessage(message);74+ if (envelope.type === 'terminal') {75+ session.failed = true;76+ session.failureDetail = envelope.reason === 'eof'77+ ? 'Pointer feedback ended'78+ : 'Pointer feedback was malformed';79+ session.remotePosition = null;80+ session.remoteVisible = false;81+ render();82+ logger.error(session.failureDetail);83+ disconnectIfConnected(capabilities());84+ return;85+ }86+ const feedback = envelope.feedback;87+ session.received = true;88+ session.latest = feedback;89+ const snapshot = capabilities();90+ if (snapshot.connected === true) applyPosition(feedback, session);91+ } catch (error) {92+ session.failed = true;93+ session.failureDetail = `Pointer feedback failed: ${error}`;94+ session.remotePosition = null;95+ session.remoteVisible = false;96+ render();97+ logger.error('invalid pointer-position feedback:', error);98+ disconnectIfConnected(capabilities());99+ }100+ }101+102+ return Object.freeze({103+ applyPosition,104+ handleMessage,105+ });106+}
M portal/pointer-feedback.test.mjs +206 -1
@@ -1,7 +1,41 @@
11 import assert from 'node:assert/strict';2+import { readFileSync } from 'node:fs';23 import test from 'node:test';34 4-import { newPointerSession, parsePointerFeedbackMessage } from './pointer-feedback.mjs';5+import {6+ createPointerFeedbackRuntime,7+ newPointerSession,8+ parsePointerFeedbackMessage,9+} from './pointer-feedback.mjs';10+11+const main = readFileSync(new URL('./main.js', import.meta.url), 'utf8');12+13+function runtimeHarness() {14+ let activeSession = newPointerSession();15+ let capabilitySnapshot = { connected: false, pointerPositionFeedback: true };16+ let surface = { width: 1280, height: 800 };17+ let renders = 0;18+ let disconnects = 0;19+ const errors = [];20+ const runtime = createPointerFeedbackRuntime({21+ getActiveSession: () => activeSession,22+ getCapabilities: () => capabilitySnapshot,23+ getSurface: () => surface,24+ render: () => { renders++; },25+ disconnect: () => { disconnects++; },26+ logger: { error: (...args) => errors.push(args) },27+ });28+ return {29+ runtime,30+ errors,31+ get session() { return activeSession; },32+ get renders() { return renders; },33+ get disconnects() { return disconnects; },34+ setActiveSession(session) { activeSession = session; },35+ setCapabilities(snapshot) { capabilitySnapshot = snapshot; },36+ setSurface(nextSurface) { surface = nextSurface; },37+ };38+}539 640 test('new pointer sessions pin feedback, channel, surface, and render defaults', () => {741 assert.deepEqual(newPointerSession(), {
@@ -87,3 +121,174 @@ test('pointer feedback envelopes reject malformed or out-of-surface positions',
87121 /outside the negotiated surface/,88122 );89123 });124+125+test('runtime stages pre-connect feedback without applying or rendering it', () => {126+ const fixture = runtimeHarness();127+ fixture.runtime.handleMessage({128+ type: 'position',129+ sequence: 1,130+ position: { x: 50, y: 60 },131+ pointer_visible: true,132+ }, fixture.session);133+134+ assert.equal(fixture.session.received, true);135+ assert.deepEqual(fixture.session.latest, {136+ sequence: 1,137+ position: { x: 50, y: 60 },138+ pointer_visible: true,139+ });140+ assert.equal(fixture.session.remotePosition, null);141+ assert.equal(fixture.session.remoteVisible, false);142+ assert.equal(fixture.renders, 0);143+ assert.equal(fixture.disconnects, 0);144+});145+146+test('runtime applies active connected feedback only with capability and a surface', () => {147+ const fixture = runtimeHarness();148+ fixture.setCapabilities({ connected: true, pointerPositionFeedback: true });149+ fixture.runtime.handleMessage({150+ type: 'position',151+ sequence: 2,152+ position: { x: 100, y: 200 },153+ pointer_visible: false,154+ }, fixture.session);155+ assert.deepEqual(fixture.session.remotePosition, { x: 100, y: 200 });156+ assert.equal(fixture.session.remoteVisible, false);157+ assert.equal(fixture.renders, 1);158+159+ fixture.setCapabilities({ connected: true, pointerPositionFeedback: false });160+ fixture.runtime.handleMessage({161+ type: 'position',162+ sequence: 3,163+ position: { x: 101, y: 201 },164+ }, fixture.session);165+ assert.equal(fixture.session.latest.sequence, 3);166+ assert.deepEqual(fixture.session.remotePosition, { x: 100, y: 200 });167+ assert.equal(fixture.renders, 1);168+169+ fixture.setCapabilities({ connected: true, pointerPositionFeedback: true });170+ fixture.setSurface(null);171+ fixture.runtime.handleMessage({172+ type: 'position',173+ sequence: 4,174+ position: { x: 102, y: 202 },175+ }, fixture.session);176+ assert.equal(fixture.session.latest.sequence, 4);177+ assert.deepEqual(fixture.session.remotePosition, { x: 100, y: 200 });178+ assert.equal(fixture.renders, 1);179+});180+181+test('runtime ignores stale, failed, and closing session deliveries', () => {182+ const fixture = runtimeHarness();183+ const stale = newPointerSession();184+ fixture.setCapabilities({ connected: true, pointerPositionFeedback: true });185+ fixture.runtime.applyPosition({186+ sequence: 0, position: { x: 0, y: 0 }, pointer_visible: true,187+ }, stale);188+ assert.equal(stale.remotePosition, null);189+ assert.equal(fixture.renders, 0);190+191+ fixture.runtime.handleMessage({192+ type: 'position', sequence: 1, position: { x: 1, y: 1 },193+ }, stale);194+ assert.equal(stale.received, false);195+196+ fixture.session.failed = true;197+ fixture.runtime.handleMessage({198+ type: 'position', sequence: 2, position: { x: 2, y: 2 },199+ }, fixture.session);200+ assert.equal(fixture.session.received, false);201+202+ const closing = newPointerSession();203+ closing.closing = true;204+ fixture.setActiveSession(closing);205+ fixture.runtime.handleMessage({206+ type: 'position', sequence: 3, position: { x: 3, y: 3 },207+ }, closing);208+ assert.equal(closing.received, false);209+ assert.equal(fixture.renders, 0);210+});211+212+test('terminal feedback clears render state and disconnects only while connected', () => {213+ for (const [reason, detail] of [214+ ['eof', 'Pointer feedback ended'],215+ ['malformed', 'Pointer feedback was malformed'],216+ ]) {217+ const fixture = runtimeHarness();218+ fixture.session.remotePosition = { x: 4, y: 5 };219+ fixture.session.remoteVisible = true;220+ fixture.runtime.handleMessage({ type: 'terminal', reason }, fixture.session);221+ assert.equal(fixture.session.failed, true);222+ assert.equal(fixture.session.failureDetail, detail);223+ assert.equal(fixture.session.remotePosition, null);224+ assert.equal(fixture.session.remoteVisible, false);225+ assert.equal(fixture.renders, 1);226+ assert.deepEqual(fixture.errors, [[detail]]);227+ assert.equal(fixture.disconnects, 0);228+229+ const connected = runtimeHarness();230+ connected.setCapabilities({ connected: true, pointerPositionFeedback: true });231+ connected.runtime.handleMessage({ type: 'terminal', reason }, connected.session);232+ assert.equal(connected.disconnects, 1);233+ }234+});235+236+test('invalid and out-of-surface feedback fail, clear state, and preserve error logging', () => {237+ const fixture = runtimeHarness();238+ fixture.setCapabilities({ connected: true, pointerPositionFeedback: true });239+ fixture.session.remotePosition = { x: 3, y: 4 };240+ fixture.session.remoteVisible = true;241+ fixture.runtime.handleMessage({242+ type: 'position',243+ sequence: 7,244+ position: { x: 1280, y: 0 },245+ }, fixture.session);246+247+ assert.equal(fixture.session.received, true);248+ assert.equal(fixture.session.latest.sequence, 7);249+ assert.equal(fixture.session.failed, true);250+ assert.match(fixture.session.failureDetail, /outside the negotiated surface/);251+ assert.equal(fixture.session.remotePosition, null);252+ assert.equal(fixture.session.remoteVisible, false);253+ assert.equal(fixture.renders, 1);254+ assert.equal(fixture.errors[0][0], 'invalid pointer-position feedback:');255+ assert.match(String(fixture.errors[0][1]), /outside the negotiated surface/);256+ assert.equal(fixture.disconnects, 1);257+});258+259+test('runtime validates its injected ownership and side-effect boundaries', () => {260+ const required = {261+ getActiveSession: () => null,262+ getCapabilities: () => ({}),263+ getSurface: () => null,264+ render: () => {},265+ disconnect: () => {},266+ logger: { error: () => {} },267+ };268+ for (const name of ['getActiveSession', 'getCapabilities', 'getSurface', 'render', 'disconnect']) {269+ assert.throws(270+ () => createPointerFeedbackRuntime({ ...required, [name]: null }),271+ new RegExp(`${name} must be a function`),272+ );273+ }274+ assert.throws(275+ () => createPointerFeedbackRuntime({ ...required, logger: {} }),276+ /logger must provide error/,277+ );278+});279+280+test('Portal delegates pointer feedback while retaining session, surface, and DOM rendering', () => {281+ assert.match(main, /const pointerFeedbackRuntime = createPointerFeedbackRuntime\(\{/);282+ assert.match(main, /getActiveSession: \(\) => activePointerSession/);283+ assert.match(main, /getSurface: currentPointerSurfaceSize/);284+ assert.match(main, /render: renderRemotePointer/);285+ assert.match(286+ main,287+ /pointerFeedbackRuntime\.handleMessage\(message, pointerSession\)/,288+ );289+ assert.match(main, /let activePointerSession = null/);290+ assert.match(main, /function currentPointerSurfaceSize\(\)/);291+ assert.match(main, /function renderRemotePointer\(\)/);292+ assert.doesNotMatch(main, /function applyPointerPositionFeedback\(/);293+ assert.doesNotMatch(main, /function handlePointerPositionFeedback\(/);294+});