react-query
Version:
Hooks for managing, caching and syncing asynchronous and remote data in React
514 lines (412 loc) • 15 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
exports.__esModule = true;
exports.Query = void 0;
var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
var _utils = require("./utils");
var _notifyManager = require("./notifyManager");
var _logger = require("./logger");
var _retryer = require("./retryer");
// CLASS
var Query = /*#__PURE__*/function () {
function Query(config) {
this.abortSignalConsumed = false;
this.hadObservers = false;
this.defaultOptions = config.defaultOptions;
this.setOptions(config.options);
this.observers = [];
this.cache = config.cache;
this.queryKey = config.queryKey;
this.queryHash = config.queryHash;
this.initialState = config.state || this.getDefaultState(this.options);
this.state = this.initialState;
this.meta = config.meta;
this.scheduleGc();
}
var _proto = Query.prototype;
_proto.setOptions = function setOptions(options) {
var _this$options$cacheTi;
this.options = (0, _extends2.default)({}, this.defaultOptions, options);
this.meta = options == null ? void 0 : options.meta; // Default to 5 minutes if not cache time is set
this.cacheTime = Math.max(this.cacheTime || 0, (_this$options$cacheTi = this.options.cacheTime) != null ? _this$options$cacheTi : 5 * 60 * 1000);
};
_proto.setDefaultOptions = function setDefaultOptions(options) {
this.defaultOptions = options;
};
_proto.scheduleGc = function scheduleGc() {
var _this = this;
this.clearGcTimeout();
if ((0, _utils.isValidTimeout)(this.cacheTime)) {
this.gcTimeout = setTimeout(function () {
_this.optionalRemove();
}, this.cacheTime);
}
};
_proto.clearGcTimeout = function clearGcTimeout() {
if (this.gcTimeout) {
clearTimeout(this.gcTimeout);
this.gcTimeout = undefined;
}
};
_proto.optionalRemove = function optionalRemove() {
if (!this.observers.length) {
if (this.state.isFetching) {
if (this.hadObservers) {
this.scheduleGc();
}
} else {
this.cache.remove(this);
}
}
};
_proto.setData = function setData(updater, options) {
var _this$options$isDataE, _this$options;
var prevData = this.state.data; // Get the new data
var data = (0, _utils.functionalUpdate)(updater, prevData); // Use prev data if an isDataEqual function is defined and returns `true`
if ((_this$options$isDataE = (_this$options = this.options).isDataEqual) == null ? void 0 : _this$options$isDataE.call(_this$options, prevData, data)) {
data = prevData;
} else if (this.options.structuralSharing !== false) {
// Structurally share data between prev and new data if needed
data = (0, _utils.replaceEqualDeep)(prevData, data);
} // Set data and mark it as cached
this.dispatch({
data: data,
type: 'success',
dataUpdatedAt: options == null ? void 0 : options.updatedAt
});
return data;
};
_proto.setState = function setState(state, setStateOptions) {
this.dispatch({
type: 'setState',
state: state,
setStateOptions: setStateOptions
});
};
_proto.cancel = function cancel(options) {
var _this$retryer;
var promise = this.promise;
(_this$retryer = this.retryer) == null ? void 0 : _this$retryer.cancel(options);
return promise ? promise.then(_utils.noop).catch(_utils.noop) : Promise.resolve();
};
_proto.destroy = function destroy() {
this.clearGcTimeout();
this.cancel({
silent: true
});
};
_proto.reset = function reset() {
this.destroy();
this.setState(this.initialState);
};
_proto.isActive = function isActive() {
return this.observers.some(function (observer) {
return observer.options.enabled !== false;
});
};
_proto.isFetching = function isFetching() {
return this.state.isFetching;
};
_proto.isStale = function isStale() {
return this.state.isInvalidated || !this.state.dataUpdatedAt || this.observers.some(function (observer) {
return observer.getCurrentResult().isStale;
});
};
_proto.isStaleByTime = function isStaleByTime(staleTime) {
if (staleTime === void 0) {
staleTime = 0;
}
return this.state.isInvalidated || !this.state.dataUpdatedAt || !(0, _utils.timeUntilStale)(this.state.dataUpdatedAt, staleTime);
};
_proto.onFocus = function onFocus() {
var _this$retryer2;
var observer = this.observers.find(function (x) {
return x.shouldFetchOnWindowFocus();
});
if (observer) {
observer.refetch();
} // Continue fetch if currently paused
(_this$retryer2 = this.retryer) == null ? void 0 : _this$retryer2.continue();
};
_proto.onOnline = function onOnline() {
var _this$retryer3;
var observer = this.observers.find(function (x) {
return x.shouldFetchOnReconnect();
});
if (observer) {
observer.refetch();
} // Continue fetch if currently paused
(_this$retryer3 = this.retryer) == null ? void 0 : _this$retryer3.continue();
};
_proto.addObserver = function addObserver(observer) {
if (this.observers.indexOf(observer) === -1) {
this.observers.push(observer);
this.hadObservers = true; // Stop the query from being garbage collected
this.clearGcTimeout();
this.cache.notify({
type: 'observerAdded',
query: this,
observer: observer
});
}
};
_proto.removeObserver = function removeObserver(observer) {
if (this.observers.indexOf(observer) !== -1) {
this.observers = this.observers.filter(function (x) {
return x !== observer;
});
if (!this.observers.length) {
// If the transport layer does not support cancellation
// we'll let the query continue so the result can be cached
if (this.retryer) {
if (this.retryer.isTransportCancelable || this.abortSignalConsumed) {
this.retryer.cancel({
revert: true
});
} else {
this.retryer.cancelRetry();
}
}
if (this.cacheTime) {
this.scheduleGc();
} else {
this.cache.remove(this);
}
}
this.cache.notify({
type: 'observerRemoved',
query: this,
observer: observer
});
}
};
_proto.getObserversCount = function getObserversCount() {
return this.observers.length;
};
_proto.invalidate = function invalidate() {
if (!this.state.isInvalidated) {
this.dispatch({
type: 'invalidate'
});
}
};
_proto.fetch = function fetch(options, fetchOptions) {
var _this2 = this,
_this$options$behavio,
_context$fetchOptions,
_abortController$abor;
if (this.state.isFetching) {
if (this.state.dataUpdatedAt && (fetchOptions == null ? void 0 : fetchOptions.cancelRefetch)) {
// Silently cancel current fetch if the user wants to cancel refetches
this.cancel({
silent: true
});
} else if (this.promise) {
var _this$retryer4;
// make sure that retries that were potentially cancelled due to unmounts can continue
(_this$retryer4 = this.retryer) == null ? void 0 : _this$retryer4.continueRetry(); // Return current promise if we are already fetching
return this.promise;
}
} // Update config if passed, otherwise the config from the last execution is used
if (options) {
this.setOptions(options);
} // Use the options from the first observer with a query function if no function is found.
// This can happen when the query is hydrated or created with setQueryData.
if (!this.options.queryFn) {
var observer = this.observers.find(function (x) {
return x.options.queryFn;
});
if (observer) {
this.setOptions(observer.options);
}
}
var queryKey = (0, _utils.ensureQueryKeyArray)(this.queryKey);
var abortController = (0, _utils.getAbortController)(); // Create query function context
var queryFnContext = {
queryKey: queryKey,
pageParam: undefined,
meta: this.meta
};
Object.defineProperty(queryFnContext, 'signal', {
enumerable: true,
get: function get() {
if (abortController) {
_this2.abortSignalConsumed = true;
return abortController.signal;
}
return undefined;
}
}); // Create fetch function
var fetchFn = function fetchFn() {
if (!_this2.options.queryFn) {
return Promise.reject('Missing queryFn');
}
_this2.abortSignalConsumed = false;
return _this2.options.queryFn(queryFnContext);
}; // Trigger behavior hook
var context = {
fetchOptions: fetchOptions,
options: this.options,
queryKey: queryKey,
state: this.state,
fetchFn: fetchFn,
meta: this.meta
};
if ((_this$options$behavio = this.options.behavior) == null ? void 0 : _this$options$behavio.onFetch) {
var _this$options$behavio2;
(_this$options$behavio2 = this.options.behavior) == null ? void 0 : _this$options$behavio2.onFetch(context);
} // Store state in case the current fetch needs to be reverted
this.revertState = this.state; // Set to fetching state if not already in it
if (!this.state.isFetching || this.state.fetchMeta !== ((_context$fetchOptions = context.fetchOptions) == null ? void 0 : _context$fetchOptions.meta)) {
var _context$fetchOptions2;
this.dispatch({
type: 'fetch',
meta: (_context$fetchOptions2 = context.fetchOptions) == null ? void 0 : _context$fetchOptions2.meta
});
} // Try to fetch the data
this.retryer = new _retryer.Retryer({
fn: context.fetchFn,
abort: abortController == null ? void 0 : (_abortController$abor = abortController.abort) == null ? void 0 : _abortController$abor.bind(abortController),
onSuccess: function onSuccess(data) {
_this2.setData(data); // Notify cache callback
_this2.cache.config.onSuccess == null ? void 0 : _this2.cache.config.onSuccess(data, _this2); // Remove query after fetching if cache time is 0
if (_this2.cacheTime === 0) {
_this2.optionalRemove();
}
},
onError: function onError(error) {
// Optimistically update state if needed
if (!((0, _retryer.isCancelledError)(error) && error.silent)) {
_this2.dispatch({
type: 'error',
error: error
});
}
if (!(0, _retryer.isCancelledError)(error)) {
// Notify cache callback
_this2.cache.config.onError == null ? void 0 : _this2.cache.config.onError(error, _this2); // Log error
(0, _logger.getLogger)().error(error);
} // Remove query after fetching if cache time is 0
if (_this2.cacheTime === 0) {
_this2.optionalRemove();
}
},
onFail: function onFail() {
_this2.dispatch({
type: 'failed'
});
},
onPause: function onPause() {
_this2.dispatch({
type: 'pause'
});
},
onContinue: function onContinue() {
_this2.dispatch({
type: 'continue'
});
},
retry: context.options.retry,
retryDelay: context.options.retryDelay
});
this.promise = this.retryer.promise;
return this.promise;
};
_proto.dispatch = function dispatch(action) {
var _this3 = this;
this.state = this.reducer(this.state, action);
_notifyManager.notifyManager.batch(function () {
_this3.observers.forEach(function (observer) {
observer.onQueryUpdate(action);
});
_this3.cache.notify({
query: _this3,
type: 'queryUpdated',
action: action
});
});
};
_proto.getDefaultState = function getDefaultState(options) {
var data = typeof options.initialData === 'function' ? options.initialData() : options.initialData;
var hasInitialData = typeof options.initialData !== 'undefined';
var initialDataUpdatedAt = hasInitialData ? typeof options.initialDataUpdatedAt === 'function' ? options.initialDataUpdatedAt() : options.initialDataUpdatedAt : 0;
var hasData = typeof data !== 'undefined';
return {
data: data,
dataUpdateCount: 0,
dataUpdatedAt: hasData ? initialDataUpdatedAt != null ? initialDataUpdatedAt : Date.now() : 0,
error: null,
errorUpdateCount: 0,
errorUpdatedAt: 0,
fetchFailureCount: 0,
fetchMeta: null,
isFetching: false,
isInvalidated: false,
isPaused: false,
status: hasData ? 'success' : 'idle'
};
};
_proto.reducer = function reducer(state, action) {
var _action$meta, _action$dataUpdatedAt;
switch (action.type) {
case 'failed':
return (0, _extends2.default)({}, state, {
fetchFailureCount: state.fetchFailureCount + 1
});
case 'pause':
return (0, _extends2.default)({}, state, {
isPaused: true
});
case 'continue':
return (0, _extends2.default)({}, state, {
isPaused: false
});
case 'fetch':
return (0, _extends2.default)({}, state, {
fetchFailureCount: 0,
fetchMeta: (_action$meta = action.meta) != null ? _action$meta : null,
isFetching: true,
isPaused: false
}, !state.dataUpdatedAt && {
error: null,
status: 'loading'
});
case 'success':
return (0, _extends2.default)({}, state, {
data: action.data,
dataUpdateCount: state.dataUpdateCount + 1,
dataUpdatedAt: (_action$dataUpdatedAt = action.dataUpdatedAt) != null ? _action$dataUpdatedAt : Date.now(),
error: null,
fetchFailureCount: 0,
isFetching: false,
isInvalidated: false,
isPaused: false,
status: 'success'
});
case 'error':
var error = action.error;
if ((0, _retryer.isCancelledError)(error) && error.revert && this.revertState) {
return (0, _extends2.default)({}, this.revertState);
}
return (0, _extends2.default)({}, state, {
error: error,
errorUpdateCount: state.errorUpdateCount + 1,
errorUpdatedAt: Date.now(),
fetchFailureCount: state.fetchFailureCount + 1,
isFetching: false,
isPaused: false,
status: 'error'
});
case 'invalidate':
return (0, _extends2.default)({}, state, {
isInvalidated: true
});
case 'setState':
return (0, _extends2.default)({}, state, action.state);
default:
return state;
}
};
return Query;
}();
exports.Query = Query;