File size: 3,894 Bytes
0bc2c6b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
/**
 * Shared API client for all MesmerTools HuggingFace Spaces.
 *
 * Spaces are static pages that call the mesmer.tools API directly from the
 * visitor's browser. That is deliberate: per-IP rate limits then apply per
 * visitor instead of being shared through one Space backend. CORS on
 * /api/v1/* is already `*`, so cross-origin fetches work with no proxy.
 *
 * Every helper throws `RateLimitError` on a 429 so the UI can surface the
 * "use the full tool for higher limits" cross-sell, and `ApiError` otherwise.
 */

import { SITE } from "./config.js";

export class RateLimitError extends Error {
  constructor(message) {
    super(message || "You've hit the free hourly limit.");
    this.name = "RateLimitError";
    this.isRateLimit = true;
  }
}

export class ApiError extends Error {
  constructor(message, status = 0) {
    super(message || "Something went wrong.");
    this.name = "ApiError";
    this.status = status;
  }
}

const DEFAULT_TIMEOUT = 60_000;

async function doFetch(url, init, timeout) {
  const ctrl = new AbortController();
  const timer = setTimeout(() => ctrl.abort(), timeout ?? DEFAULT_TIMEOUT);
  try {
    return await fetch(url, { ...init, signal: ctrl.signal });
  } catch (err) {
    if (err && err.name === "AbortError") {
      throw new ApiError(
        "The request timed out. The free demo can be slow under load — try the full tool on mesmer.tools.",
        408,
      );
    }
    throw new ApiError("Network error. Check your connection and try again.", 0);
  } finally {
    clearTimeout(timer);
  }
}

async function readJson(res) {
  try {
    return await res.json();
  } catch {
    return null;
  }
}

/**
 * REST call. GET with `params`, or POST with a JSON `body`.
 * @returns parsed JSON response
 */
export async function callRest(apiPath, { method = "GET", params, body, timeout } = {}) {
  let url = SITE.origin + apiPath;
  const init = { method, headers: {} };
  if (params) {
    const qs = new URLSearchParams(params).toString();
    if (qs) url += (url.includes("?") ? "&" : "?") + qs;
  }
  if (body !== undefined) {
    init.headers["Content-Type"] = "application/json";
    init.body = JSON.stringify(body);
  }
  const res = await doFetch(url, init, timeout);
  const data = await readJson(res);
  if (res.status === 429) throw new RateLimitError(data && data.error);
  if (!res.ok) throw new ApiError((data && data.error) || `Request failed (${res.status}).`, res.status);
  return data;
}

/**
 * tRPC v11 mutation (superjson transformer). Used by the logo space.
 * Input is wrapped as `{ json: input }`; the unwrapped value is returned.
 */
export async function callTrpcMutation(trpcPath, input, { timeout } = {}) {
  const res = await doFetch(
    SITE.origin + trpcPath,
    {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ json: input }),
    },
    timeout,
  );
  const data = await readJson(res);
  if (res.status === 429) throw new RateLimitError(extractTrpcError(data));
  if (!res.ok) throw new ApiError(extractTrpcError(data) || `Request failed (${res.status}).`, res.status);
  // superjson success envelope: { result: { data: { json: <value> } } }
  const out = data && data.result && data.result.data;
  return out && typeof out === "object" && "json" in out ? out.json : out;
}

function extractTrpcError(data) {
  if (!data || !data.error) return null;
  // superjson error envelope: { error: { json: { message, data: {...} } } }
  return (data.error.json && data.error.json.message) || data.error.message || null;
}

/** Fetch a generated JSON artifact (benchmark, voices) from mesmer.tools. */
export async function fetchData(url, { timeout } = {}) {
  const res = await doFetch(url, { method: "GET" }, timeout ?? 20_000);
  if (!res.ok) throw new ApiError(`Could not load data (${res.status}).`, res.status);
  return readJson(res);
}