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 controlProtocol: 'unknown',100 };101102 const snapshot = () => ({103 connected: state.connected,104 connecting: state.connecting,105 disconnecting: state.disconnecting,106 developmentMode: state.developmentMode,107 inputCapabilities: { ...state.inputCapabilities },108 mediaTransport: state.mediaTransport,109 controlProtocol: state.controlProtocol,110 });111112 async function connect({ pin = '' } = {}) {113 if (state.connecting || state.connected) return false;114 state.connecting = true;115 let attempt = null;116 let rustConnectionCommitted = false;117 try {118 const normalizedPin = String(pin).trim();119 if (!state.developmentMode && !normalizedPin) {120 onStatus('err', 'enter pin');121 return false;122 }123124 beforeConnect();125 onStatus('pending', 'connecting...');126 attempt = await createAttempt({ createChannel });127 const connectionArgs = attempt?.connectionArgs ?? {};128 const result = await invokeCommand('iroh_client_connect', {129 pin: normalizedPin,130 ...connectionArgs,131 });132 rustConnectionCommitted = committedRustConnection(result);133 validateGeneration(result);134 validateConnectedResult(result, attempt);135136 if (!result.connected) {137 closeAttempt(attempt, { committed: false, rejected: true });138 await teardownAttempt(attempt);139 onStatus('err', 'failed');140 return false;141 }142143 activateAttempt(result, attempt);144 state.connected = true;145 state.disconnecting = false;146 state.inputCapabilities = normalizeInputCapabilities(result);147 state.mediaTransport = normalizeMediaTransport(result.media_transport);148 state.controlProtocol = result.control_protocol === 'control-v2' ? 'control-v2' : 'legacy-v1';149 onStatus('ok', connectionStatus(result, state.inputCapabilities), {150 result,151 attempt,152 state: snapshot(),153 });154 await onConnected({ result, attempt, state: snapshot() });155 rustConnectionCommitted = false;156 return true;157 } catch (error) {158 closeAttempt(attempt, { committed: rustConnectionCommitted, error });159 if (rustConnectionCommitted) {160 try {161 await disconnectRejectedRustConnection(invokeCommand, rustConnectionCommitted);162 } catch (disconnectError) {163 onNativeDisconnectError(disconnectError);164 }165 }166 await teardownAttempt(attempt);167 onFailure({ error, attempt, state: snapshot() });168 onStatus('err', 'error');169 return false;170 } finally {171 state.connecting = false;172 }173 }174175 async function disconnect() {176 if (state.disconnecting) return false;177 state.disconnecting = true;178 await beforeDisconnect({ state: snapshot() });179 try {180 await invokeCommand('iroh_client_disconnect');181 } catch (error) {182 onDisconnectError(error);183 }184 await teardownConnection();185 state.connected = false;186 state.inputCapabilities = emptyInputCapabilities();187 state.mediaTransport = 'unknown';188 state.controlProtocol = 'unknown';189 await onDisconnected({ state: snapshot() });190 state.disconnecting = false;191 return true;192 }193194 return {195 get connected() { return state.connected; },196 get connecting() { return state.connecting; },197 get disconnecting() { return state.disconnecting; },198 get developmentMode() { return state.developmentMode; },199 get inputCapabilities() { return { ...state.inputCapabilities }; },200 get mediaTransport() { return state.mediaTransport; },201 get controlProtocol() { return state.controlProtocol; },202 snapshot,203 setDevelopmentMode(enabled) {204 state.developmentMode = enabled === true;205 },206 updateInputCapabilities(result) {207 state.inputCapabilities = normalizeInputCapabilities({208 relative_pointer_available: result.relative_pointer,209 pointer_position_feedback_available: result.pointer_position_feedback,210 absolute_pointer_available: result.absolute_pointer,211 keyboard_available: result.keyboard,212 text_available: result.text,213 gamepad_available: result.gamepad,214 control_available: result.control,215 });216 return { ...state.inputCapabilities };217 },218 connect,219 disconnect,220 };221}