UNPKG

react-query

Version:

Hooks for managing, caching and syncing asynchronous and remote data in React

1,057 lines (911 loc) 29.5 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.ReactQueryBroadcastQueryClientExperimental = {})); })(this, (function (exports) { 'use strict'; /** * returns true if the given object is a promise */ function isPromise(obj) { if (obj && typeof obj.then === 'function') { return true; } else { return false; } } function sleep(time) { if (!time) time = 0; return new Promise(function (res) { return setTimeout(res, time); }); } function randomInt(min, max) { return Math.floor(Math.random() * (max - min + 1) + min); } /** * https://stackoverflow.com/a/8084248 */ function randomToken() { return Math.random().toString(36).substring(2); } var lastMs = 0; var additional = 0; /** * returns the current time in micro-seconds, * WARNING: This is a pseudo-function * Performance.now is not reliable in webworkers, so we just make sure to never return the same time. * This is enough in browsers, and this function will not be used in nodejs. * The main reason for this hack is to ensure that BroadcastChannel behaves equal to production when it is used in fast-running unit tests. */ function microSeconds$4() { var ms = new Date().getTime(); if (ms === lastMs) { additional++; return ms * 1000 + additional; } else { lastMs = ms; additional = 0; return ms * 1000; } } /** * copied from the 'detect-node' npm module * We cannot use the module directly because it causes problems with rollup * @link https://github.com/iliakan/detect-node/blob/master/index.js */ var isNode = Object.prototype.toString.call(typeof process !== 'undefined' ? process : 0) === '[object process]'; var microSeconds$3 = microSeconds$4; var type$3 = 'native'; function create$3(channelName) { var state = { messagesCallback: null, bc: new BroadcastChannel(channelName), subFns: [] // subscriberFunctions }; state.bc.onmessage = function (msg) { if (state.messagesCallback) { state.messagesCallback(msg.data); } }; return state; } function close$3(channelState) { channelState.bc.close(); channelState.subFns = []; } function postMessage$3(channelState, messageJson) { channelState.bc.postMessage(messageJson, false); } function onMessage$3(channelState, fn) { channelState.messagesCallback = fn; } function canBeUsed$3() { /** * in the electron-renderer, isNode will be true even if we are in browser-context * so we also check if window is undefined */ if (isNode && typeof window === 'undefined') return false; if (typeof BroadcastChannel === 'function') { if (BroadcastChannel._pubkey) { throw new Error('BroadcastChannel: Do not overwrite window.BroadcastChannel with this module, this is not a polyfill'); } return true; } else return false; } function averageResponseTime$3() { return 150; } var NativeMethod = { create: create$3, close: close$3, onMessage: onMessage$3, postMessage: postMessage$3, canBeUsed: canBeUsed$3, type: type$3, averageResponseTime: averageResponseTime$3, microSeconds: microSeconds$3 }; /** * this is a set which automatically forgets * a given entry when a new entry is set and the ttl * of the old one is over * @constructor */ var ObliviousSet = function ObliviousSet(ttl) { var set = new Set(); var timeMap = new Map(); this.has = set.has.bind(set); this.add = function (value) { timeMap.set(value, now()); set.add(value); _removeTooOldValues(); }; this.clear = function () { set.clear(); timeMap.clear(); }; function _removeTooOldValues() { var olderThen = now() - ttl; var iterator = set[Symbol.iterator](); while (true) { var value = iterator.next().value; if (!value) return; // no more elements var time = timeMap.get(value); if (time < olderThen) { timeMap["delete"](value); set["delete"](value); } else { // we reached a value that is not old enough return; } } } }; function now() { return new Date().getTime(); } var ObliviousSet$1 = ObliviousSet; function fillOptionsWithDefaults() { var originalOptions = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; var options = JSON.parse(JSON.stringify(originalOptions)); // main if (typeof options.webWorkerSupport === 'undefined') options.webWorkerSupport = true; // indexed-db if (!options.idb) options.idb = {}; // after this time the messages get deleted if (!options.idb.ttl) options.idb.ttl = 1000 * 45; if (!options.idb.fallbackInterval) options.idb.fallbackInterval = 150; // handles abrupt db onclose events. if (originalOptions.idb && typeof originalOptions.idb.onclose === 'function') options.idb.onclose = originalOptions.idb.onclose; // localstorage if (!options.localstorage) options.localstorage = {}; if (!options.localstorage.removeTimeout) options.localstorage.removeTimeout = 1000 * 60; // custom methods if (originalOptions.methods) options.methods = originalOptions.methods; // node if (!options.node) options.node = {}; if (!options.node.ttl) options.node.ttl = 1000 * 60 * 2; // 2 minutes; if (typeof options.node.useFastPath === 'undefined') options.node.useFastPath = true; return options; } /** * this method uses indexeddb to store the messages * There is currently no observerAPI for idb * @link https://github.com/w3c/IndexedDB/issues/51 */ var microSeconds$2 = microSeconds$4; var DB_PREFIX = 'pubkey.broadcast-channel-0-'; var OBJECT_STORE_ID = 'messages'; var type$2 = 'idb'; function getIdb() { if (typeof indexedDB !== 'undefined') return indexedDB; if (typeof window !== 'undefined') { if (typeof window.mozIndexedDB !== 'undefined') return window.mozIndexedDB; if (typeof window.webkitIndexedDB !== 'undefined') return window.webkitIndexedDB; if (typeof window.msIndexedDB !== 'undefined') return window.msIndexedDB; } return false; } function createDatabase(channelName) { var IndexedDB = getIdb(); // create table var dbName = DB_PREFIX + channelName; var openRequest = IndexedDB.open(dbName, 1); openRequest.onupgradeneeded = function (ev) { var db = ev.target.result; db.createObjectStore(OBJECT_STORE_ID, { keyPath: 'id', autoIncrement: true }); }; var dbPromise = new Promise(function (res, rej) { openRequest.onerror = function (ev) { return rej(ev); }; openRequest.onsuccess = function () { res(openRequest.result); }; }); return dbPromise; } /** * writes the new message to the database * so other readers can find it */ function writeMessage(db, readerUuid, messageJson) { var time = new Date().getTime(); var writeObject = { uuid: readerUuid, time: time, data: messageJson }; var transaction = db.transaction([OBJECT_STORE_ID], 'readwrite'); return new Promise(function (res, rej) { transaction.oncomplete = function () { return res(); }; transaction.onerror = function (ev) { return rej(ev); }; var objectStore = transaction.objectStore(OBJECT_STORE_ID); objectStore.add(writeObject); }); } function getMessagesHigherThan(db, lastCursorId) { var objectStore = db.transaction(OBJECT_STORE_ID).objectStore(OBJECT_STORE_ID); var ret = []; function openCursor() { // Occasionally Safari will fail on IDBKeyRange.bound, this // catches that error, having it open the cursor to the first // item. When it gets data it will advance to the desired key. try { var keyRangeValue = IDBKeyRange.bound(lastCursorId + 1, Infinity); return objectStore.openCursor(keyRangeValue); } catch (e) { return objectStore.openCursor(); } } return new Promise(function (res) { openCursor().onsuccess = function (ev) { var cursor = ev.target.result; if (cursor) { if (cursor.value.id < lastCursorId + 1) { cursor["continue"](lastCursorId + 1); } else { ret.push(cursor.value); cursor["continue"](); } } else { res(ret); } }; }); } function removeMessageById(db, id) { var request = db.transaction([OBJECT_STORE_ID], 'readwrite').objectStore(OBJECT_STORE_ID)["delete"](id); return new Promise(function (res) { request.onsuccess = function () { return res(); }; }); } function getOldMessages(db, ttl) { var olderThen = new Date().getTime() - ttl; var objectStore = db.transaction(OBJECT_STORE_ID).objectStore(OBJECT_STORE_ID); var ret = []; return new Promise(function (res) { objectStore.openCursor().onsuccess = function (ev) { var cursor = ev.target.result; if (cursor) { var msgObk = cursor.value; if (msgObk.time < olderThen) { ret.push(msgObk); //alert("Name for SSN " + cursor.key + " is " + cursor.value.name); cursor["continue"](); } else { // no more old messages, res(ret); return; } } else { res(ret); } }; }); } function cleanOldMessages(db, ttl) { return getOldMessages(db, ttl).then(function (tooOld) { return Promise.all(tooOld.map(function (msgObj) { return removeMessageById(db, msgObj.id); })); }); } function create$2(channelName, options) { options = fillOptionsWithDefaults(options); return createDatabase(channelName).then(function (db) { var state = { closed: false, lastCursorId: 0, channelName: channelName, options: options, uuid: randomToken(), /** * emittedMessagesIds * contains all messages that have been emitted before * @type {ObliviousSet} */ eMIs: new ObliviousSet$1(options.idb.ttl * 2), // ensures we do not read messages in parrallel writeBlockPromise: Promise.resolve(), messagesCallback: null, readQueuePromises: [], db: db }; /** * Handle abrupt closes that do not originate from db.close(). * This could happen, for example, if the underlying storage is * removed or if the user clears the database in the browser's * history preferences. */ db.onclose = function () { state.closed = true; if (options.idb.onclose) options.idb.onclose(); }; /** * if service-workers are used, * we have no 'storage'-event if they post a message, * therefore we also have to set an interval */ _readLoop(state); return state; }); } function _readLoop(state) { if (state.closed) return; readNewMessages(state).then(function () { return sleep(state.options.idb.fallbackInterval); }).then(function () { return _readLoop(state); }); } function _filterMessage(msgObj, state) { if (msgObj.uuid === state.uuid) return false; // send by own if (state.eMIs.has(msgObj.id)) return false; // already emitted if (msgObj.data.time < state.messagesCallbackTime) return false; // older then onMessageCallback return true; } /** * reads all new messages from the database and emits them */ function readNewMessages(state) { // channel already closed if (state.closed) return Promise.resolve(); // if no one is listening, we do not need to scan for new messages if (!state.messagesCallback) return Promise.resolve(); return getMessagesHigherThan(state.db, state.lastCursorId).then(function (newerMessages) { var useMessages = newerMessages /** * there is a bug in iOS where the msgObj can be undefined some times * so we filter them out * @link https://github.com/pubkey/broadcast-channel/issues/19 */ .filter(function (msgObj) { return !!msgObj; }).map(function (msgObj) { if (msgObj.id > state.lastCursorId) { state.lastCursorId = msgObj.id; } return msgObj; }).filter(function (msgObj) { return _filterMessage(msgObj, state); }).sort(function (msgObjA, msgObjB) { return msgObjA.time - msgObjB.time; }); // sort by time useMessages.forEach(function (msgObj) { if (state.messagesCallback) { state.eMIs.add(msgObj.id); state.messagesCallback(msgObj.data); } }); return Promise.resolve(); }); } function close$2(channelState) { channelState.closed = true; channelState.db.close(); } function postMessage$2(channelState, messageJson) { channelState.writeBlockPromise = channelState.writeBlockPromise.then(function () { return writeMessage(channelState.db, channelState.uuid, messageJson); }).then(function () { if (randomInt(0, 10) === 0) { /* await (do not await) */ cleanOldMessages(channelState.db, channelState.options.idb.ttl); } }); return channelState.writeBlockPromise; } function onMessage$2(channelState, fn, time) { channelState.messagesCallbackTime = time; channelState.messagesCallback = fn; readNewMessages(channelState); } function canBeUsed$2() { if (isNode) return false; var idb = getIdb(); if (!idb) return false; return true; } function averageResponseTime$2(options) { return options.idb.fallbackInterval * 2; } var IndexeDbMethod = { create: create$2, close: close$2, onMessage: onMessage$2, postMessage: postMessage$2, canBeUsed: canBeUsed$2, type: type$2, averageResponseTime: averageResponseTime$2, microSeconds: microSeconds$2 }; /** * A localStorage-only method which uses localstorage and its 'storage'-event * This does not work inside of webworkers because they have no access to locastorage * This is basically implemented to support IE9 or your grandmothers toaster. * @link https://caniuse.com/#feat=namevalue-storage * @link https://caniuse.com/#feat=indexeddb */ var microSeconds$1 = microSeconds$4; var KEY_PREFIX = 'pubkey.broadcastChannel-'; var type$1 = 'localstorage'; /** * copied from crosstab * @link https://github.com/tejacques/crosstab/blob/master/src/crosstab.js#L32 */ function getLocalStorage() { var localStorage; if (typeof window === 'undefined') return null; try { localStorage = window.localStorage; localStorage = window['ie8-eventlistener/storage'] || window.localStorage; } catch (e) {// New versions of Firefox throw a Security exception // if cookies are disabled. See // https://bugzilla.mozilla.org/show_bug.cgi?id=1028153 } return localStorage; } function storageKey(channelName) { return KEY_PREFIX + channelName; } /** * writes the new message to the storage * and fires the storage-event so other readers can find it */ function postMessage$1(channelState, messageJson) { return new Promise(function (res) { sleep().then(function () { var key = storageKey(channelState.channelName); var writeObj = { token: randomToken(), time: new Date().getTime(), data: messageJson, uuid: channelState.uuid }; var value = JSON.stringify(writeObj); getLocalStorage().setItem(key, value); /** * StorageEvent does not fire the 'storage' event * in the window that changes the state of the local storage. * So we fire it manually */ var ev = document.createEvent('Event'); ev.initEvent('storage', true, true); ev.key = key; ev.newValue = value; window.dispatchEvent(ev); res(); }); }); } function addStorageEventListener(channelName, fn) { var key = storageKey(channelName); var listener = function listener(ev) { if (ev.key === key) { fn(JSON.parse(ev.newValue)); } }; window.addEventListener('storage', listener); return listener; } function removeStorageEventListener(listener) { window.removeEventListener('storage', listener); } function create$1(channelName, options) { options = fillOptionsWithDefaults(options); if (!canBeUsed$1()) { throw new Error('BroadcastChannel: localstorage cannot be used'); } var uuid = randomToken(); /** * eMIs * contains all messages that have been emitted before * @type {ObliviousSet} */ var eMIs = new ObliviousSet$1(options.localstorage.removeTimeout); var state = { channelName: channelName, uuid: uuid, eMIs: eMIs // emittedMessagesIds }; state.listener = addStorageEventListener(channelName, function (msgObj) { if (!state.messagesCallback) return; // no listener if (msgObj.uuid === uuid) return; // own message if (!msgObj.token || eMIs.has(msgObj.token)) return; // already emitted if (msgObj.data.time && msgObj.data.time < state.messagesCallbackTime) return; // too old eMIs.add(msgObj.token); state.messagesCallback(msgObj.data); }); return state; } function close$1(channelState) { removeStorageEventListener(channelState.listener); } function onMessage$1(channelState, fn, time) { channelState.messagesCallbackTime = time; channelState.messagesCallback = fn; } function canBeUsed$1() { if (isNode) return false; var ls = getLocalStorage(); if (!ls) return false; try { var key = '__broadcastchannel_check'; ls.setItem(key, 'works'); ls.removeItem(key); } catch (e) { // Safari 10 in private mode will not allow write access to local // storage and fail with a QuotaExceededError. See // https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API#Private_Browsing_Incognito_modes return false; } return true; } function averageResponseTime$1() { var defaultTime = 120; var userAgent = navigator.userAgent.toLowerCase(); if (userAgent.includes('safari') && !userAgent.includes('chrome')) { // safari is much slower so this time is higher return defaultTime * 2; } return defaultTime; } var LocalstorageMethod = { create: create$1, close: close$1, onMessage: onMessage$1, postMessage: postMessage$1, canBeUsed: canBeUsed$1, type: type$1, averageResponseTime: averageResponseTime$1, microSeconds: microSeconds$1 }; var microSeconds = microSeconds$4; var type = 'simulate'; var SIMULATE_CHANNELS = new Set(); function create(channelName) { var state = { name: channelName, messagesCallback: null }; SIMULATE_CHANNELS.add(state); return state; } function close(channelState) { SIMULATE_CHANNELS["delete"](channelState); } function postMessage(channelState, messageJson) { return new Promise(function (res) { return setTimeout(function () { var channelArray = Array.from(SIMULATE_CHANNELS); channelArray.filter(function (channel) { return channel.name === channelState.name; }).filter(function (channel) { return channel !== channelState; }).filter(function (channel) { return !!channel.messagesCallback; }).forEach(function (channel) { return channel.messagesCallback(messageJson); }); res(); }, 5); }); } function onMessage(channelState, fn) { channelState.messagesCallback = fn; } function canBeUsed() { return true; } function averageResponseTime() { return 5; } var SimulateMethod = { create: create, close: close, onMessage: onMessage, postMessage: postMessage, canBeUsed: canBeUsed, type: type, averageResponseTime: averageResponseTime, microSeconds: microSeconds }; var METHODS = [NativeMethod, // fastest IndexeDbMethod, LocalstorageMethod]; /** * The NodeMethod is loaded lazy * so it will not get bundled in browser-builds */ if (isNode) { /** * we use the non-transpiled code for nodejs * because it runs faster */ var NodeMethod = require('../../src/methods/' + // use this hack so that browserify and others // do not import the node-method by default // when bundling. 'node.js'); /** * this will be false for webpackbuilds * which will shim the node-method with an empty object {} */ if (typeof NodeMethod.canBeUsed === 'function') { METHODS.push(NodeMethod); } } function chooseMethod(options) { var chooseMethods = [].concat(options.methods, METHODS).filter(Boolean); // directly chosen if (options.type) { if (options.type === 'simulate') { // only use simulate-method if directly chosen return SimulateMethod; } var ret = chooseMethods.find(function (m) { return m.type === options.type; }); if (!ret) throw new Error('method-type ' + options.type + ' not found');else return ret; } /** * if no webworker support is needed, * remove idb from the list so that localstorage is been chosen */ if (!options.webWorkerSupport && !isNode) { chooseMethods = chooseMethods.filter(function (m) { return m.type !== 'idb'; }); } var useMethod = chooseMethods.find(function (method) { return method.canBeUsed(); }); if (!useMethod) throw new Error('No useable methode found:' + JSON.stringify(METHODS.map(function (m) { return m.type; })));else return useMethod; } var BroadcastChannel$1 = function BroadcastChannel(name, options) { this.name = name; if (ENFORCED_OPTIONS) { options = ENFORCED_OPTIONS; } this.options = fillOptionsWithDefaults(options); this.method = chooseMethod(this.options); // isListening this._iL = false; /** * _onMessageListener * setting onmessage twice, * will overwrite the first listener */ this._onML = null; /** * _addEventListeners */ this._addEL = { message: [], internal: [] }; /** * _beforeClose * array of promises that will be awaited * before the channel is closed */ this._befC = []; /** * _preparePromise */ this._prepP = null; _prepareChannel(this); }; // STATICS /** * used to identify if someone overwrites * window.BroadcastChannel with this * See methods/native.js */ BroadcastChannel$1._pubkey = true; /** * if set, this method is enforced, * no mather what the options are */ var ENFORCED_OPTIONS; BroadcastChannel$1.prototype = { postMessage: function postMessage(msg) { if (this.closed) { throw new Error('BroadcastChannel.postMessage(): ' + 'Cannot post message after channel has closed'); } return _post(this, 'message', msg); }, postInternal: function postInternal(msg) { return _post(this, 'internal', msg); }, set onmessage(fn) { var time = this.method.microSeconds(); var listenObj = { time: time, fn: fn }; _removeListenerObject(this, 'message', this._onML); if (fn && typeof fn === 'function') { this._onML = listenObj; _addListenerObject(this, 'message', listenObj); } else { this._onML = null; } }, addEventListener: function addEventListener(type, fn) { var time = this.method.microSeconds(); var listenObj = { time: time, fn: fn }; _addListenerObject(this, type, listenObj); }, removeEventListener: function removeEventListener(type, fn) { var obj = this._addEL[type].find(function (obj) { return obj.fn === fn; }); _removeListenerObject(this, type, obj); }, close: function close() { var _this = this; if (this.closed) return; this.closed = true; var awaitPrepare = this._prepP ? this._prepP : Promise.resolve(); this._onML = null; this._addEL.message = []; return awaitPrepare.then(function () { return Promise.all(_this._befC.map(function (fn) { return fn(); })); }).then(function () { return _this.method.close(_this._state); }); }, get type() { return this.method.type; } }; function _post(broadcastChannel, type, msg) { var time = broadcastChannel.method.microSeconds(); var msgObj = { time: time, type: type, data: msg }; var awaitPrepare = broadcastChannel._prepP ? broadcastChannel._prepP : Promise.resolve(); return awaitPrepare.then(function () { return broadcastChannel.method.postMessage(broadcastChannel._state, msgObj); }); } function _prepareChannel(channel) { var maybePromise = channel.method.create(channel.name, channel.options); if (isPromise(maybePromise)) { channel._prepP = maybePromise; maybePromise.then(function (s) { // used in tests to simulate slow runtime /*if (channel.options.prepareDelay) { await new Promise(res => setTimeout(res, this.options.prepareDelay)); }*/ channel._state = s; }); } else { channel._state = maybePromise; } } function _hasMessageListeners(channel) { if (channel._addEL.message.length > 0) return true; if (channel._addEL.internal.length > 0) return true; return false; } function _addListenerObject(channel, type, obj) { channel._addEL[type].push(obj); _startListening(channel); } function _removeListenerObject(channel, type, obj) { channel._addEL[type] = channel._addEL[type].filter(function (o) { return o !== obj; }); _stopListening(channel); } function _startListening(channel) { if (!channel._iL && _hasMessageListeners(channel)) { // someone is listening, start subscribing var listenerFn = function listenerFn(msgObj) { channel._addEL[msgObj.type].forEach(function (obj) { if (msgObj.time >= obj.time) { obj.fn(msgObj.data); } }); }; var time = channel.method.microSeconds(); if (channel._prepP) { channel._prepP.then(function () { channel._iL = true; channel.method.onMessage(channel._state, listenerFn, time); }); } else { channel._iL = true; channel.method.onMessage(channel._state, listenerFn, time); } } } function _stopListening(channel) { if (channel._iL && !_hasMessageListeners(channel)) { // noone is listening, stop subscribing channel._iL = false; var time = channel.method.microSeconds(); channel.method.onMessage(channel._state, null, time); } } function broadcastQueryClient({ queryClient, broadcastChannel = 'react-query' }) { let transaction = false; const tx = cb => { transaction = true; cb(); transaction = false; }; const channel = new BroadcastChannel$1(broadcastChannel, { webWorkerSupport: false }); const queryCache = queryClient.getQueryCache(); queryClient.getQueryCache().subscribe(queryEvent => { if (transaction) { return; } const { query: { queryHash, queryKey, state } } = queryEvent; if (queryEvent.type === 'updated' && queryEvent.action.type === 'success') { channel.postMessage({ type: 'updated', queryHash, queryKey, state }); } if (queryEvent.type === 'removed') { channel.postMessage({ type: 'removed', queryHash, queryKey }); } }); channel.onmessage = action => { if (!(action != null && action.type)) { return; } tx(() => { const { type, queryHash, queryKey, state } = action; if (type === 'updated') { const query = queryCache.get(queryHash); if (query) { query.setState(state); return; } queryCache.build(queryClient, { queryKey, queryHash }, state); } else if (type === 'removed') { const query = queryCache.get(queryHash); if (query) { queryCache.remove(query); } } }); }; } exports.broadcastQueryClient = broadcastQueryClient; Object.defineProperty(exports, '__esModule', { value: true }); })); //# sourceMappingURL=broadcastQueryClient-experimental.development.js.map