diff --git a/lib/shared/timeouts.js b/lib/shared/timeouts.js --- a/lib/shared/timeouts.js +++ b/lib/shared/timeouts.js @@ -44,6 +44,9 @@ // Client-side timeout duration for certain Tunnelbroker WebSocket requests. export const tunnelbrokerRequestTimeout = 10000; // in milliseconds +// Client-side timeout duration for certain Farcaster requests. +export const farcasterRequestTimeout = 5000; // in milliseconds + // This controls how long the client waits before trying to reconnect a // disconnected Tunnelbroker socket. export const clientTunnelbrokerSocketReconnectDelay = 3000; diff --git a/lib/tunnelbroker/tunnelbroker-context.js b/lib/tunnelbroker/tunnelbroker-context.js --- a/lib/tunnelbroker/tunnelbroker-context.js +++ b/lib/tunnelbroker/tunnelbroker-context.js @@ -15,11 +15,13 @@ import { DMOpsQueueHandler } from '../shared/dm-ops/dm-ops-queue-handler.react.js'; import { IdentityClientContext } from '../shared/identity-client-context.js'; import { + farcasterRequestTimeout, tunnelbrokerHeartbeatTimeout, tunnelbrokerRequestTimeout, } from '../shared/timeouts.js'; import { isWebPlatform } from '../types/device-types.js'; import type { MessageSentStatus } from '../types/tunnelbroker/device-to-tunnelbroker-request-status-types.js'; +import type { FarcasterAPIRequest } from '../types/tunnelbroker/farcaster-messages-types.js'; import type { MessageReceiveConfirmation } from '../types/tunnelbroker/message-receive-confirmation-types.js'; import type { MessageToDeviceRequest } from '../types/tunnelbroker/message-to-device-request-types.js'; import type { MessageToTunnelbrokerRequest } from '../types/tunnelbroker/message-to-tunnelbroker-request-types.js'; @@ -40,6 +42,11 @@ import type { Heartbeat } from '../types/websocket/heartbeat-types.js'; import { getConfig } from '../utils/config.js'; import { getContentSigningKey } from '../utils/crypto-utils.js'; +import { + FarcasterAPIError, + FarcasterMissingTokenError, +} from '../utils/errors.js'; +import { promiseWithTimeout } from '../utils/promises.js'; import { useSelector } from '../utils/redux-utils.js'; import sleep from '../utils/sleep.js'; @@ -57,6 +64,13 @@ +reject: (error: string) => void, }; type Promises = { [clientMessageID: string]: PromiseCallbacks }; +type PromiseRequestCallbacks = { + +resolve: (res: string) => void, + +reject: ( + error: string | FarcasterAPIError | FarcasterMissingTokenError | Error, + ) => void, +}; +type FarcasterRequests = { [requestID: string]: PromiseRequestCallbacks }; export type TunnelbrokerSocketState = | { @@ -73,6 +87,15 @@ retryCount: 0, }; +type SendFarcasterRequestPayload = $Diff< + FarcasterAPIRequest, + { + +type: 'FarcasterAPIRequest', + +requestID: string, + +userID: string, + }, +>; + type TunnelbrokerContextType = { +sendMessageToDevice: ( message: TunnelbrokerClientMessageToDevice, @@ -85,6 +108,9 @@ +socketState: TunnelbrokerSocketState, +setUnauthorizedDeviceID: (unauthorizedDeviceID: ?string) => void, +confirmMessageToTunnelbroker: (messageID: string) => void, + +sendFarcasterRequest: ( + request: SendFarcasterRequestPayload, + ) => Promise, }; const TunnelbrokerContext: React.Context = @@ -172,6 +198,7 @@ const socket = React.useRef(null); const socketSessionCounter = React.useRef(0); const promises = React.useRef({}); + const requests = React.useRef({}); const heartbeatTimeoutID = React.useRef(); const identityContext = React.useContext(IdentityClientContext); @@ -356,6 +383,37 @@ type: deviceToTunnelbrokerMessageTypes.HEARTBEAT, }; socket.current?.send(JSON.stringify(heartbeat)); + } else if ( + message.type === + tunnelbrokerToDeviceMessageTypes.FARCASTER_API_RESPONSE + ) { + const { requestID, response } = message; + if (response.type === 'Success') { + requests.current[requestID]?.resolve(response.data); + } else if (response.type === 'ErrorResponse') { + requests.current[requestID].reject( + new FarcasterAPIError( + response.data.status, + response.data.message, + ), + ); + } else if (response.type === 'Error') { + requests.current[requestID].reject(new Error(response.data)); + } else if (response.type === 'SerializationError') { + console.log( + 'SerializationError for Farcaster request: ', + response.data, + ); + requests.current[requestID].reject(new Error(response.type)); + } else if (response.type === 'InvalidRequest') { + console.log('Tunnelbroker recorded InvalidRequest'); + requests.current[requestID].reject(new Error(response.type)); + } else if (response.type === 'MissingFarcasterDCsToken') { + requests.current[requestID].reject( + new FarcasterMissingTokenError(response.type), + ); + } + delete requests.current[requestID]; } }; @@ -456,6 +514,44 @@ [sendMessage], ); + const sendFarcasterRequest: ( + request: SendFarcasterRequestPayload, + ) => Promise = React.useCallback( + request => { + const resultPromise: Promise = new Promise((resolve, reject) => { + const socketActive = socketStateRef.current.connected && socket.current; + if (!shouldBeClosed && !socketActive) { + throw new Error('Tunnelbroker not connected'); + } + invariant(userID, 'userID should be defined'); + + const requestID = uuid.v4(); + requests.current[requestID] = { + resolve, + reject, + }; + const fullRequest: FarcasterAPIRequest = { + ...request, + type: deviceToTunnelbrokerMessageTypes.FARCASTER_API_REQUEST, + requestID, + userID, + }; + if (socketActive) { + socket.current?.send(JSON.stringify(fullRequest)); + } else { + secondaryTunnelbrokerConnection?.sendMessage(requestID, fullRequest); + } + }); + + return promiseWithTimeout( + resultPromise, + farcasterRequestTimeout, + 'Farcaster request', + ); + }, + [secondaryTunnelbrokerConnection, shouldBeClosed, userID], + ); + React.useEffect( () => secondaryTunnelbrokerConnection?.onSendMessage( @@ -551,15 +647,17 @@ removeListener, setUnauthorizedDeviceID, confirmMessageToTunnelbroker, + sendFarcasterRequest, }), [ + addListener, + confirmMessageToTunnelbroker, + removeListener, + sendFarcasterRequest, sendMessageToDevice, - sendNotif, sendMessageToTunnelbroker, + sendNotif, socketState, - addListener, - removeListener, - confirmMessageToTunnelbroker, ], ); diff --git a/lib/utils/errors.js b/lib/utils/errors.js --- a/lib/utils/errors.js +++ b/lib/utils/errors.js @@ -77,6 +77,21 @@ } } +class FarcasterAPIError extends ExtendableError { + status: number; + + constructor(status: number, message: string) { + super(message); + this.status = status; + } +} + +class FarcasterMissingTokenError extends ExtendableError { + constructor(message: string) { + super(message); + } +} + function getMessageForException(e: mixed): ?string { if (typeof e === 'string') { return e; @@ -100,4 +115,6 @@ SocketTimeout, SendMessageError, BackupIsNewerError, + FarcasterAPIError, + FarcasterMissingTokenError, };