magicbell
Version:
MagicBell API wrapper
146 lines • 5.93 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.createListener = createListener;
const tslib_1 = require("tslib");
require("../lib/signal-polyfill.js");
const ky_1 = tslib_1.__importDefault(require("@smeijer/ky"));
const cache_js_1 = require("../client/cache.js");
const log_js_1 = require("../client/log.js");
const paginate_js_1 = require("../client/paginate.js");
function createListener(client) {
let eventSource;
let channels;
let lastEvent;
let configPromise;
const cache = new cache_js_1.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 = (0, ky_1.default)(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 {
(0, log_js_1.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 = (0, paginate_js_1.makeForEach)(asyncIteratorNext, dispose);
const autoPaginationMethods = {
forEach,
close: () => void dispose(),
next: asyncIteratorNext,
return: dispose,
[paginate_js_1.ASYNC_ITERATOR_SYMBOL]: () => {
return autoPaginationMethods;
},
};
return autoPaginationMethods;
}
return listen;
}
//# sourceMappingURL=listen.js.map