UNPKG

magicbell

Version:
142 lines 5.69 kB
import '../lib/signal-polyfill.js'; import ky from '@smeijer/ky'; import { Cache } from '../client/cache.js'; import { debug } from '../client/log.js'; import { ASYNC_ITERATOR_SYMBOL, makeForEach } from '../client/paginate.js'; export function createListener(client) { let eventSource; let channels; let lastEvent; let configPromise; const cache = new Cache(); const messages = []; let resolve; let activeCount = 0; const pushMessage = (p) => { messages.push(p); if (resolve) { resolve(); resolve = null; } }; // accept callback or yield async function connect(options) { activeCount++; // invoke config request in the background, as we only need it after the ably authentication if (!channels && !configPromise) { configPromise = client .request({ method: 'GET', path: '/config' }, options) .then((x) => (channels = x.ws.channel)); } const auth = await client.request({ method: 'POST', path: '/ws/auth' }, options); const tokenUrl = new URL(`https://rest.ably.io/keys/${auth.keyName}/requestToken`); // authenticate against ably const cacheKey = cache.getRequestKey({ url: tokenUrl, method: 'POST', data: auth }); let authRequest = cache.get(cacheKey); if (!authRequest) { authRequest = ky(tokenUrl, { method: 'POST', json: auth, }).then((x) => x.json()); cache.set(cacheKey, authRequest); } const { token } = await authRequest; // make sure that the config request has finished await configPromise; // establish a connection with that token const url = new URL('https://realtime.ably.io/sse'); url.searchParams.append('v', '1.1'); url.searchParams.append('accessToken', token); url.searchParams.append('channels', channels); url.searchParams.append('heartbeats', 'true'); if (lastEvent) { url.searchParams.append('lastEvent', lastEvent); } if (eventSource) { eventSource.close(); } // dispose could've been called while we were waiting for the config request to finish if (activeCount < 1) return; // new eventsource, flush all messages so we don't auto close this immediately // because of stuck { done: true } messages in React.StrictMode messages.length = 0; eventSource = new EventSource(url.toString()); // handle incoming messages eventSource.addEventListener('message', (event) => { // event.origin can be undefined in react-native (devmode?) if (event.origin && event.origin !== url.origin) return; lastEvent = event.lastEventId; if (!('data' in event)) return; const message = JSON.parse(event.data); if (message.type === 'close') { return pushMessage({ value: null, done: true }); } message.data = message.encoding === 'json' ? JSON.parse(message.data) : message.data; pushMessage({ value: message, done: false }); }); // handle close eventSource.addEventListener('close', () => { return pushMessage({ value: null, done: true }); }); // handle connection errors eventSource.addEventListener('error', (msg) => { const err = 'data' in msg ? JSON.parse(msg.data) : {}; const isTokenErr = err.code >= 40140 && err.code < 40150; if (isTokenErr) { eventSource.close(); connect(options); } else if (/invalid channel id/i.test(err.message)) { eventSource.close(); pushMessage({ value: null, done: true }); } else { debug('sse error', msg); } }); } function listen(options) { void connect(options); const asyncIteratorNext = async () => { let event = null; // 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 while (!event) { if (!messages.length) await new Promise((r) => (resolve = r)); event = messages.pop(); } if (!event) return { done: false, value: '' }; if (event.done && eventSource) eventSource.close(); return event; }; const dispose = () => { activeCount--; eventSource?.close(); // 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