From f788ea84e25a4f7216ca0604aeb216346403b6ef Mon Sep 17 00:00:00 2001 From: Owen Jacobson Date: Tue, 25 Feb 2025 00:37:33 -0500 Subject: Track state on a per-session basis, rather than via globals. Sorry about the thousand-line omnibus change; this is functionally a rewrite of the client's state tracking, flavoured to resemble the existing code as far as is possible, rather than something that can be parted out and committed in pieces. Highlights: * No more `store.writeable()`s. All state is now tracked using state runs or derivatives. State is still largely structured the way it was, but several bits of nested state have been rewritten to ensure that their properties are reactive just as much as their containers are. * State is no longer global. `(app)/+layout` manages a stateful session, created via its load hook and started/stopped via component mount and destroy events. The session also tracks an event source for the current state, and feeds events into the state, broadly along the same lines as the previous stores-based approach. Together these two changes fix up several rough spots integrating state with Svelte, and allow for the possibility of multiple states. This is a major step towards restartable states, and thus towards better connection management, which will require the ability to "start over" once a connection is restored. --- ui/lib/state/local/channels.svelte.js | 118 ++++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 ui/lib/state/local/channels.svelte.js (limited to 'ui/lib/state/local') diff --git a/ui/lib/state/local/channels.svelte.js b/ui/lib/state/local/channels.svelte.js new file mode 100644 index 0000000..9040685 --- /dev/null +++ b/ui/lib/state/local/channels.svelte.js @@ -0,0 +1,118 @@ +import { DateTime } from 'luxon'; +import { SvelteMap } from 'svelte/reactivity'; + +import * as iter from '$lib/iterator.js'; + +export const STORE_KEY_CHANNELS_DATA = 'pilcrow:channelsData'; + +class Channel { + draft = $state(); + lastReadAt = $state(null); + scrollPosition = $state(null); + + static fromStored({ draft, lastReadAt, scrollPosition }) { + return new Channel({ + draft, + lastReadAt: lastReadAt == null ? null : DateTime.fromISO(lastReadAt), + scrollPosition + }); + } + + constructor({ draft = '', lastReadAt = null, scrollPosition = null } = {}) { + this.draft = draft; + this.lastReadAt = lastReadAt; + this.scrollPosition = scrollPosition; + } + + toStored() { + const { draft, lastReadAt, scrollPosition } = this; + return { + draft, + lastReadAt: lastReadAt?.toISO(), + scrollPosition + }; + } +} + +export class Channels { + // Store channelId -> { draft = '', lastReadAt = null, scrollPosition = null } + all = $state(); + + static fromLocalStorage() { + const stored = localStorage.getItem(STORE_KEY_CHANNELS_DATA); + if (stored !== null) { + return Channels.fromStored(stored); + } + return Channels.empty(); + } + + static fromStored(stored) { + const loaded = Object.keys(stored).map((channelId) => [ + channelId, + Channel.fromStored(stored[channelId]) + ]); + const all = new SvelteMap(loaded); + return new Channels({ all }); + } + + static empty() { + return new Channels({ all: new SvelteMap() }); + } + + constructor({ all }) { + this.all = all; + } + + channel(channelId) { + let channel = this.all.get(channelId); + if (channel === undefined) { + channel = new Channel(); + this.all.set(channelId, channel); + } + return channel; + } + + updateLastReadAt(channelId, at) { + const channel = this.channel(channelId); + // Do it this way, rather than with Math.max tricks, to avoid assignment + // when we don't need it, to minimize reactive changes: + if (channel.lastReadAt === null || at > channel.lastReadAt) { + channel.lastReadAt = at; + this.save(); + } + } + + retainChannels(channelIds) { + const retain = new Set(channelIds); + for (const channelId of Array.from(this.all.keys())) { + if (!retain.has(channelId)) { + this.all.delete(channelId); + } + } + this.save(); + } + + toStored() { + return iter.reduce( + this.all.entries(), + (stored, [channelId, channel]) => ({ + ...stored, + [channelId]: channel.toStored() + }), + {} + ); + } + + save() { + let stored = this.toStored(); + console.log(this, stored); + localStorage.setItem(STORE_KEY_CHANNELS_DATA, JSON.stringify(stored)); + } +} + +function objectMap(object, mapFn) { + return Object.keys(object).reduce((result, key) => { + result[key] = mapFn(object[key]); + return result; + }, {}); +} -- cgit v1.2.3