UNPKG

svelte-ux

Version:

- Increment version in `package.json` and commit as `Version bump to x.y.z` - `npm run publish`

233 lines (232 loc) 10.7 kB
import { getContext, setContext } from 'svelte'; import { get, writable } from 'svelte/store'; import { merge } from 'lodash-es'; const CONTEXT_KEY = {}; export function initFetchClient(config) { setContext(CONTEXT_KEY, config); } export const defaultOptions = { headers: { 'Content-Type': 'application/json', }, }; const DEFAULT_STATE = { loading: null, data: undefined, error: undefined, request: undefined, response: undefined, }; export default function fetchStore() { const globalConfig = getContext(CONTEXT_KEY); const localErrors = writable([]); const { subscribe, set, update } = writable({ ...DEFAULT_STATE }, () => { return () => { // Remove errors from global errors when no longer subscribed (component unmounted which uses store instance) removeGlobalErrors(globalConfig === null || globalConfig === void 0 ? void 0 : globalConfig.errors, localErrors); }; }); // Track first data load for `once` let loaded = false; const promises = []; const fetchConfigStore = writable({ url: '', }); function doFetch(url, config) { var _a, _b, _c, _d, _e, _f; const mergedConfig = merge({}, globalConfig, config); const prevFetchConfig = get(fetchConfigStore); // Save for refreshing or other building derived requests (ex. exports) fetchConfigStore.set({ url, config: mergedConfig }); if ((mergedConfig === null || mergedConfig === void 0 ? void 0 : mergedConfig.disabled) === true || ((mergedConfig === null || mergedConfig === void 0 ? void 0 : mergedConfig.once) && loaded && (mergedConfig === null || mergedConfig === void 0 ? void 0 : mergedConfig.force) !== true)) { // disabled or request already loaded and `once` set (and not forced) - do nothing } else if ((mergedConfig === null || mergedConfig === void 0 ? void 0 : mergedConfig.force) !== true && url === prevFetchConfig.url && ((_a = mergedConfig === null || mergedConfig === void 0 ? void 0 : mergedConfig.options) === null || _a === void 0 ? void 0 : _a.call(mergedConfig).body) === ((_c = (_b = prevFetchConfig === null || prevFetchConfig === void 0 ? void 0 : prevFetchConfig.config) === null || _b === void 0 ? void 0 : _b.options) === null || _c === void 0 ? void 0 : _c.call(_b).body) && (mergedConfig === null || mergedConfig === void 0 ? void 0 : mergedConfig.disabled) === ((_d = prevFetchConfig === null || prevFetchConfig === void 0 ? void 0 : prevFetchConfig.config) === null || _d === void 0 ? void 0 : _d.disabled)) { // skip identifical request as last unless force enabled } else { const options = merge({}, defaultOptions, (_e = globalConfig === null || globalConfig === void 0 ? void 0 : globalConfig.options) === null || _e === void 0 ? void 0 : _e.call(globalConfig), (_f = config === null || config === void 0 ? void 0 : config.options) === null || _f === void 0 ? void 0 : _f.call(config)); const request = { url, options }; // Remove local errors from global errors and clear all local errors when loading new request removeGlobalErrors(globalConfig === null || globalConfig === void 0 ? void 0 : globalConfig.errors, localErrors); localErrors.set([]); update((currentState) => doUpdate(currentState, { request, loading: true }, null, mergedConfig)); const as = (mergedConfig === null || mergedConfig === void 0 ? void 0 : mergedConfig.as) || 'auto'; const promise = fetch(url, options) .then(async (response) => { const dataPromise = typeof as === 'function' ? as(response) : typeof as === 'object' ? parseBody(response, as) : as === 'auto' ? parseBody(response) : response[as](); try { const data = await dataPromise; const newState = { request, loading: false, data: response.ok ? data : undefined, error: response.ok ? undefined : data, response, }; update((currentState) => doUpdate(currentState, newState, promise, mergedConfig)); loaded = true; } catch (error) { const newState = { request, loading: false, data: undefined, error: error, response, }; update((currentState) => doUpdate(currentState, newState, promise, mergedConfig)); } }) .catch((error) => { // Catch request errors with no response (CORS issues, etc) const newState = { request, data: undefined, error, loading: false, }; update((currentState) => doUpdate(currentState, newState, promise, mergedConfig)); // Rethrow so not to swallow errors, especially from errors within handlers (children func / onChange) throw error; }); promises.push(promise); // TODO: more thought... return { subscribe }; } } function doUpdate(currentState, nextState, currentPromise, config) { if (currentPromise) { // Handle (i.e. ignore) promises resolved out of order from requests const index = promises.indexOf(currentPromise); if (index === -1) { // Ignore update as a later request/promise has already been processed // console.log('skipping update'); return currentState; } // Remove currently resolved promise and any outstanding promises // (which will cause them to be ignored when they do resolve/reject) promises.splice(0, index + 1); } let data = undefined; if (nextState.data && nextState.data !== currentState.data && (config === null || config === void 0 ? void 0 : config.onDataChange)) { try { data = config.onDataChange(nextState.data, currentState.data); } catch (err) { console.error(err); } } let newState = { ...currentState, ...nextState, ...(data !== undefined && { data }), // If `onDataChange` returned a value, we use it for data passed down to the children function }; if (nextState.response && nextState.response !== currentState.response && (config === null || config === void 0 ? void 0 : config.onResponseChange)) { try { data = config.onResponseChange(nextState.response, newState); } catch (err) { console.error(err); } } newState = { ...newState, ...(data !== undefined && { data }), // If `onResponseChange` returned a value, we use it for data passed down to the children function }; // if (isFunction(onChange)) { // // Always call onChange even if unmounted. Useful for `POST` requests with a redirect // onChange({ // ...currentState, // ...nextState, // ...(data !== undefined && { data }), // }); // } if (newState.error && (config === null || config === void 0 ? void 0 : config.suppressErrors) !== true) { // Add errors to global `errors` store addError(globalConfig === null || globalConfig === void 0 ? void 0 : globalConfig.errors, newState.error); // Track errors specific to this store instance as well to support removal from global errors on unsubscribe (component unmount) addError(localErrors, newState.error); } // console.log({ newState }); return newState; } return { subscribe, fetch: doFetch, refresh() { const { url, config } = get(fetchConfigStore); doFetch(url, { ...config, force: true }); }, clear() { const { config } = get(fetchConfigStore); update((currentState) => doUpdate(currentState, { ...DEFAULT_STATE }, null, config)); }, fetchConfig: fetchConfigStore, }; } function addError(store, error) { if (store) { store.update((current) => { const result = [...current]; if (Array.isArray(error)) { error.forEach((e) => result.push(e)); } else { result.push(error); } return result; }); } } function removeGlobalErrors(globalErrors, localErrors) { if (globalErrors) { const $localErrors = get(localErrors); globalErrors.update(($errors) => $errors.filter((e) => !$localErrors.includes(e))); } } function parseBody(response, mapping = {}) { const contentType = response.headers.get('Content-Type'); // Do not attempt to parse empty response if (contentType === null) { return Promise.resolve(null); } const mimeType = contentType.split(';')[0].trim(); if (mimeType in mapping) { // Direct mapping of `Content-Type`/`mimeType` to response handler return mapping[mimeType](response); } else if (mimeType === 'application/json' || mimeType === 'text/json' || /\+json$/.test(mimeType) // ends with "+json" ) { // https://mimesniff.spec.whatwg.org/#json-mime-type return 'json' in mapping ? mapping['json'](response) : response.json(); } else if (mimeType === 'text/html') { // https://mimesniff.spec.whatwg.org/#html-mime-type return 'html' in mapping ? mapping['html'](response) : response.text(); } else if (mimeType === 'application/xml' || mimeType === 'text/xml' || /\+xml$/.test(mimeType) // ends with "+xml" ) { // https://mimesniff.spec.whatwg.org/#xml-mime-type return 'xml' in mapping ? mapping['xml'](response) : response.text(); } else { return 'other' in mapping ? mapping['other'](response) : response.arrayBuffer(); } }