summaryrefslogtreecommitdiff
path: root/ui/lib/session.svelte.js
blob: 21a391d898841e9927a49a1c74e1a5878b52abd6 (plain)
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
116
117
import { redirect } from '@sveltejs/kit';

import { goto } from '$app/navigation';

import * as api from './apiServer.js';
import * as r from './state/remote/state.svelte.js';
import * as l from './state/local/channels.svelte.js';
import { Watchdog } from './watchdog.js';

class Session {
  remote = $state();
  local = $state();
  currentUser = $derived(this.remote.currentUser);
  users = $derived(this.remote.users.all);
  channels = $derived(this.remote.channels.all);
  messages = $derived(
    this.remote.messages.all.map((message) =>
      message.resolve({ sender: (id) => this.users.get(id) })
    )
  );

  static boot({ user, users, channels, messages, resume_point, heartbeat }) {
    const remote = r.State.boot({
      currentUser: user,
      users,
      channels,
      messages,
      resumePoint: resume_point,
      heartbeat
    });
    const local = l.Channels.fromLocalStorage();
    return new Session(remote, local);
  }

  reboot({ user, users, channels, messages, resume_point, heartbeat }) {
    this.remote = r.State.boot({
      currentUser: user,
      users,
      channels,
      messages,
      resumePoint: resume_point,
      heartbeat
    });
  }

  constructor(remote, local) {
    this.watchdog = new Watchdog(this.watchdogExpired.bind(this));
    this.remote = remote;
    this.local = local;
  }

  begin() {
    this.events = api.subscribeToEvents(this.remote.resumePoint);
    this.events.onmessage = this.onMessage.bind(this);
    this.watchdog.reset(this.heartbeatMillis());
  }

  end() {
    this.watchdog.stop();
    this.events.close();
    this.events = null;
  }

  active() {
    return this.events !== null;
  }

  onMessage(message) {
    const event = JSON.parse(message.data);
    this.remote.onEvent(event);
    this.local.retainChannels(this.remote.channels.all.keys());
    this.watchdog.reset(this.heartbeatMillis());
  }

  heartbeatMillis() {
    return this.remote.heartbeat /* in seconds */ * 1000 /* millis */;
  }

  async watchdogExpired() {
    // We leave `this.events` set here as a marker that the interruption is temporary. That's then
    // used below, after a potential delay, to decide whether to start the stream back up again or
    // not.
    this.events.close();
    this.watchdog.stop();

    const response = await bootOrNavigate(goto);
    // Session abandoned; give up here. We need to do this after each await, because that's time in
    // which the session may have been abandoned.
    if (!this.active()) return;

    this.reboot(response);
    this.begin();
  }
}

async function bootOrNavigate(navigateTo) {
  try {
    const response = await api.retry(async () => await api.boot());
    return response.data;
  } catch (err) {
    switch (true) {
      case err instanceof api.LoggedOut:
        await navigateTo('/login');
        break;
      case err instanceof api.SetupRequired:
        await navigateTo('/setup');
        break;
      default:
        throw err;
    }
  }
}

export async function boot() {
  const response = await bootOrNavigate(async (url) => redirect(307, url));
  return Session.boot(response);
}