UNPKG

svelte-statusable

Version:

Super tiny, simple to use SvelteJS store to control your application status.

182 lines (156 loc) 4.87 kB
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 };