UNPKG

magicbell

Version:
188 lines 6.85 kB
import '../lib/signal-polyfill.js'; import WebSocket from 'reconnecting-websocket'; import * as log from '../client/log.js'; import { ASYNC_ITERATOR_SYMBOL, makeForEach } from '../client/paginate.js'; import { generateID } from '../lib/crypto.js'; function getWebSocket() { if (typeof WebSocket === 'function') { return WebSocket; } const moduleDefault = WebSocket.default; if (typeof moduleDefault === 'function') { return moduleDefault; } throw Error('WebSocket is not defined'); } export function getSessionId() { if (typeof sessionStorage === 'undefined') { return generateID(64); } // sessionStorage gets cleared when the page session ends. A page // session lasts for as long as the browser is open and survives // over page reloads and restores. Opening a page in a new tab or // window will cause a new session to be initiated. This gives us // a stable ID per tab, and different ID's across tabs. const stored = sessionStorage.getItem('magicbell--realtime-token'); if (stored) return stored; const id = generateID(64); sessionStorage.setItem('magicbell--realtime-token', id); return id; } export function createListener(client, options) { let socket = null; let origin = null; const socketURL = options.socketURL || 'wss://ws.magicbell.com'; const messages = []; let resolve; let activeCount = 0; let reconnecting = false; const pushMessage = (p) => { messages.push(p); if (resolve) { resolve(); resolve = null; } }; async function getToken(requestOptions) { const data = { inbox: { token: getSessionId() } }; const res = await client.request({ method: 'POST', path: '/channels/in_app/inbox/tokens', data }, requestOptions); return res['in_app/inbox'].token; } async function getUrl(requestOptions) { const token = await getToken(requestOptions); const url = new URL(socketURL); url.searchParams.set('api_key', options.apiKey); url.searchParams.set('token', token); origin = url.origin; return url.toString(); } // accept callback or yield async function connect(requestOptions) { if (activeCount < 1) return; const WS = getWebSocket(); if (socket && socket.readyState !== WS.CLOSED) { return; } // new connection, flush all messages so we don't auto close this immediately // because of stuck { done: true } messages in React.StrictMode messages.length = 0; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore socket = new WS(() => getUrl(requestOptions)); socket.addEventListener('open', () => { log.info('socket: open'); }); const handleError = (err) => { const isTokenErr = err?.code >= 40140 && err.code < 40150; if (isTokenErr) { reconnecting = true; socket?.close(); socket = null; void connect(requestOptions); return; } if (/invalid channel id/i.test(err?.message)) { socket?.close(); socket = null; pushMessage({ value: null, done: true }); return; } log.error('socket: error', err); }; socket.addEventListener('message', (event) => { if (event.origin !== origin) return; if (!('data' in event)) return; if (event.type !== 'message') return; try { const data = JSON.parse(event.data); if (data.type === 'error') { handleError(data); return; } if (data.type === 'close') { return pushMessage({ value: null, done: true }); } log.debug('socket: received message', data); pushMessage({ value: data, done: false }); } catch (e) { log.error('socket: failed to parse message', { data: event.data, error: e, }); } }); socket.addEventListener('close', () => { log.info('socket: close'); if (reconnecting) { reconnecting = false; return; } return pushMessage({ value: null, done: true }); }); // handle connection errors socket.addEventListener('error', (event) => { if (!('data' in event)) { log.error('socket: error', event); return; } try { const err = JSON.parse(event.data); handleError(err); } catch (error) { log.error('socket: error', { error, event }); } }); } function listen(options) { activeCount++; void connect(options); const asyncIteratorNext = async () => { let event = null; while (!event) { if (!messages.length) await new Promise((r) => (resolve = r)); event = messages.pop(); } // It's weird that `event` can be undefined? We don't push empty messages // This happens when running in <React.StrictMode />. I guess there are // two instances resolving the promise above, the second resolve results // in no content. See this github pr for more context: // https://github.com/magicbell/magicbell-js/pull/189 if (!event) return { done: false, value: '' }; if (event.done && socket) socket.close(); return event; }; const dispose = () => { activeCount--; if (activeCount < 1) { socket?.close(); socket = null; } // push to resolve async iterators, return for sync ones pushMessage({ done: true, value: undefined }); return { done: true, value: undefined }; }; const forEach = makeForEach(asyncIteratorNext, dispose); const autoPaginationMethods = { forEach, close: () => void dispose(), next: asyncIteratorNext, return: dispose, [ASYNC_ITERATOR_SYMBOL]: () => { return autoPaginationMethods; }, }; return autoPaginationMethods; } return listen; } //# sourceMappingURL=listen.js.map