File size: 3,422 Bytes
0815e01
 
66fd359
 
 
 
 
 
 
0815e01
 
 
 
 
 
 
66fd359
 
 
0815e01
 
 
 
 
 
66fd359
 
b6f5bc4
 
 
 
 
66fd359
 
0815e01
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import { track } from "./busy.js";
import { onSignOut, token } from "./session.js";

export class ApiError extends Error {
  constructor(status, message) { super(message); this.status = status; }
}

const MESSAGES = { 503: "砖讬专讜转 讛讛转讞讘专讜转 诇讗 讝诪讬谉 讻专讙注, 谞住讜 砖讜讘 讘注讜讚 专讙注", 403: "讗讬谉 讛专砖讗讛 诇驻注讜诇讛 讛讝讜" };

const remembered = new Map();                    // GET path -> the last answer
let changes = 0;                                 // counts changes, so an answer to a read that began before one is not kept
onSignOut(() => { remembered.clear(); changes++; });

async function request(path, { method = "GET", body } = {}) {
  const started = changes;
  if (method !== "GET") { remembered.clear(); changes++; }      // after a change of theirs, no old answer is ever shown
  const headers = { Authorization: "Bearer " + await token() };
  if (body !== undefined) headers["Content-Type"] = "application/json";
  const response = await fetch(path, { method, headers, body: body === undefined ? undefined : JSON.stringify(body) });
  if (response.ok) {
    const data = response.status === 204 ? null : await response.json();
    if (method === "GET") { if (started === changes) remembered.set(path, data); }
    else { remembered.clear(); changes++; }
    return data;
  }
  let detail = "";
  try { detail = (await response.json()).detail; } catch { /* no body */ }
  if (detail && typeof detail === "object" && typeof detail.message === "string") {        // {message, problems}: a refusal with its list
    const error = new ApiError(response.status, detail.message);
    error.problems = Array.isArray(detail.problems) ? detail.problems : [];
    throw error;
  }
  throw new ApiError(response.status, typeof detail === "string" && detail ? detail : (MESSAGES[response.status] || "诪砖讛讜 讛砖转讘砖, 谞住讜 砖讜讘"));
}

/** A request to the server. The button that was just pressed shows it is working if the answer takes a moment (busy.js). */
export const api = (path, options) => track(request(path, options));

/** The last answer for this GET path, or undefined (nothing yet, or something was changed since). */
export const lastAnswer = path => remembered.get(path);

let screen = 0;
const busyNow = () => {
  const here = document.activeElement;
  return !!(here && here.matches?.("input, textarea, select, [contenteditable]")) || !!document.querySelector("main details[open], .sheet, dialog[open]");
};

/** Shows a screen: draws `render(data)` at once from the last answer if there is one, then draws again when the fresh answer
 *  differs, unless the person has moved on, is typing, or has something open. Without a last answer it waits, as before.
 *  `path` may be a list: then `render` gets the answers as a list. */
export async function swr(path, render) {
  const paths = Array.isArray(path) ? path : [path], one = !Array.isArray(path);
  const give = list => render(one ? list[0] : list);
  const mine = ++screen, hash = location.hash, before = paths.map(p => remembered.get(p));
  if (before.some(x => x === undefined)) return give(await Promise.all(paths.map(p => api(p))));
  give(before);
  Promise.all(paths.map(p => api(p))).then(fresh => {
    if (mine === screen && hash === location.hash && JSON.stringify(fresh) !== JSON.stringify(before) && !busyNow()) give(fresh);
  }).catch(() => { /* the screen already shows what it knew */ });
}