{"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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"}