diff --git a/apps/web/app/(app)/automation/BulkRunRules.tsx b/apps/web/app/(app)/automation/BulkRunRules.tsx index b2ae75d891..39b1aefc0d 100644 --- a/apps/web/app/(app)/automation/BulkRunRules.tsx +++ b/apps/web/app/(app)/automation/BulkRunRules.tsx @@ -37,7 +37,7 @@ export function BulkRunRules() { const [startDate, setStartDate] = useState(); const [endDate, setEndDate] = useState(); - const abortRef = useRef<() => void>(); + const abortRef = useRef<() => void>(undefined); return (
diff --git a/apps/web/app/(app)/bulk-unsubscribe/ArchiveProgress.tsx b/apps/web/app/(app)/bulk-unsubscribe/ArchiveProgress.tsx index c4914b047f..021124045d 100644 --- a/apps/web/app/(app)/bulk-unsubscribe/ArchiveProgress.tsx +++ b/apps/web/app/(app)/bulk-unsubscribe/ArchiveProgress.tsx @@ -1,25 +1,23 @@ -import { useEffect } from "react"; +import { memo, useEffect } from "react"; import { AnimatePresence, motion } from "framer-motion"; import { useAtomValue } from "jotai"; import { ProgressBar } from "@tremor/react"; -import { queueAtoms, resetTotalThreads } from "@/store/archive-queue"; +import { queueAtom, resetTotalThreads } from "@/store/archive-queue"; import { cn } from "@/utils"; -export const ArchiveProgress = () => { - const { totalThreads, activeThreadIds } = useAtomValue(queueAtoms.archive); +export const ArchiveProgress = memo(() => { + const { totalThreads, activeThreads } = useAtomValue(queueAtom); - // Make sure activeThreadIds is an object as this was causing an error. - const threadsRemaining = Object.values(activeThreadIds || {}).filter( - Boolean, - ).length; - const totalArchived = totalThreads - threadsRemaining; - const progress = (totalArchived / totalThreads) * 100; + // Make sure activeThreads is an object as this was causing an error. + const threadsRemaining = Object.values(activeThreads || {}).length; + const totalProcessed = totalThreads - threadsRemaining; + const progress = (totalProcessed / totalThreads) * 100; const isCompleted = progress === 100; useEffect(() => { if (isCompleted) { setTimeout(() => { - resetTotalThreads("archive"); + resetTotalThreads(); }, 5_000); } }, [isCompleted]); @@ -50,11 +48,11 @@ export const ArchiveProgress = () => { {isCompleted ? "Archiving complete!" : "Archiving emails..."} - {totalArchived} of {totalThreads} emails archived + {totalProcessed} of {totalThreads} emails archived

); -}; +}); diff --git a/apps/web/app/(app)/bulk-unsubscribe/BulkUnsubscribeSection.tsx b/apps/web/app/(app)/bulk-unsubscribe/BulkUnsubscribeSection.tsx index 5153c1f950..b067328d2c 100644 --- a/apps/web/app/(app)/bulk-unsubscribe/BulkUnsubscribeSection.tsx +++ b/apps/web/app/(app)/bulk-unsubscribe/BulkUnsubscribeSection.tsx @@ -40,6 +40,7 @@ import { SearchBar } from "@/app/(app)/bulk-unsubscribe/SearchBar"; import { useToggleSelect } from "@/hooks/useToggleSelect"; import { BulkActions } from "@/app/(app)/bulk-unsubscribe/BulkActions"; import { ArchiveProgress } from "@/app/(app)/bulk-unsubscribe/ArchiveProgress"; +import { ClientOnly } from "@/components/ClientOnly"; type Newsletter = NewsletterStatsResponse["newsletters"][number]; @@ -227,7 +228,9 @@ export function BulkUnsubscribeSection({ - + + + {isStatsLoading && !isLoading && !data?.newsletters.length ? (
diff --git a/apps/web/app/(app)/bulk-unsubscribe/hooks.ts b/apps/web/app/(app)/bulk-unsubscribe/hooks.ts index 3092e2a0c6..b169779a83 100644 --- a/apps/web/app/(app)/bulk-unsubscribe/hooks.ts +++ b/apps/web/app/(app)/bulk-unsubscribe/hooks.ts @@ -143,7 +143,7 @@ async function autoArchive( await mutate(); await decrementUnsubscribeCreditAction(); await refetchPremium(); - await archiveAllSenderEmails(name, () => {}); + await archiveAllSenderEmails(name, () => {}, labelId); } export function useAutoArchive({ @@ -182,6 +182,8 @@ export function useAutoArchive({ status: null, }); await mutate(); + + setAutoArchiveLoading(false); }, [item.name, mutate, posthog, refetchPremium]); const onAutoArchiveAndLabel = useCallback( diff --git a/apps/web/package.json b/apps/web/package.json index 3743aa2280..eaeee387a2 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -95,6 +95,7 @@ "novel": "0.3.1", "openai": "^4.67.3", "p-queue": "^8.0.1", + "p-retry": "^6.2.0", "posthog-js": "^1.167.0", "posthog-node": "^4.2.0", "prettier": "^3.3.3", diff --git a/apps/web/store/QueueInitializer.tsx b/apps/web/store/QueueInitializer.tsx index 1e4e3712fc..7c40bd0ac9 100644 --- a/apps/web/store/QueueInitializer.tsx +++ b/apps/web/store/QueueInitializer.tsx @@ -1,28 +1,22 @@ "use client"; -import { useAtom } from "jotai"; +import { useAtomValue } from "jotai"; import { useEffect } from "react"; -import { processQueue, queueAtoms } from "@/store/archive-queue"; +import { processQueue, queueAtom } from "@/store/archive-queue"; -function useInitializeQueues() { - const [archiveQueue] = useAtom(queueAtoms.archive); - const [deleteQueue] = useAtom(queueAtoms.delete); - const [markReadQueue] = useAtom(queueAtoms.markRead); - - useEffect(() => { - const threadIds = Object.keys(archiveQueue.activeThreadIds || {}); - if (threadIds.length) processQueue("archive", threadIds); - }, [archiveQueue]); +let isInitialized = false; - useEffect(() => { - const threadIds = Object.keys(deleteQueue.activeThreadIds || {}); - if (threadIds.length) processQueue("delete", threadIds); - }, [deleteQueue]); +function useInitializeQueues() { + const queueState = useAtomValue(queueAtom); useEffect(() => { - const threadIds = Object.keys(markReadQueue.activeThreadIds || {}); - if (threadIds.length) processQueue("markRead", threadIds); - }, [markReadQueue]); + if (!isInitialized) { + isInitialized = true; + if (queueState.activeThreads) { + processQueue({ threads: queueState.activeThreads }); + } + } + }, [queueState.activeThreads]); } export function QueueInitializer() { diff --git a/apps/web/store/archive-queue.ts b/apps/web/store/archive-queue.ts index f8bb2b53b7..b97829420c 100644 --- a/apps/web/store/archive-queue.ts +++ b/apps/web/store/archive-queue.ts @@ -1,4 +1,5 @@ import { atomWithStorage, createJSONStorage } from "jotai/utils"; +import pRetry from "p-retry"; import { jotaiStore } from "@/store"; import { emailActionQueue } from "@/utils/queue/email-action-queue"; import { @@ -6,98 +7,140 @@ import { trashThreadAction, markReadThreadAction, } from "@/utils/actions/mail"; +import { isActionError, ServerActionResponse } from "@/utils/error"; +import { exponentialBackoff, sleep } from "@/utils/sleep"; -type QueueType = "archive" | "delete" | "markRead"; +type ActionType = "archive" | "delete" | "markRead"; + +type QueueItem = { + threadId: string; + actionType: ActionType; + labelId?: string; +}; type QueueState = { - activeThreadIds: Record; + activeThreads: Record<`${ActionType}-${string}`, QueueItem>; totalThreads: number; }; -function getInitialState(): QueueState { - return { activeThreadIds: {}, totalThreads: 0 }; -} - -// some users were somehow getting null for activeThreadIds, this should fix it +// some users were somehow getting null for activeThreads, this should fix it const createStorage = () => { + if (typeof window === "undefined") return; const storage = createJSONStorage(() => localStorage); return { ...storage, getItem: (key: string, initialValue: QueueState) => { const storedValue = storage.getItem(key, initialValue); return { - activeThreadIds: storedValue.activeThreadIds || {}, + activeThreads: storedValue.activeThreads || {}, totalThreads: storedValue.totalThreads || 0, }; }, }; }; -// Create atoms with localStorage persistence for each queue type -export const queueAtoms = { - archive: atomWithStorage("archiveQueue", getInitialState(), createStorage()), - delete: atomWithStorage("deleteQueue", getInitialState(), createStorage()), - markRead: atomWithStorage( - "markReadQueue", - getInitialState(), - createStorage(), - ), -}; +// Create atoms with localStorage persistence +export const queueAtom = atomWithStorage( + "gmailActionQueue", + { activeThreads: {}, totalThreads: 0 }, + createStorage(), + { getOnInit: true }, +); -type ActionFunction = (threadId: string, ...args: any[]) => Promise; +type ActionFunction = ( + threadId: string, + labelId?: string, +) => Promise>; -const actionMap: Record = { - archive: archiveThreadAction, +const actionMap: Record = { + archive: (threadId: string, labelId?: string) => + archiveThreadAction(threadId, labelId), delete: trashThreadAction, markRead: (threadId: string) => markReadThreadAction(threadId, true), }; -export const addThreadsToQueue = ( - queueType: QueueType, - threadIds: string[], - refetch?: () => void, -) => { - const queueAtom = queueAtoms[queueType]; +export const addThreadsToQueue = ({ + actionType, + threadIds, + labelId, + refetch, +}: { + actionType: ActionType; + threadIds: string[]; + labelId?: string; + refetch?: () => void; +}) => { + const threads = Object.fromEntries( + threadIds.map((threadId) => [ + `${actionType}-${threadId}`, + { threadId, actionType, labelId }, + ]), + ); jotaiStore.set(queueAtom, (prev) => ({ - activeThreadIds: { - ...prev.activeThreadIds, - ...Object.fromEntries(threadIds.map((id) => [id, true])), + activeThreads: { + ...prev.activeThreads, + ...threads, }, - totalThreads: prev.totalThreads + threadIds.length, + totalThreads: prev.totalThreads + Object.keys(threads).length, })); - processQueue(queueType, threadIds, refetch); + processQueue({ threads, refetch }); }; -export function processQueue( - queueType: QueueType, - threadIds: string[], - refetch?: () => void, -) { - const queueAtom = queueAtoms[queueType]; - const action = actionMap[queueType]; - +export function processQueue({ + threads, + refetch, +}: { + threads: Record; + refetch?: () => void; +}) { emailActionQueue.addAll( - threadIds.map((threadId) => async () => { - await action(threadId); - - // remove completed thread from activeThreadIds - jotaiStore.set(queueAtom, (prev) => { - const { [threadId]: _, ...remainingThreads } = prev.activeThreadIds; - return { - ...prev, - activeThreadIds: remainingThreads, - }; - }); - - refetch?.(); - }), + Object.entries(threads).map( + ([_key, { threadId, actionType, labelId }]) => + async () => { + await pRetry( + async (attemptCount) => { + console.log( + `Queue: ${actionType}. Processing ${threadId}` + + (attemptCount > 1 ? ` (attempt ${attemptCount})` : ""), + ); + + const result = await actionMap[actionType](threadId, labelId); + + // when Gmail API returns a rate limit error, throw an error so it can be retried + if (isActionError(result)) { + await sleep(exponentialBackoff(attemptCount, 1_000)); + throw new Error(result.error); + } + refetch?.(); + }, + { retries: 3 }, + ); + + // remove completed thread from activeThreads + jotaiStore.set(queueAtom, (prev) => { + const remainingThreads = Object.fromEntries( + Object.entries(prev.activeThreads).filter( + ([_key, value]) => + !( + value.threadId === threadId && + value.actionType === actionType + ), + ), + ); + + return { + ...prev, + activeThreads: remainingThreads, + }; + }); + }, + ), ); } -export const resetTotalThreads = (queueType: QueueType) => { - const queueAtom = queueAtoms[queueType]; +export const resetTotalThreads = () => { jotaiStore.set(queueAtom, (prev) => ({ ...prev, totalThreads: 0, diff --git a/apps/web/utils/actions/mail.ts b/apps/web/utils/actions/mail.ts index 7f75684224..f59baab6e1 100644 --- a/apps/web/utils/actions/mail.ts +++ b/apps/web/utils/actions/mail.ts @@ -25,7 +25,7 @@ const isStatusOk = (status: number) => status >= 200 && status < 300; export const archiveThreadAction = withActionInstrumentation( "archiveThread", - async (threadId: string) => { + async (threadId: string, labelId?: string) => { const { gmail, user, error } = await getSessionAndGmailClient(); if (error) return { error }; if (!gmail) return { error: "Could not load Gmail" }; @@ -35,6 +35,7 @@ export const archiveThreadAction = withActionInstrumentation( threadId, ownerEmail: user.email, actionSource: "user", + labelId, }); if (!isStatusOk(res.status)) return { error: "Failed to archive thread" }; diff --git a/apps/web/utils/gmail/label.ts b/apps/web/utils/gmail/label.ts index 1b362bed9b..c5862189ef 100644 --- a/apps/web/utils/gmail/label.ts +++ b/apps/web/utils/gmail/label.ts @@ -28,19 +28,25 @@ export async function labelThread(options: { }); } -export async function archiveThread(options: { +export async function archiveThread({ + gmail, + threadId, + ownerEmail, + actionSource, + labelId, +}: { gmail: gmail_v1.Gmail; threadId: string; ownerEmail: string; actionSource: TinybirdEmailAction["actionSource"]; + labelId?: string; }) { - const { gmail, threadId, ownerEmail, actionSource } = options; - const archivePromise = gmail.users.threads.modify({ userId: "me", id: threadId, requestBody: { removeLabelIds: [INBOX_LABEL_ID], + ...(labelId ? { addLabelIds: [labelId] } : {}), }, }); diff --git a/apps/web/utils/queue/email-action-queue.ts b/apps/web/utils/queue/email-action-queue.ts index 5b94711071..efcaf5a1e1 100644 --- a/apps/web/utils/queue/email-action-queue.ts +++ b/apps/web/utils/queue/email-action-queue.ts @@ -3,4 +3,4 @@ import PQueue from "p-queue"; // Avoid overwhelming Gmail API -export const emailActionQueue = new PQueue({ concurrency: 3 }); +export const emailActionQueue = new PQueue({ concurrency: 1 }); diff --git a/apps/web/utils/queue/email-actions.ts b/apps/web/utils/queue/email-actions.ts index a3aefd6806..c5332f629c 100644 --- a/apps/web/utils/queue/email-actions.ts +++ b/apps/web/utils/queue/email-actions.ts @@ -12,27 +12,29 @@ import { emailActionQueue } from "@/utils/queue/email-action-queue"; export const archiveEmails = async ( threadIds: string[], refetch?: () => void, + labelId?: string, ) => { - addThreadsToQueue("archive", threadIds, refetch); + addThreadsToQueue({ actionType: "archive", threadIds, labelId, refetch }); }; export const markReadThreads = async ( threadIds: string[], refetch: () => void, ) => { - addThreadsToQueue("markRead", threadIds, refetch); + addThreadsToQueue({ actionType: "markRead", threadIds, refetch }); }; export const deleteEmails = async ( threadIds: string[], refetch: () => void, ) => { - addThreadsToQueue("delete", threadIds, refetch); + addThreadsToQueue({ actionType: "delete", threadIds, refetch }); }; export const archiveAllSenderEmails = async ( from: string, onComplete: () => void, + labelId?: string, ) => { try { // 1. search gmail for messages from sender @@ -45,7 +47,11 @@ export const archiveAllSenderEmails = async ( // 2. archive messages if (data?.length) { - archiveEmails(data.map((t) => t.id).filter(isDefined), onComplete); + archiveEmails( + data.map((t) => t.id).filter(isDefined), + onComplete, + labelId, + ); } else { onComplete(); } diff --git a/apps/web/utils/sleep.ts b/apps/web/utils/sleep.ts index 69dc25d672..6cdeee06ee 100644 --- a/apps/web/utils/sleep.ts +++ b/apps/web/utils/sleep.ts @@ -1,3 +1,6 @@ export async function sleep(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); } + +export const exponentialBackoff = (retryCount: number, ms: number) => + Math.pow(2, retryCount) * ms; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f7a73ed876..06749a599b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -338,6 +338,9 @@ importers: p-queue: specifier: ^8.0.1 version: 8.0.1 + p-retry: + specifier: ^6.2.0 + version: 6.2.0 posthog-js: specifier: ^1.167.0 version: 1.167.0 @@ -5110,6 +5113,9 @@ packages: '@types/react@18.3.11': resolution: {integrity: sha512-r6QZ069rFTjrEYgFdOck1gK7FLVsgJE7tTz0pQBczlBNUhBNk0MQH4UbnFSwjpQLMkLzgqvBBa+qGpLje16eTQ==} + '@types/retry@0.12.2': + resolution: {integrity: sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==} + '@types/semver@7.5.8': resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} @@ -7831,6 +7837,10 @@ packages: resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} engines: {node: '>= 0.4'} + is-network-error@1.1.0: + resolution: {integrity: sha512-tUdRRAnhT+OtCZR/LxZelH/C7QtjtFrTu5tXCA8pl55eTUElUHT+GPYV8MBMBvea/j+NxQqVt3LbWMRir7Gx9g==} + engines: {node: '>=16'} + is-number-object@1.0.7: resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} engines: {node: '>= 0.4'} @@ -9063,6 +9073,10 @@ packages: resolution: {integrity: sha512-NXzu9aQJTAzbBqOt2hwsR63ea7yvxJc0PwN/zobNAudYfb1B7R08SzB4TsLeSbUCuG467NhnoT0oO6w1qRO+BA==} engines: {node: '>=18'} + p-retry@6.2.0: + resolution: {integrity: sha512-JA6nkq6hKyWLLasXQXUrO4z8BUZGUt/LjlJxx8Gb2+2ntodU/SS63YZ8b0LUTbQ8ZB9iwOfhEPhg4ykKnn2KsA==} + engines: {node: '>=16.17'} + p-timeout@6.1.2: resolution: {integrity: sha512-UbD77BuZ9Bc9aABo74gfXhNvzC9Tx7SxtHSh1fxvx3jTLLYvmVhiQZZrJzqqU0jKbN32kb5VOKiLEQI/3bIjgQ==} engines: {node: '>=14.16'} @@ -10000,6 +10014,10 @@ packages: resolution: {integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==} engines: {node: '>=10'} + retry@0.13.1: + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} + engines: {node: '>= 4'} + reusify@1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -16804,6 +16822,8 @@ snapshots: '@types/prop-types': 15.7.5 csstype: 3.1.3 + '@types/retry@0.12.2': {} + '@types/semver@7.5.8': {} '@types/shallow-equals@1.0.3': {} @@ -18820,8 +18840,8 @@ snapshots: '@typescript-eslint/parser': 8.8.1(eslint@8.57.1)(typescript@5.6.3) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1))(eslint@8.57.1) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.1) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.1) eslint-plugin-jsx-a11y: 6.8.0(eslint@8.57.1) eslint-plugin-react: 7.35.0(eslint@8.57.1) eslint-plugin-react-hooks: 4.6.0(eslint@8.57.1) @@ -18869,13 +18889,13 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1))(eslint@8.57.1): + eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.1): dependencies: debug: 4.3.6 enhanced-resolve: 5.15.0 eslint: 8.57.1 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-module-utils: 2.8.0(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.1))(eslint@8.57.1) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.1) fast-glob: 3.3.2 get-tsconfig: 4.7.5 is-core-module: 2.13.1 @@ -18897,14 +18917,14 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.8.0(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): + eslint-module-utils@2.8.0(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.1))(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.8.1(eslint@8.57.1)(typescript@5.6.3) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1))(eslint@8.57.1) + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.1) transitivePeerDependencies: - supports-color @@ -18924,7 +18944,7 @@ snapshots: eslint: 9.10.0(jiti@1.21.6) ignore: 5.3.1 - eslint-plugin-import@2.29.1(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): + eslint-plugin-import@2.29.1(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.1): dependencies: array-includes: 3.1.8 array.prototype.findlastindex: 1.2.3 @@ -18934,7 +18954,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-module-utils: 2.8.0(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.1))(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.13.1 is-glob: 4.0.3 @@ -20305,6 +20325,8 @@ snapshots: is-negative-zero@2.0.3: {} + is-network-error@1.1.0: {} + is-number-object@1.0.7: dependencies: has-tostringtag: 1.0.2 @@ -21870,6 +21892,12 @@ snapshots: eventemitter3: 5.0.1 p-timeout: 6.1.2 + p-retry@6.2.0: + dependencies: + '@types/retry': 0.12.2 + is-network-error: 1.1.0 + retry: 0.13.1 + p-timeout@6.1.2: {} p-try@2.2.0: {} @@ -22924,6 +22952,8 @@ snapshots: ret@0.5.0: {} + retry@0.13.1: {} + reusify@1.0.4: {} rfdc@1.4.1: {}