1import {2 committedRustConnection,3 disconnectRejectedRustConnection,4} from './connection-attempt.mjs';56// Mirrors MEDIA_TRANSPORT_NAMES in src-tauri/src/media/transport.rs.7export const MEDIA_TRANSPORT_ALLOWLIST = Object.freeze([8 'iroh-moq',9 'grouped-v3',10]);1112export function normalizeMediaTransport(value) {13 return MEDIA_TRANSPORT_ALLOWLIST.includes(value) ? value : 'unknown';14}1516function emptyInputCapabilities() {17 return {18 relativePointer: false,19 pointerPositionFeedback: false,20 absolutePointer: false,21 keyboard: false,22 text: false,23 gamepad: false,24 control: false,25 };26}2728function normalizeInputCapabilities(result) {29 const capabilities = {30 relativePointer: result.relative_pointer_available === true,31 pointerPositionFeedback: result.pointer_position_feedback_available === true,32 absolutePointer: result.absolute_pointer_available === true,33 keyboard: result.keyboard_available === true,34 text: result.text_available === true,35 gamepad: result.gamepad_available === true,36 control: result.control_available === true,37 };38 capabilities.control = capabilities.control39 && (capabilities.relativePointer40 || capabilities.absolutePointer41 || capabilities.keyboard42 || capabilities.text43 || capabilities.gamepad);44 return capabilities;45}4647function connectionStatus(result, capabilities) {48 return [49 'connected',50 capabilities.control ? null : 'view only',51 result.development_mode ? 'dev direct-node' : null,52 ].filter(Boolean).join(' ยท ');53}5455function validateGeneration(result) {56 if (result.connected57 && (!Number.isSafeInteger(result.media_generation) || result.media_generation <= 0)) {58 throw new Error('host returned an invalid media generation');59 }60 if (result.connected61 && result.audio_available === true62 && (!Number.isSafeInteger(result.audio_generation) || result.audio_generation <= 0)) {63 throw new Error('host returned an invalid audio generation');64 }65}6667const noop = () => {};68const asyncNoop = async () => {};6970export function newConnectionState({71 invokeCommand,72 createChannel = null,73 beforeConnect = noop,74 createAttempt = asyncNoop,75 validateConnectedResult = noop,76 activateAttempt = noop,77 closeAttempt = noop,78 teardownAttempt = asyncNoop,79 beforeDisconnect = asyncNoop,80 teardownConnection = asyncNoop,81 onStatus = noop,82 onConnected = noop,83 onDisconnected = noop,84 onFailure = noop,85 onDisconnectError = noop,86 onNativeDisconnectError = noop,87} = {}) {88 if (typeof invokeCommand !== 'function') {89 throw new TypeError('invokeCommand must be a function');90 }9192 const state = {93 connected: false,94 connecting: false,95 disconnecting: false,96 developmentMode: false,97 inputCapabilities: emptyInputCapabilities(),98 mediaTransport: 'unknown',99 };100101 const snapshot = () => ({102 connected: state.connected,103 connecting: state.connecting,104 disconnecting: state.disconnecting,105 developmentMode: state.developmentMode,106 inputCapabilities: { ...state.inputCapabilities },107 mediaTransport: state.mediaTransport,108 });109110 async function connect({ pin = '' } = {}) {111 if (state.connecting || state.connected) return false;112 state.connecting = true;113 let attempt = null;114 let rustConnectionCommitted = false;115 try {116 const normalizedPin = String(pin).trim();117 if (!state.developmentMode && !normalizedPin) {118 onStatus('err', 'enter pin');119 return false;120 }121122 beforeConnect();123 onStatus('pending', 'connecting...');124 attempt = await createAttempt({ createChannel });125 const connectionArgs = attempt?.connectionArgs ?? {};126 const result = await invokeCommand('iroh_client_connect', {127 pin: normalizedPin,128 ...connectionArgs,129 });130 rustConnectionCommitted = committedRustConnection(result);131 validateGeneration(result);132 validateConnectedResult(result, attempt);133134 if (!result.connected) {135 closeAttempt(attempt, { committed: false, rejected: true });136 await teardownAttempt(attempt);137 onStatus('err', 'failed');138 return false;139 }140141 activateAttempt(result, attempt);142 state.connected = true;143 state.disconnecting = false;144 state.inputCapabilities = normalizeInputCapabilities(result);145 state.mediaTransport = normalizeMediaTransport(result.media_transport);146 onStatus('ok', connectionStatus(result, state.inputCapabilities), {147 result,148 attempt,149 state: snapshot(),150 });151 await onConnected({ result, attempt, state: snapshot() });152 rustConnectionCommitted = false;153 return true;154 } catch (error) {155 closeAttempt(attempt, { committed: rustConnectionCommitted, error });156 if (rustConnectionCommitted) {157 try {158 await disconnectRejectedRustConnection(invokeCommand, rustConnectionCommitted);159 } catch (disconnectError) {160 onNativeDisconnectError(disconnectError);161 }162 }163 await teardownAttempt(attempt);164 onFailure({ error, attempt, state: snapshot() });165 onStatus('err', 'error');166 return false;167 } finally {168 state.connecting = false;169 }170 }171172 async function disconnect() {173 if (state.disconnecting) return false;174 state.disconnecting = true;175 await beforeDisconnect({ state: snapshot() });176 try {177 await invokeCommand('iroh_client_disconnect');178 } catch (error) {179 onDisconnectError(error);180 }181 await teardownConnection();182 state.connected = false;183 state.inputCapabilities = emptyInputCapabilities();184 state.mediaTransport = 'unknown';185 await onDisconnected({ state: snapshot() });186 state.disconnecting = false;187 return true;188 }189190 return {191 get connected() { return state.connected; },192 get connecting() { return state.connecting; },193 get disconnecting() { return state.disconnecting; },194 get developmentMode() { return state.developmentMode; },195 get inputCapabilities() { return { ...state.inputCapabilities }; },196 get mediaTransport() { return state.mediaTransport; },197 snapshot,198 setDevelopmentMode(enabled) {199 state.developmentMode = enabled === true;200 },201 connect,202 disconnect,203 };204}