cat / goq
clone

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

fix/docs-activation-truth
goq / portal / focus-runtime.mjs
focus-runtime.mjs 5.3K
1function currentProposalForHolder(session) {2 const proposal = session.snapshot?.focus_proposal;3 const focus = session.snapshot?.focus;4 if (!proposal || focus?.state !== 'held' || !session.focused) return null;5 if (proposal.holder !== session.snapshot.self_presence_id6 || proposal.holder_session_id !== session.snapshot.viewers.find((viewer) => viewer.you)?.session_id) {7 return null;8 }9 return proposal;10}1112export function createFocusRuntime({13 invokeCommand,14 getSession,15 startFocusedInput,16 activateLocalControl,17 onChange = () => {},18} = {}) {19 if (typeof invokeCommand !== 'function') throw new TypeError('invokeCommand must be a function');20 if (typeof getSession !== 'function') throw new TypeError('getSession must be a function');21 if (typeof startFocusedInput !== 'function') throw new TypeError('startFocusedInput must be a function');22 if (typeof activateLocalControl !== 'function') throw new TypeError('activateLocalControl must be a function');23 let nextRequestId = 1;24 let pending = null;2526 async function queue(kind, command, args, controllerInitiated = false) {27 if (pending !== null) return false;28 const requestId = nextRequestId++;29 pending = { requestId, kind, command, controllerInitiated };30 onChange(snapshot());31 try {32 const queued = await invokeCommand(command, { requestId, ...args });33 if (!queued && pending?.requestId === requestId) pending = null;34 onChange(snapshot());35 return queued === true;36 } catch (error) {37 if (pending?.requestId === requestId) pending = null;38 onChange(snapshot());39 throw error;40 }41 }4243 async function request({ controllerInitiated = false } = {}) {44 const session = getSession();45 if (session.mode !== 'control-v2' || !session.eligible || session.focused) return false;46 return queue('request', 'iroh_client_request_focus', {47 generation: session.nativeGeneration,48 expectedRevision: session.revision,49 }, controllerInitiated);50 }5152 async function approve() {53 const session = getSession();54 const proposal = currentProposalForHolder(session);55 if (!proposal) return false;56 return queue('approve', 'iroh_client_approve_focus', {57 generation: session.nativeGeneration,58 expectedRevision: session.revision,59 expectedFocusGeneration: session.focusGeneration,60 expectedProposalId: proposal.proposal_id,61 });62 }6364 async function deny() {65 const session = getSession();66 const proposal = currentProposalForHolder(session);67 if (!proposal) return false;68 return queue('deny', 'iroh_client_deny_focus', {69 generation: session.nativeGeneration,70 expectedRevision: session.revision,71 expectedFocusGeneration: session.focusGeneration,72 expectedProposalId: proposal.proposal_id,73 });74 }7576 async function release() {77 const session = getSession();78 if (session.mode !== 'control-v2' || !session.focused) return false;79 return queue('release', 'iroh_client_release_focus', {80 generation: session.nativeGeneration,81 expectedRevision: session.revision,82 expectedFocusGeneration: session.focusGeneration,83 });84 }8586 async function preempt({ controllerInitiated = false } = {}) {87 const session = getSession();88 const focus = session.snapshot?.focus;89 if (session.mode !== 'control-v2'90 || !session.eligible91 || session.focused92 || session.snapshot?.self_is_focus_owner !== true93 || focus?.state !== 'held') return false;94 return queue('preempt', 'iroh_client_preempt_focus', {95 generation: session.nativeGeneration,96 expectedRevision: session.revision,97 expectedFocusGeneration: focus.focus_generation,98 }, controllerInitiated);99 }100101 async function observeSession() {102 const session = getSession();103 const request = pending;104 if (!request) return false;105 if ((request.kind === 'request' || request.kind === 'preempt') && session.focused) {106 pending = null;107 onChange(snapshot());108 await startFocusedInput();109 await activateLocalControl({ controllerInitiated: request.controllerInitiated });110 return true;111 }112 if (request.kind === 'release' && !session.focused) {113 pending = null;114 onChange(snapshot());115 return true;116 }117 if (request.kind === 'request') {118 const proposal = session.snapshot?.focus_proposal;119 const selfId = session.snapshot?.self_presence_id;120 const stillPending = proposal?.requester === selfId;121 const terminalReason = ['handoff_denied', 'proposal_expired', 'disconnected', 'replaced', 'revoked']122 .includes(session.snapshot?.transition_reason);123 if (!stillPending && terminalReason) {124 pending = null;125 onChange(snapshot());126 }127 }128 return false;129 }130131 function observeResult(result) {132 if (!pending || result?.request_id !== pending.requestId) return false;133 if (result.accepted === false || ['approve', 'deny'].includes(pending.kind)) {134 pending = null;135 onChange(snapshot());136 }137 return true;138 }139140 function reset() {141 pending = null;142 onChange(snapshot());143 }144145 function snapshot() {146 return {147 pending: pending !== null,148 kind: pending?.kind ?? null,149 requestId: pending?.requestId ?? null,150 };151 }152153 return Object.freeze({154 approve,155 deny,156 preempt,157 release,158 request,159 observeSession,160 observeResult,161 reset,162 snapshot,163 });164}