UNPKG

airdcpp-apisocket

Version:
371 lines 13.8 kB
import ApiConstants from './ApiConstants.js'; import SocketLogger from './SocketLogger.js'; import SocketSubscriptionHandler from './SocketSubscriptionHandler.js'; import SocketRequestHandler from './SocketRequestHandler.js'; import invariant from 'invariant'; import Promise from './Promise.js'; // CONSTANTS const defaultOptions = { autoReconnect: true, reconnectInterval: 10, userSession: false, }; const ApiSocket = (userOptions, WebSocketImpl) => { const options = { ...defaultOptions, ...userOptions }; let ws = null; let authToken = null; let socket = null; let reconnectTimer; let forceNoAutoConnect = true; let connectCallback = null; let connectedCallback = null; let sessionResetCallback = null; let disconnectCallback = null; let disconnectedCallback = null; const logger = SocketLogger(options); const subscriptions = SocketSubscriptionHandler(() => socket, logger, options); const requests = SocketRequestHandler(() => socket, logger, options); invariant(userOptions.url, '"url" must be defined in settings object'); const resetSession = () => { if (authToken) { if (sessionResetCallback) { sessionResetCallback(); } authToken = null; } }; const onClosed = (event) => { if (event.wasClean) { logger.info('Websocket was closed normally'); } else { logger.error(`Websocket failed: ${event.reason} (code: ${event.code})`); } requests.onSocketDisconnected(); subscriptions.onSocketDisconnected(); ws = null; if (disconnectedCallback) { disconnectedCallback(event.reason, event.code, event.wasClean); } if (authToken && options.autoReconnect && !forceNoAutoConnect) { setTimeout(() => { if (forceNoAutoConnect) { return; } socket.reconnect() .catch((error) => { logger.error('Reconnect failed for a closed socket', error.message); }); }); } }; const onMessage = (event) => { const messageObj = JSON.parse(event.data); if (messageObj.callback_id) { // Callback requests.handleMessage(messageObj); } else { // Listener message subscriptions.handleMessage(messageObj); } }; const setSocketHandlers = () => { ws.onerror = (event) => { logger.error(`Websocket failed: ${event.reason}`); }; ws.onclose = onClosed; ws.onmessage = onMessage; }; // Connect handler for creation of new session const handlePasswordLogin = (username = options.username, password = options.password) => { if (!username) { throw new Error('"username" option was not supplied for authentication'); } if (!password) { throw new Error('"password" option was not supplied for authentication'); } const data = { username, password, grant_type: 'password', }; return requests.postAuthenticate(ApiConstants.LOGIN_URL, data); }; const handleRefreshTokenLogin = (refreshToken) => { if (!refreshToken) { throw new Error('"refreshToken" option was not supplied for authentication'); } const data = { refresh_token: refreshToken, grant_type: 'refresh_token', }; return requests.postAuthenticate(ApiConstants.LOGIN_URL, data); }; // Connect handler for associating socket with an existing session token const handleAuthorizeToken = () => { const data = { auth_token: authToken, }; return requests.postAuthenticate(ApiConstants.CONNECT_URL, data); }; // Called after a successful authentication request const onSocketAuthenticated = (data) => { if (!authToken) { // New session logger.info('Login succeed'); authToken = data.auth_token; } else { // Existing session logger.info('Socket associated with an existing session'); } if (connectedCallback) { // Catch separately as we don't want an infinite reconnect loop try { connectedCallback(data); } catch (e) { console.error('Error in socket connect handler', e.message); } requests.onSocketConnected(); } }; // Send API authentication and handle the result // Authentication handler should send the actual authentication request const authenticate = (resolve, reject, authenticationHandler, reconnectHandler) => { authenticationHandler() .then((data) => { onSocketAuthenticated(data); resolve(data); }) .catch((error) => { if (error.code) { if (authToken && error.code === 400 && options.autoReconnect) { // The session was lost (most likely the client was restarted) logger.info('Session lost, re-sending credentials'); resetSession(); authenticate(resolve, reject, handlePasswordLogin, reconnectHandler); return; } else if (error.code === 401) { // Invalid credentials, reset the token if we were reconnecting to avoid an infinite loop resetSession(); } // Authentication was rejected socket.disconnect(undefined, 'Authentication failed'); } else { // Socket was disconnected during the authentication logger.info('Socket disconnected during authentication, reconnecting'); reconnectHandler(); return; } reject(error); }); }; // Authentication handler should send the actual authentication request const connectInternal = (resolve, reject, authenticationHandler, reconnectOnFailure = true) => { ws = new WebSocketImpl(options.url); const scheduleReconnect = () => { ws = null; if (!reconnectOnFailure) { reject('Cannot connect to the server'); return; } reconnectTimer = setTimeout(() => { logger.info('Socket reconnecting'); connectInternal(resolve, reject, authenticationHandler, reconnectOnFailure); }, options.reconnectInterval * 1000); }; ws.onopen = () => { logger.info('Socket connected'); setSocketHandlers(); authenticate(resolve, reject, authenticationHandler, scheduleReconnect); }; ws.onerror = (event) => { logger.error('Connecting socket failed (network/system error, most likely the server is unreachable)'); scheduleReconnect(); }; }; // Authentication handler should send the actual authentication request const startConnect = (authenticationHandler, reconnectOnFailure) => { forceNoAutoConnect = false; if (connectCallback) { connectCallback(); } return new Promise((resolve, reject) => { logger.info(`Starting socket connect to ${userOptions.url}`); connectInternal(resolve, reject, authenticationHandler, reconnectOnFailure); }); }; // Is the socket connected and authorized? const isConnected = () => { return !!(ws && ws.readyState === (ws.OPEN || 1) && authToken); }; // Is the socket connected but not possibly authorized? const isConnecting = () => { return !!(ws && !isConnected()); }; // Socket exists const isActive = () => { return !!ws; }; const disableReconnect = () => { clearTimeout(reconnectTimer); forceNoAutoConnect = true; }; const waitDisconnected = (timeoutMs = 2000) => { const checkInterval = 50; const maxAttempts = timeoutMs > 0 ? timeoutMs / checkInterval : 0; return new Promise((resolve, reject) => { let attempts = 0; const wait = () => { if (isActive()) { if (attempts >= maxAttempts) { logger.error(`Socket disconnect timed out after ${timeoutMs} ms`); reject(new Error('Socket disconnect timed out')); } else { setTimeout(wait, checkInterval); attempts++; } } else { resolve(); } }; wait(); }); }; // Disconnects the socket but keeps the session token const disconnect = (autoConnect = false, reason = 'Manually disconnected by the client') => { if (!ws) { if (!forceNoAutoConnect) { if (!autoConnect) { logger.verbose('Disconnecting a closed socket with auto reconnect enabled (cancel reconnect)'); disableReconnect(); } else { logger.verbose('Attempting to disconnect a closed socket with auto reconnect enabled (continue connecting)'); } } else { logger.warn('Attempting to disconnect a closed socket (ignore)'); } return; } if (disconnectCallback) { disconnectCallback(reason); } logger.info('Disconnecting socket'); if (!autoConnect) { disableReconnect(); } ws.close(1000, reason); }; socket = { // Start connect // Username and password are not required if those are available in socket options connect: (username, password, reconnectOnFailure = true) => { if (isActive()) { throw new Error('Connect may only be used for a closed socket'); } resetSession(); return startConnect(() => handlePasswordLogin(username, password), reconnectOnFailure); }, connectRefreshToken: (refreshToken, reconnectOnFailure = true) => { if (isActive()) { throw new Error('Connect may only be used for a closed socket'); } resetSession(); return startConnect(() => handleRefreshTokenLogin(refreshToken), reconnectOnFailure); }, // Connect and attempt to associate the socket with an existing session reconnect: (token = undefined, reconnectOnFailure = true) => { if (isActive()) { throw new Error('Reconnect may only be used for a closed socket'); } if (token) { authToken = token; } if (!authToken) { throw new Error('No session token available for reconnecting'); } logger.info('Reconnecting socket'); return startConnect(handleAuthorizeToken, reconnectOnFailure); }, // Remove the associated API session and close the socket logout: () => { const resolver = Promise.pending(); socket.delete(ApiConstants.LOGOUT_URL) .then((data) => { logger.info('Logout succeed'); resetSession(); resolver.resolve(data); // Don't fire the disconnected event before resolver actions are handled disconnect(undefined, 'Logged out'); }) .catch((error) => { logger.error('Logout failed', error); resolver.reject(error); }); return resolver.promise; }, disconnect, isConnecting, isConnected, isActive, logger, waitDisconnected, // Function to call each time a connect attempt is initiated set onConnect(handler) { connectCallback = handler; }, // Function to call each time the socket has been connected (and authorized) set onConnected(handler) { connectedCallback = handler; }, // Function to call each time the stored session token was reset (manual logout/rejected reconnect) set onSessionReset(handler) { sessionResetCallback = handler; }, // Function to call each time the socket is being manually disconnected set onDisconnect(handler) { disconnectCallback = handler; }, // Function to call each time the socket has been disconnected set onDisconnected(handler) { disconnectedCallback = handler; }, get onConnect() { return connectCallback; }, get onConnected() { return connectedCallback; }, get onSessionReset() { return sessionResetCallback; }, get onDisconnect() { return disconnectCallback; }, get onDisconnected() { return disconnectedCallback; }, get nativeSocket() { return ws; }, get url() { return userOptions.url; }, ...subscriptions.socket, ...requests.socket, }; return socket; }; export default ApiSocket; //# sourceMappingURL=SocketBase.js.map