File size: 3,351 Bytes
b2c1c67
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
// API client: token handling, automatic refresh, JSON requests and SSE streaming.

const TOKEN_KEY = "synapse_tokens";

export function getTokens() {
  try {
    return JSON.parse(localStorage.getItem(TOKEN_KEY)) || null;
  } catch {
    return null;
  }
}

export function setTokens(tokens) {
  if (tokens) localStorage.setItem(TOKEN_KEY, JSON.stringify(tokens));
  else localStorage.removeItem(TOKEN_KEY);
}

let onUnauthorized = () => {};
export function setUnauthorizedHandler(handler) {
  onUnauthorized = handler;
}

async function tryRefresh() {
  const tokens = getTokens();
  if (!tokens?.refresh_token) return false;
  const response = await fetch("/api/auth/refresh", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ refresh_token: tokens.refresh_token }),
  });
  if (!response.ok) return false;
  setTokens(await response.json());
  return true;
}

async function rawRequest(path, options = {}) {
  const tokens = getTokens();
  const headers = { ...(options.headers || {}) };
  if (tokens?.access_token) headers.Authorization = `Bearer ${tokens.access_token}`;
  if (options.body && !(options.body instanceof FormData)) {
    headers["Content-Type"] = "application/json";
    options = { ...options, body: JSON.stringify(options.body) };
  }
  return fetch(path, { ...options, headers });
}

export async function api(path, options = {}) {
  let response = await rawRequest(path, options);
  if (response.status === 401 && (await tryRefresh())) {
    response = await rawRequest(path, options);
  }
  if (response.status === 401) {
    setTokens(null);
    onUnauthorized();
    throw new Error("Session expired. Please sign in again.");
  }
  if (!response.ok) {
    let detail = `Request failed (${response.status})`;
    try {
      const body = await response.json();
      if (body.detail) detail = typeof body.detail === "string" ? body.detail : detail;
    } catch {
      /* keep default detail */
    }
    throw new Error(detail);
  }
  if (response.status === 204) return null;
  return response.json();
}

// Stream a chat reply. handlers: { onEvent(event), onError(err) }
export async function streamChat(sessionId, body, handlers, signal) {
  let response = await rawRequest(`/api/chat/${sessionId}`, {
    method: "POST",
    body,
    signal,
  });
  if (response.status === 401 && (await tryRefresh())) {
    response = await rawRequest(`/api/chat/${sessionId}`, { method: "POST", body, signal });
  }
  if (!response.ok || !response.body) {
    let detail = `Chat request failed (${response.status})`;
    try {
      const errBody = await response.json();
      if (typeof errBody.detail === "string") detail = errBody.detail;
    } catch {
      /* ignore */
    }
    throw new Error(detail);
  }

  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  let buffer = "";
  for (;;) {
    const { done, value } = await reader.read();
    if (done) break;
    buffer += decoder.decode(value, { stream: true });
    const blocks = buffer.split("\n\n");
    buffer = blocks.pop() ?? "";
    for (const block of blocks) {
      const line = block.trim();
      if (!line.startsWith("data: ")) continue;
      try {
        handlers.onEvent(JSON.parse(line.slice(6)));
      } catch {
        /* skip malformed frames */
      }
    }
  }
}