summaryrefslogtreecommitdiff
path: root/web/static/sync.js
blob: 3cf6ccae90ef49bf82c0a453716d53b9c314bd09 (plain) (blame)
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
export const mountSync = () => {
  const form = /** @type {HTMLFormElement | null} */ (document.getElementById("sync-form"));
  if (!form) return;
  /** @type {HTMLButtonElement} */
  const button = form.querySelector("button[type=submit]");
  /** @type {HTMLElement} */
  const progressPanel = document.getElementById("sync-progress");
  const progressBar = /** @type {HTMLProgressElement} */ (document.getElementById("sync-progress-bar"));
  /** @type {HTMLElement} */
  const progressCount = document.getElementById("sync-progress-count");
  /** @type {HTMLElement} */
  const progressDetails = document.getElementById("sync-progress-details");
  /** @type {HTMLElement} */
  const notice = document.getElementById("sync-notice");
  /** @type {HTMLElement} */
  const error = document.getElementById("sync-error");

  const showError = (message) => {
    error.textContent = message;
    error.hidden = false;
  };
  const updateProgress = (progress) => {
    progressPanel.hidden = false;
    progressBar.max = Math.max(progress.total, 1);
    progressBar.value = progress.completed;
    progressCount.textContent = `${progress.completed} / ${progress.total}`;
    progressDetails.textContent = `${progress.imported} imported · ${progress.skipped} skipped`;
  };
  const handleEvent = (eventName, data) => {
    if (eventName === "progress") {
      updateProgress(data);
      return false;
    }
    if (eventName === "complete") {
      updateProgress(data);
      notice.textContent = `Sync complete: ${data.imported} imported, ${data.skipped} skipped.`;
      notice.hidden = false;
      return true;
    }
    if (eventName === "error") {
      if (data.progress) updateProgress(data.progress);
      throw new Error(data.message || "Strava sync failed");
    }
    return false;
  };
  const consumeEvents = async (response) => {
    const reader = response.body.getReader();
    const decoder = new TextDecoder();
    let buffer = "";
    let completed = false;
    const consumeBlock = (block) => {
      let eventName = "message";
      let data = "";
      for (const line of block.split(/\r?\n/)) {
        if (line.startsWith("event: ")) eventName = line.slice(7);
        if (line.startsWith("data: ")) data += line.slice(6);
      }
      if (data) completed = handleEvent(eventName, JSON.parse(data)) || completed;
    };
    while (true) {
      const { value, done } = await reader.read();
      buffer += decoder.decode(value || new Uint8Array(), { stream: !done });
      const blocks = buffer.split(/\r?\n\r?\n/);
      buffer = blocks.pop();
      for (const block of blocks) consumeBlock(block);
      if (done) break;
    }
    if (buffer.trim()) consumeBlock(buffer);
    if (!completed) throw new Error("Strava sync ended before completion");
  };

  form.addEventListener("submit", async (event) => {
    event.preventDefault();
    button.disabled = true;
    button.textContent = "Syncing...";
    notice.hidden = true;
    error.hidden = true;
    progressPanel.hidden = false;
    progressCount.textContent = "0 / 0";
    progressDetails.textContent = "Preparing import...";
    try {
      const response = await fetch(form.action, {
        method: "POST",
        body: new FormData(form),
        headers: { Accept: "text/event-stream" },
      });
      if (response.redirected) {
        window.location.assign(response.url);
        return;
      }
      if (!response.ok) throw new Error((await response.text()) || `Sync failed (HTTP ${response.status})`);
      if (!response.body) throw new Error("The browser does not support streaming responses");
      await consumeEvents(response);
    } catch (caught) {
      showError(caught instanceof Error ? caught.message : "Strava sync failed");
    } finally {
      button.disabled = false;
      button.textContent = "Sync rides";
    }
  });
};

mountSync();