@pinia/colada-plugin-retry
Version:
Retry failed requests with Pinia Colada
1 lines • 12.4 kB
Source Map (JSON)
{"version":3,"file":"index.mjs","names":[],"sources":["../src/retry.ts"],"sourcesContent":["/**\n * Pinia Colada Retry plugin.\n *\n * Adds the ability to retry failed queries.\n *\n * @module @pinia/colada-plugin-retry\n */\nimport type { PiniaColadaPluginContext, UseQueryEntry } from '@pinia/colada'\nimport type { ShallowRef } from 'vue'\nimport { shallowRef, toValue } from 'vue'\n\n/**\n * Options for the Pinia Colada Retry plugin.\n */\nexport interface RetryOptions {\n /**\n * The delay between retries. Can be a duration in ms or a function that\n * receives the attempt number (starts at 0) and returns a duration in ms. By\n * default, it will wait 2^attempt * 1000 ms, but never more than 30 seconds.\n *\n * @param attempt -\n * @returns\n */\n delay?: number | ((attempt: number) => number)\n\n /**\n * The maximum number of times to retry the operation. Set to 0 to disable or\n * to Infinity to retry forever. It can also be a function that receives the\n * failure count and the error and returns if it should retry. Defaults to 3.\n * **Must be a positive number**.\n */\n retry?: number | ((failureCount: number, error: unknown) => boolean)\n}\n\nexport interface RetryEntry {\n retryCount: number\n timeoutId?: ReturnType<typeof setTimeout>\n}\n\nconst RETRY_OPTIONS_DEFAULTS = {\n delay: (attempt: number) => {\n const time = Math.min(\n 2 ** attempt * 1000,\n // never more than 30 seconds\n 30_000,\n )\n if (process.env.NODE_ENV === 'development') {\n // oxlint-disable-next-line no-console\n console.debug(`⏲️ delaying attempt #${attempt + 1} by ${time}ms`)\n }\n return time\n },\n retry: (count) => {\n if (process.env.NODE_ENV === 'development') {\n // oxlint-disable-next-line no-console\n console.debug(`🔄 Retrying ${'🟨'.repeat(count + 1)}${'⬜️'.repeat(2 - count)}`)\n }\n return count < 2\n },\n} satisfies Required<RetryOptions>\n\n/**\n * Plugin that adds the ability to retry failed queries.\n *\n * @param globalOptions - global options for the retries\n */\nexport function PiniaColadaRetry(\n globalOptions?: RetryOptions,\n): (context: PiniaColadaPluginContext) => void {\n const defaults = { ...RETRY_OPTIONS_DEFAULTS, ...globalOptions }\n\n return ({ queryCache, scope }) => {\n const retryMap = new Map<string, RetryEntry>()\n\n let isInternalCall = false\n queryCache.$onAction(({ name, args, after, onError }) => {\n if (name === 'extend') {\n const [entry] = args\n scope.run(() => {\n entry.ext.isRetrying = shallowRef(false)\n entry.ext.retryCount = shallowRef(0)\n entry.ext.retryError = shallowRef(null)\n })\n if (process.env.NODE_ENV === 'development') {\n updateDevtoolsState(entry)\n }\n return\n } else if (name === 'remove' || name === 'cancel') {\n // cleanup all pending retries\n const [cacheEntry] = args\n const key = cacheEntry.keyHash\n const entry = retryMap.get(key)\n if (entry) {\n clearTimeout(entry.timeoutId)\n retryMap.delete(key)\n }\n // also reset the state\n cacheEntry.ext.isRetrying.value = false\n cacheEntry.ext.retryCount.value = 0\n cacheEntry.ext.retryError.value = null\n\n if (process.env.NODE_ENV === 'development') {\n updateDevtoolsState(cacheEntry)\n }\n } else if (name === 'fetch') {\n const [queryEntry] = args\n const localOptions = queryEntry.options?.retry\n\n const options = {\n ...(typeof localOptions === 'object'\n ? localOptions\n : {\n retry: localOptions,\n }),\n } satisfies RetryOptions\n\n const retry = options.retry ?? defaults.retry\n const delay = options.delay ?? defaults.delay\n // avoid setting up anything at all\n if (retry === 0) return\n\n const key = queryEntry.keyHash\n\n // clear any pending retry\n clearTimeout(retryMap.get(key)?.timeoutId)\n // if the user manually calls the action, reset the retry count\n if (!isInternalCall) {\n retryMap.delete(key)\n queryEntry.ext.isRetrying.value = false\n queryEntry.ext.retryCount.value = 0\n queryEntry.ext.retryError.value = null\n if (process.env.NODE_ENV === 'development') {\n updateDevtoolsState(queryEntry)\n }\n }\n\n // capture state before the fetch runs so we can revert during retries\n const previousState = queryEntry.state.value\n\n const retryFetch = () => {\n if (queryEntry.state.value.status === 'error') {\n const error = queryEntry.state.value.error\n // ensure the entry exists\n let entry = retryMap.get(key)\n if (!entry) {\n entry = { retryCount: 0 }\n retryMap.set(key, entry)\n }\n\n const shouldRetry =\n typeof retry === 'number' ? retry > entry.retryCount : retry(entry.retryCount, error)\n\n if (shouldRetry) {\n queryEntry.ext.isRetrying.value = true\n queryEntry.ext.retryCount.value = entry.retryCount + 1\n queryEntry.ext.retryError.value = error\n if (process.env.NODE_ENV === 'development') {\n updateDevtoolsState(queryEntry)\n }\n // revert to pre-fetch state so the error is only visible via retryError\n queryEntry.state.value = previousState\n const delayTime = typeof delay === 'function' ? delay(entry.retryCount) : delay\n queryEntry.when = 0\n entry.timeoutId = setTimeout(() => {\n if (!queryEntry.active || toValue(queryEntry.options?.enabled) === false) {\n retryMap.delete(key)\n queryEntry.ext.isRetrying.value = false\n queryEntry.ext.retryCount.value = 0\n queryEntry.ext.retryError.value = null\n if (process.env.NODE_ENV === 'development') {\n updateDevtoolsState(queryEntry)\n }\n return\n }\n // NOTE: we could add some default error handler\n isInternalCall = true\n Promise.resolve(queryCache.fetch(queryEntry)).catch(\n process.env.NODE_ENV !== 'test' ? console.error : () => {},\n )\n isInternalCall = false\n if (entry) {\n entry.retryCount++\n }\n }, delayTime)\n } else {\n // remove the entry if we are not going to retry\n queryEntry.ext.isRetrying.value = false\n queryEntry.ext.retryError.value = null\n retryMap.delete(key)\n if (process.env.NODE_ENV === 'development') {\n updateDevtoolsState(queryEntry)\n }\n }\n } else {\n // remove the entry if it worked out to reset it\n queryEntry.ext.isRetrying.value = false\n queryEntry.ext.retryCount.value = 0\n queryEntry.ext.retryError.value = null\n retryMap.delete(key)\n if (process.env.NODE_ENV === 'development') {\n updateDevtoolsState(queryEntry)\n }\n }\n }\n onError(retryFetch)\n after(retryFetch)\n }\n })\n }\n}\n\n/**\n * Updates the devtools state for the retry plugin. Only used in development mode.\n *\n * @param entry - the query entry to update\n *\n * @internal\n */\nfunction updateDevtoolsState(entry: UseQueryEntry): void {\n if (entry.ext.retry) {\n entry.ext.retry.isRetrying = entry.ext.isRetrying.value\n entry.ext.retry.retryCount = entry.ext.retryCount.value\n entry.ext.retry.retryError = entry.ext.retryError.value\n } else {\n entry.ext.retry ??= {\n isRetrying: entry.ext.isRetrying.value,\n retryCount: entry.ext.retryCount.value,\n retryError: entry.ext.retryError.value,\n }\n }\n}\n\ndeclare module '@pinia/colada' {\n // eslint-disable-next-line unused-imports/no-unused-vars\n export interface UseQueryOptions<TData, TError, TDataInitial> {\n /**\n * Options for the retries of this query added by `@pinia/colada-plugin-retry`.\n */\n retry?: RetryOptions | Exclude<RetryOptions['retry'], undefined>\n }\n\n // eslint-disable-next-line unused-imports/no-unused-vars\n interface UseQueryEntryExtensions<TData, TError, TDataInitial> {\n /**\n * Whether the query is currently retrying. Requires the `@pinia/colada-plugin-retry` plugin.\n */\n isRetrying: ShallowRef<boolean>\n /**\n * The number of retries that have been scheduled so far. Resets on success or manual refetch.\n * Requires the `@pinia/colada-plugin-retry` plugin.\n */\n retryCount: ShallowRef<number>\n /**\n * The error that triggered the current retry. `null` when not retrying or when retries are exhausted.\n * Requires the `@pinia/colada-plugin-retry` plugin.\n */\n retryError: ShallowRef<TError | null>\n /**\n * Plain object with retry state for devtools. Only present in development mode.\n */\n retry?: { isRetrying: boolean; retryCount: number; retryError: unknown }\n }\n}\n"],"mappings":"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"}