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) { const response = await api.boot(); switch (response.status) { case 401: await navigateTo('/login'); break; case 503: await navigateTo('/setup'); break; case 200: return response.data; } } export async function boot() { const response = await bootOrNavigate(async (url) => redirect(307, url)); return Session.boot(response); }