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);
}
|