.refine check status
This commit is contained in:
@@ -23,10 +23,34 @@ type App struct {
|
|||||||
ctx context.Context
|
ctx context.Context
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const checkOperationTimeout = 10 * time.Second
|
||||||
|
|
||||||
func NewApp() *App {
|
func NewApp() *App {
|
||||||
return &App{}
|
return &App{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type timedResult[T any] struct {
|
||||||
|
value T
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func runWithTimeout[T any](timeout time.Duration, fn func() (T, error)) (T, error) {
|
||||||
|
resultCh := make(chan timedResult[T], 1)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
value, err := fn()
|
||||||
|
resultCh <- timedResult[T]{value: value, err: err}
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case result := <-resultCh:
|
||||||
|
return result.value, result.err
|
||||||
|
case <-time.After(timeout):
|
||||||
|
var zero T
|
||||||
|
return zero, fmt.Errorf("operation timed out after %s", timeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (a *App) getFirstArtist(artistString string) string {
|
func (a *App) getFirstArtist(artistString string) string {
|
||||||
if artistString == "" {
|
if artistString == "" {
|
||||||
return ""
|
return ""
|
||||||
@@ -757,49 +781,57 @@ func (a *App) ExportFailedDownloads() (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) CheckAPIStatus(apiType string, apiURL string) bool {
|
func (a *App) CheckAPIStatus(apiType string, apiURL string) bool {
|
||||||
var checkURL string
|
isOnline, err := runWithTimeout(checkOperationTimeout, func() (bool, error) {
|
||||||
if apiType == "tidal" {
|
var checkURL string
|
||||||
checkURL = fmt.Sprintf("%s/track/?id=441821360&quality=HI_RES_LOSSLESS", apiURL)
|
if apiType == "tidal" {
|
||||||
} else if apiType == "qobuz" {
|
checkURL = fmt.Sprintf("%s/track/?id=441821360&quality=HI_RES_LOSSLESS", apiURL)
|
||||||
checkURL = fmt.Sprintf("%s/api/stream?trackId=360735657&format_id=27", apiURL)
|
} else if apiType == "qobuz" {
|
||||||
} else if apiType == "qbz" {
|
checkURL = fmt.Sprintf("%s/api/stream?trackId=360735657&format_id=27", apiURL)
|
||||||
checkURL = fmt.Sprintf("%s/api/track/360735657?quality=27", apiURL)
|
} else if apiType == "qbz" {
|
||||||
} else if apiType == "amazon" {
|
checkURL = fmt.Sprintf("%s/api/track/360735657?quality=27", apiURL)
|
||||||
checkURL = fmt.Sprintf("%s/status", apiURL)
|
} else if apiType == "amazon" {
|
||||||
} else {
|
checkURL = fmt.Sprintf("%s/status", apiURL)
|
||||||
checkURL = apiURL
|
} else {
|
||||||
}
|
checkURL = apiURL
|
||||||
|
}
|
||||||
|
|
||||||
client := &http.Client{Timeout: 15 * time.Second}
|
client := &http.Client{Timeout: 10 * time.Second}
|
||||||
req, err := http.NewRequest("GET", checkURL, nil)
|
req, err := http.NewRequest("GET", checkURL, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false, err
|
||||||
}
|
}
|
||||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36")
|
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36")
|
||||||
|
|
||||||
maxRetries := 3
|
maxRetries := 3
|
||||||
for i := 0; i < maxRetries; i++ {
|
for i := 0; i < maxRetries; i++ {
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
statusCode := resp.StatusCode
|
statusCode := resp.StatusCode
|
||||||
if apiType == "amazon" && statusCode == 200 {
|
if apiType == "amazon" && statusCode == 200 {
|
||||||
body, readErr := io.ReadAll(resp.Body)
|
body, readErr := io.ReadAll(resp.Body)
|
||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
if readErr == nil && strings.Contains(string(body), `"amazonMusic":"up"`) {
|
if readErr == nil && strings.Contains(string(body), `"amazonMusic":"up"`) {
|
||||||
return true
|
return true, nil
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
if statusCode == 200 {
|
if statusCode == 200 {
|
||||||
return true
|
return true, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if i < maxRetries-1 {
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if i < maxRetries-1 {
|
return false, nil
|
||||||
time.Sleep(1 * time.Second)
|
})
|
||||||
}
|
if err != nil {
|
||||||
|
fmt.Printf("CheckAPIStatus timeout/error for %s (%s): %v\n", apiType, apiURL, err)
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
return false
|
|
||||||
|
return isOnline
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) Quit() {
|
func (a *App) Quit() {
|
||||||
@@ -1085,18 +1117,20 @@ func (a *App) CheckTrackAvailability(spotifyTrackID string) (string, error) {
|
|||||||
return "", fmt.Errorf("spotify track ID is required")
|
return "", fmt.Errorf("spotify track ID is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
client := backend.NewSongLinkClient()
|
return runWithTimeout(checkOperationTimeout, func() (string, error) {
|
||||||
availability, err := client.CheckTrackAvailability(spotifyTrackID)
|
client := backend.NewSongLinkClient()
|
||||||
if err != nil {
|
availability, err := client.CheckTrackAvailability(spotifyTrackID)
|
||||||
return "", err
|
if err != nil {
|
||||||
}
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
jsonData, err := json.Marshal(availability)
|
jsonData, err := json.Marshal(availability)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to encode response: %v", err)
|
return "", fmt.Errorf("failed to encode response: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return string(jsonData), nil
|
return string(jsonData), nil
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) IsFFmpegInstalled() (bool, error) {
|
func (a *App) IsFFmpegInstalled() (bool, error) {
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import { useMetadata } from "@/hooks/useMetadata";
|
|||||||
import { useLyrics } from "@/hooks/useLyrics";
|
import { useLyrics } from "@/hooks/useLyrics";
|
||||||
import { useCover } from "@/hooks/useCover";
|
import { useCover } from "@/hooks/useCover";
|
||||||
import { useAvailability } from "@/hooks/useAvailability";
|
import { useAvailability } from "@/hooks/useAvailability";
|
||||||
|
import { ensureApiStatusCheckStarted } from "@/lib/api-status";
|
||||||
import { useDownloadQueueDialog } from "@/hooks/useDownloadQueueDialog";
|
import { useDownloadQueueDialog } from "@/hooks/useDownloadQueueDialog";
|
||||||
import { useDownloadProgress } from "@/hooks/useDownloadProgress";
|
import { useDownloadProgress } from "@/hooks/useDownloadProgress";
|
||||||
const HISTORY_KEY = "spotiflac_fetch_history";
|
const HISTORY_KEY = "spotiflac_fetch_history";
|
||||||
@@ -179,6 +180,7 @@ function App() {
|
|||||||
};
|
};
|
||||||
mediaQuery.addEventListener("change", handleChange);
|
mediaQuery.addEventListener("change", handleChange);
|
||||||
checkForUpdates();
|
checkForUpdates();
|
||||||
|
ensureApiStatusCheckStarted();
|
||||||
loadHistory();
|
loadHistory();
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
setShowScrollTop(window.scrollY > 300);
|
setShowScrollTop(window.scrollY > 300);
|
||||||
|
|||||||
@@ -273,8 +273,7 @@ export function AboutPage() {
|
|||||||
Note
|
Note
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs leading-relaxed text-sky-700 dark:text-sky-300">
|
<p className="text-xs leading-relaxed text-sky-700 dark:text-sky-300">
|
||||||
SpotiFLAC Next is a separate project created as a thank-you
|
This project is a thank-you to everyone who supported SpotiFLAC on Ko-fi.
|
||||||
to everyone who has supported SpotiFLAC on Ko-fi.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>)}
|
</CardContent>)}
|
||||||
|
|||||||
@@ -1,59 +1,20 @@
|
|||||||
import { useState, useEffect } from "react";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { RefreshCw, CheckCircle2, XCircle, Loader2 } from "lucide-react";
|
import { RefreshCw, CheckCircle2, XCircle, Loader2 } from "lucide-react";
|
||||||
import { CheckAPIStatus } from "../../wailsjs/go/main/App";
|
|
||||||
import { TidalIcon, QobuzIcon, AmazonIcon } from "./PlatformIcons";
|
import { TidalIcon, QobuzIcon, AmazonIcon } from "./PlatformIcons";
|
||||||
interface ApiSource {
|
import { useApiStatus } from "@/hooks/useApiStatus";
|
||||||
id: string;
|
|
||||||
type: string;
|
|
||||||
name: string;
|
|
||||||
url: string;
|
|
||||||
}
|
|
||||||
const SOURCES: ApiSource[] = [
|
|
||||||
{ id: "tidal1", type: "tidal", name: "Tidal A", url: "https://hifi-one.spotisaver.net" },
|
|
||||||
{ id: "tidal2", type: "tidal", name: "Tidal B", url: "https://hifi-two.spotisaver.net" },
|
|
||||||
{ id: "tidal3", type: "tidal", name: "Tidal C", url: "https://eu-central.monochrome.tf" },
|
|
||||||
{ id: "tidal4", type: "tidal", name: "Tidal D", url: "https://us-west.monochrome.tf" },
|
|
||||||
{ id: "tidal5", type: "tidal", name: "Tidal E", url: "https://api.monochrome.tf" },
|
|
||||||
{ id: "tidal6", type: "tidal", name: "Tidal F", url: "https://monochrome-api.samidy.com" },
|
|
||||||
{ id: "tidal7", type: "tidal", name: "Tidal G", url: "https://tidal.kinoplus.online" },
|
|
||||||
{ id: "qobuz1", type: "qobuz", name: "Qobuz A", url: "https://dab.yeet.su" },
|
|
||||||
{ id: "qobuz2", type: "qobuz", name: "Qobuz B", url: "https://dabmusic.xyz" },
|
|
||||||
{ id: "qobuz3", type: "qbz", name: "Qobuz C", url: "https://qbz.afkarxyz.qzz.io" },
|
|
||||||
{ id: "amazon1", type: "amazon", name: "Amazon Music", url: "https://amzn.afkarxyz.qzz.io" },
|
|
||||||
];
|
|
||||||
export function ApiStatusTab() {
|
export function ApiStatusTab() {
|
||||||
const [statuses, setStatuses] = useState<Record<string, "checking" | "online" | "offline" | "idle">>({});
|
const { sources, statuses, isCheckingAll, refreshAll } = useApiStatus();
|
||||||
const [isCheckingAll, setIsCheckingAll] = useState(false);
|
|
||||||
const checkStatus = async (sourceId: string, apiType: string, url: string) => {
|
|
||||||
setStatuses(prev => ({ ...prev, [sourceId]: "checking" }));
|
|
||||||
try {
|
|
||||||
const isOnline = await CheckAPIStatus(apiType, url);
|
|
||||||
setStatuses(prev => ({ ...prev, [sourceId]: isOnline ? "online" : "offline" }));
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
setStatuses(prev => ({ ...prev, [sourceId]: "offline" }));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const checkAll = async () => {
|
|
||||||
setIsCheckingAll(true);
|
|
||||||
const promises = SOURCES.map(s => checkStatus(s.id, s.type, s.url));
|
|
||||||
await Promise.allSettled(promises);
|
|
||||||
setIsCheckingAll(false);
|
|
||||||
};
|
|
||||||
useEffect(() => {
|
|
||||||
checkAll();
|
|
||||||
}, []);
|
|
||||||
return (<div className="space-y-6">
|
return (<div className="space-y-6">
|
||||||
<div className="flex items-center justify-end">
|
<div className="flex items-center justify-end">
|
||||||
<Button variant="outline" onClick={checkAll} disabled={isCheckingAll} className="gap-2">
|
<Button variant="outline" onClick={() => void refreshAll()} disabled={isCheckingAll} className="gap-2">
|
||||||
<RefreshCw className={`h-4 w-4 ${isCheckingAll ? "animate-spin" : ""}`}/>
|
<RefreshCw className={`h-4 w-4 ${isCheckingAll ? "animate-spin" : ""}`}/>
|
||||||
Refresh All
|
Refresh All
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-4 gap-4">
|
<div className="grid grid-cols-4 gap-4">
|
||||||
{SOURCES.map((source) => {
|
{sources.map((source) => {
|
||||||
const status = statuses[source.id] || "idle";
|
const status = statuses[source.id] || "idle";
|
||||||
return (<div key={source.id} className="flex items-center justify-between p-4 border rounded-lg bg-card text-card-foreground shadow-sm">
|
return (<div key={source.id} className="flex items-center justify-between p-4 border rounded-lg bg-card text-card-foreground shadow-sm">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import {
|
||||||
|
API_SOURCES,
|
||||||
|
checkAllApiStatuses,
|
||||||
|
ensureApiStatusCheckStarted,
|
||||||
|
getApiStatusState,
|
||||||
|
subscribeApiStatus,
|
||||||
|
} from "@/lib/api-status";
|
||||||
|
|
||||||
|
export function useApiStatus() {
|
||||||
|
const [state, setState] = useState(getApiStatusState);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
ensureApiStatusCheckStarted();
|
||||||
|
return subscribeApiStatus(() => {
|
||||||
|
setState(getApiStatusState());
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
sources: API_SOURCES,
|
||||||
|
refreshAll: checkAllApiStatuses,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import { useState, useCallback } from "react";
|
|||||||
import { CheckTrackAvailability } from "../../wailsjs/go/main/App";
|
import { CheckTrackAvailability } from "../../wailsjs/go/main/App";
|
||||||
import type { TrackAvailability } from "@/types/api";
|
import type { TrackAvailability } from "@/types/api";
|
||||||
import { logger } from "@/lib/logger";
|
import { logger } from "@/lib/logger";
|
||||||
|
import { CHECK_TIMEOUT_MS, withTimeout } from "@/lib/async-timeout";
|
||||||
export function useAvailability() {
|
export function useAvailability() {
|
||||||
const [checking, setChecking] = useState(false);
|
const [checking, setChecking] = useState(false);
|
||||||
const [checkingTrackId, setCheckingTrackId] = useState<string | null>(null);
|
const [checkingTrackId, setCheckingTrackId] = useState<string | null>(null);
|
||||||
@@ -20,7 +21,11 @@ export function useAvailability() {
|
|||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
logger.info(`Checking availability for track: ${spotifyId}`);
|
logger.info(`Checking availability for track: ${spotifyId}`);
|
||||||
const response = await CheckTrackAvailability(spotifyId);
|
const response = await withTimeout(
|
||||||
|
CheckTrackAvailability(spotifyId),
|
||||||
|
CHECK_TIMEOUT_MS,
|
||||||
|
`Availability check timed out after 10 seconds for ${spotifyId}`,
|
||||||
|
);
|
||||||
const availability: TrackAvailability = JSON.parse(response);
|
const availability: TrackAvailability = JSON.parse(response);
|
||||||
setAvailabilityMap((prev) => {
|
setAvailabilityMap((prev) => {
|
||||||
const newMap = new Map(prev);
|
const newMap = new Map(prev);
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
import { CheckAPIStatus } from "../../wailsjs/go/main/App";
|
||||||
|
import { CHECK_TIMEOUT_MS, withTimeout } from "@/lib/async-timeout";
|
||||||
|
|
||||||
|
export type ApiCheckStatus = "checking" | "online" | "offline" | "idle";
|
||||||
|
|
||||||
|
export interface ApiSource {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const API_SOURCES: ApiSource[] = [
|
||||||
|
{ id: "tidal1", type: "tidal", name: "Tidal A", url: "https://hifi-one.spotisaver.net" },
|
||||||
|
{ id: "tidal2", type: "tidal", name: "Tidal B", url: "https://hifi-two.spotisaver.net" },
|
||||||
|
{ id: "tidal3", type: "tidal", name: "Tidal C", url: "https://eu-central.monochrome.tf" },
|
||||||
|
{ id: "tidal4", type: "tidal", name: "Tidal D", url: "https://us-west.monochrome.tf" },
|
||||||
|
{ id: "tidal5", type: "tidal", name: "Tidal E", url: "https://api.monochrome.tf" },
|
||||||
|
{ id: "tidal6", type: "tidal", name: "Tidal F", url: "https://monochrome-api.samidy.com" },
|
||||||
|
{ id: "tidal7", type: "tidal", name: "Tidal G", url: "https://tidal.kinoplus.online" },
|
||||||
|
{ id: "qobuz1", type: "qobuz", name: "Qobuz A", url: "https://dab.yeet.su" },
|
||||||
|
{ id: "qobuz2", type: "qobuz", name: "Qobuz B", url: "https://dabmusic.xyz" },
|
||||||
|
{ id: "qobuz3", type: "qbz", name: "Qobuz C", url: "https://qbz.afkarxyz.qzz.io" },
|
||||||
|
{ id: "amazon1", type: "amazon", name: "Amazon Music", url: "https://amzn.afkarxyz.qzz.io" },
|
||||||
|
];
|
||||||
|
|
||||||
|
type ApiStatusState = {
|
||||||
|
isCheckingAll: boolean;
|
||||||
|
statuses: Record<string, ApiCheckStatus>;
|
||||||
|
};
|
||||||
|
|
||||||
|
let apiStatusState: ApiStatusState = {
|
||||||
|
isCheckingAll: false,
|
||||||
|
statuses: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
let activeCheckAll: Promise<void> | null = null;
|
||||||
|
|
||||||
|
const listeners = new Set<() => void>();
|
||||||
|
|
||||||
|
function emitApiStatusChange() {
|
||||||
|
for (const listener of listeners) {
|
||||||
|
listener();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setApiStatusState(updater: (current: ApiStatusState) => ApiStatusState) {
|
||||||
|
apiStatusState = updater(apiStatusState);
|
||||||
|
emitApiStatusChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkSingleApiStatus(source: ApiSource): Promise<void> {
|
||||||
|
setApiStatusState((current) => ({
|
||||||
|
...current,
|
||||||
|
statuses: {
|
||||||
|
...current.statuses,
|
||||||
|
[source.id]: "checking",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const isOnline = await withTimeout(
|
||||||
|
CheckAPIStatus(source.type, source.url),
|
||||||
|
CHECK_TIMEOUT_MS,
|
||||||
|
`API status check timed out after 10 seconds for ${source.url}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
setApiStatusState((current) => ({
|
||||||
|
...current,
|
||||||
|
statuses: {
|
||||||
|
...current.statuses,
|
||||||
|
[source.id]: isOnline ? "online" : "offline",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
} catch {
|
||||||
|
setApiStatusState((current) => ({
|
||||||
|
...current,
|
||||||
|
statuses: {
|
||||||
|
...current.statuses,
|
||||||
|
[source.id]: "offline",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getApiStatusState(): ApiStatusState {
|
||||||
|
return apiStatusState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function subscribeApiStatus(listener: () => void): () => void {
|
||||||
|
listeners.add(listener);
|
||||||
|
return () => {
|
||||||
|
listeners.delete(listener);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasApiStatusResults(): boolean {
|
||||||
|
return API_SOURCES.some((source) => {
|
||||||
|
const status = apiStatusState.statuses[source.id];
|
||||||
|
return status === "online" || status === "offline";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ensureApiStatusCheckStarted(): void {
|
||||||
|
if (!activeCheckAll && !hasApiStatusResults()) {
|
||||||
|
void checkAllApiStatuses();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkAllApiStatuses(): Promise<void> {
|
||||||
|
if (activeCheckAll) {
|
||||||
|
return activeCheckAll;
|
||||||
|
}
|
||||||
|
|
||||||
|
activeCheckAll = (async () => {
|
||||||
|
setApiStatusState((current) => ({
|
||||||
|
...current,
|
||||||
|
isCheckingAll: true,
|
||||||
|
}));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Promise.allSettled(API_SOURCES.map((source) => checkSingleApiStatus(source)));
|
||||||
|
} finally {
|
||||||
|
setApiStatusState((current) => ({
|
||||||
|
...current,
|
||||||
|
isCheckingAll: false,
|
||||||
|
}));
|
||||||
|
activeCheckAll = null;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return activeCheckAll;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
export const CHECK_TIMEOUT_MS = 10 * 1000;
|
||||||
|
|
||||||
|
export function withTimeout<T>(
|
||||||
|
promise: Promise<T>,
|
||||||
|
timeoutMs: number = CHECK_TIMEOUT_MS,
|
||||||
|
message: string = `Operation timed out after ${Math.round(timeoutMs / 1000)} seconds`,
|
||||||
|
): Promise<T> {
|
||||||
|
return new Promise<T>((resolve, reject) => {
|
||||||
|
const timer = window.setTimeout(() => {
|
||||||
|
reject(new Error(message));
|
||||||
|
}, timeoutMs);
|
||||||
|
|
||||||
|
promise
|
||||||
|
.then((value) => {
|
||||||
|
window.clearTimeout(timer);
|
||||||
|
resolve(value);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
window.clearTimeout(timer);
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user