svelte-statusable
Version:
Super tiny, simple to use SvelteJS store to control your application status.
182 lines (156 loc) • 4.87 kB
JavaScript
function noop() { }
function safe_not_equal(a, b) {
return a != a ? b == b : a !== b || ((a && typeof a === 'object') || typeof a === 'function');
}
Promise.resolve();
const subscriber_queue = [];
/**
* Creates a `Readable` store that allows reading by subscription.
* @param value initial value
* @param {StartStopNotifier}start start and stop notifications for subscriptions
*/
function readable(value, start) {
return {
subscribe: writable(value, start).subscribe
};
}
/**
* Create a `Writable` store that allows both updating and reading by subscription.
* @param {*=}value initial value
* @param {StartStopNotifier=}start start and stop notifications for subscriptions
*/
function writable(value, start = noop) {
let stop;
const subscribers = new Set();
function set(new_value) {
if (safe_not_equal(value, new_value)) {
value = new_value;
if (stop) { // store is ready
const run_queue = !subscriber_queue.length;
for (const subscriber of subscribers) {
subscriber[1]();
subscriber_queue.push(subscriber, value);
}
if (run_queue) {
for (let i = 0; i < subscriber_queue.length; i += 2) {
subscriber_queue[i][0](subscriber_queue[i + 1]);
}
subscriber_queue.length = 0;
}
}
}
}
function update(fn) {
set(fn(value));
}
function subscribe(run, invalidate = noop) {
const subscriber = [run, invalidate];
subscribers.add(subscriber);
if (subscribers.size === 1) {
stop = start(set) || noop;
}
run(value);
return () => {
subscribers.delete(subscriber);
if (subscribers.size === 0) {
stop();
stop = null;
}
};
}
return { set, update, subscribe };
}
const hasWindow = typeof window !== 'undefined';
const hasNavigator = typeof navigator !== 'undefined';
const hasDocument = typeof document !== 'undefined';
const hasEventSource = typeof EventSource !== 'undefined';
const hasAbortController = typeof AbortController !== 'undefined';
const defaultRetry = 10000;
function setIntervalImmediately(func, interval) {
func();
return setInterval(func, interval);
}
function heartbeat({ url, abort = 0, payload = false, retry, ...options }) {
/*eslint no-unused-vars: "off"*/
if (abort && hasAbortController) {
const ac = new AbortController();
options.signal = ac.signal;
setTimeout(() => ac.abort(), abort);
}
return fetch(url, options)
.then((res) => (payload ? res.json() : res.type === 'opaque' || res.ok))
.catch(() => false);
}
function statusable({ ping, sse }) {
let value = {
online: hasNavigator ? navigator.onLine : true,
hidden: hasDocument ? document.hidden : false,
heartbeat: !hasWindow, // for SSR
stream: !hasWindow,
};
if (typeof ping === 'string') {
ping = {
url: ping,
method: 'HEAD',
cache: 'no-cache',
credentials: 'omit',
referrerPolicy: 'no-referrer',
};
}
if (typeof sse === 'string') {
sse = {
url: sse,
withCredentials: false,
};
}
return readable(value, (set) => {
if (!hasWindow || !hasNavigator || !hasDocument) return;
let es;
let interval;
function assign(key, val) {
if (value[key] === val) return;
set((value = { ...value, [key]: val }));
}
function online() {
assign('online', navigator.onLine);
}
function visibility() {
assign('hidden', document.hidden);
}
function stream(e) {
assign('stream', e.target.readyState === EventSource.OPEN);
}
if (sse && hasEventSource) {
es = new EventSource(sse.url, { withCredentials: sse.withCredentials });
es.addEventListener('open', stream);
es.addEventListener('error', stream);
if (sse.event) {
es.addEventListener(sse.event, stream);
}
assign('stream', es.readyState === EventSource.OPEN);
}
if (ping) {
interval = setIntervalImmediately(() => {
if (document.hidden || !navigator.onLine) return;
heartbeat(ping).then((heartbeat) => assign('heartbeat', heartbeat));
}, ping.retry || defaultRetry);
}
window.addEventListener('online', online);
window.addEventListener('offline', online);
window.addEventListener('visibilitychange', visibility);
return () => {
window.removeEventListener('online', online);
window.removeEventListener('offline', online);
window.removeEventListener('visibilitychange', visibility);
if (es) {
es.removeEventListener('open', stream);
es.removeEventListener('error', stream);
if (sse.event) {
es.removeEventListener(sse.event, stream);
}
}
clearInterval(interval);
};
});
}
export { statusable };