refactor: add post cache garbage collection and modernize settings store to Svelte 5 state
6 unmodified lines7891011126 unmodified lines{"id":"int-ef0bb2b7","kind":"field_change","created_at":"2026-06-29T14:35:47.990869363Z","actor":"dawn","issue_id":"nucleus-9ot","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Removed old package and verified typescript / prettier formatting"}}{"id":"int-a4d53813","kind":"field_change","created_at":"2026-06-29T14:35:55.325922806Z","actor":"dawn","issue_id":"nucleus-bkx","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Epic complete: all list virtualizations rewritten to use TanStack Virtual."}}{"id":"int-3ef022dd","kind":"field_change","created_at":"2026-06-29T15:44:24.7334854Z","actor":"dawn","issue_id":"nucleus-huf","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Converted global notificationStream, jetstream, and pulsingPostId stores to Svelte 5 state objects."}}{"id":"int-fb44cac5","kind":"field_change","created_at":"2026-06-29T16:02:53.45125394Z","actor":"dawn","issue_id":"nucleus-bwr","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Fixed Svelte 5 Date and Set lint errors by using Date.parse, SvelteDate, and SvelteSet."}}{"id":"int-0e35bdb1","kind":"field_change","created_at":"2026-06-29T16:32:22.208395256Z","actor":"dawn","issue_id":"nucleus-20e","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Implemented gcPosts function and background interval to prune unreferenced posts."}}{"id":"int-2be9147c","kind":"field_change","created_at":"2026-06-29T16:34:32.902566131Z","actor":"dawn","issue_id":"nucleus-cfv","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Converted settings store to Svelte 5 state and updated all usages across components and pages."}}1234566 unmodified lines{"_type":"issue","id":"nucleus-20e","title":"Implement post cache garbage collection","description":"Add a gcPosts function to state.svelte.ts that runs periodically to prune orphaned post records from allPosts map and clean up secondary indexes.","status":"closed","priority":1,"issue_type":"task","assignee":"dawn","owner":"90008@klbr.net","created_at":"2026-06-29T16:31:45Z","created_by":"dawn","updated_at":"2026-06-29T16:32:22Z","started_at":"2026-06-29T16:31:54Z","closed_at":"2026-06-29T16:32:22Z","close_reason":"Implemented gcPosts function and background interval to prune unreferenced posts.","dependency_count":0,"dependent_count":0,"comment_count":0}{"_type":"issue","id":"nucleus-cfv","title":"Modernize settings store to Svelte 5 state","description":"Convert settings store in settings.ts to Svelte 5 state object, update imports and usage in BskyPost, FeedSelector, RichText, SettingsView, and +page.svelte.","status":"closed","priority":2,"issue_type":"task","assignee":"dawn","owner":"90008@klbr.net","created_at":"2026-06-29T16:32:01Z","created_by":"dawn","updated_at":"2026-06-29T16:34:33Z","started_at":"2026-06-29T16:32:09Z","closed_at":"2026-06-29T16:34:33Z","close_reason":"Converted settings store to Svelte 5 state and updated all usages across components and pages.","dependency_count":0,"dependent_count":0,"comment_count":0}{"_type":"issue","id":"nucleus-bwr","title":"Resolve Svelte 5 Date and Set lint errors in state.svelte.ts","description":"Replace built-in Date instantiation with Date.parse or SvelteDate, and built-in Set instantiation with SvelteSet to satisfy Svelte 5 lint rules.","status":"closed","priority":2,"issue_type":"task","assignee":"dawn","owner":"90008@klbr.net","created_at":"2026-06-29T16:01:47Z","created_by":"dawn","updated_at":"2026-06-29T16:02:53Z","started_at":"2026-06-29T16:01:55Z","closed_at":"2026-06-29T16:02:53Z","close_reason":"Fixed Svelte 5 Date and Set lint errors by using Date.parse, SvelteDate, and SvelteSet.","dependency_count":0,"dependent_count":0,"comment_count":0}{"_type":"issue","id":"nucleus-huf","title":"Modernize state stores to Svelte 5 Runes","description":"Convert notificationStream, jetstream, and pulsingPostId from Svelte 4 writables to Svelte 5 state/derived runes, updating components and routes.","status":"closed","priority":2,"issue_type":"task","assignee":"dawn","owner":"90008@klbr.net","created_at":"2026-06-29T15:41:52Z","created_by":"dawn","updated_at":"2026-06-29T15:44:25Z","started_at":"2026-06-29T15:42:02Z","closed_at":"2026-06-29T15:44:25Z","close_reason":"Converted global notificationStream, jetstream, and pulsingPostId stores to Svelte 5 state objects.","dependency_count":0,"dependent_count":0,"comment_count":0}{"_type":"issue","id":"nucleus-9ot","title":"Clean up @tutorlatin/svelte-tiny-virtual-list and verify build/lints","description":"Uninstall unused dependency, run linter and build to make sure everything works perfectly","status":"closed","priority":2,"issue_type":"task","assignee":"dawn","owner":"90008@klbr.net","created_at":"2026-06-29T14:27:01Z","created_by":"dawn","updated_at":"2026-06-29T14:35:48Z","started_at":"2026-06-29T14:33:32Z","closed_at":"2026-06-29T14:35:48Z","close_reason":"Removed old package and verified typescript / prettier formatting","dependencies":[{"issue_id":"nucleus-9ot","depends_on_id":"nucleus-3w3","type":"blocks","created_at":"2026-06-29T17:28:10Z","created_by":"dawn","metadata":"{}"},{"issue_id":"nucleus-9ot","depends_on_id":"nucleus-ehl","type":"blocks","created_at":"2026-06-29T17:28:23Z","created_by":"dawn","metadata":"{}"},{"issue_id":"nucleus-9ot","depends_on_id":"nucleus-fcu","type":"blocks","created_at":"2026-06-29T17:28:15Z","created_by":"dawn","metadata":"{}"},{"issue_id":"nucleus-9ot","depends_on_id":"nucleus-ib8","type":"blocks","created_at":"2026-06-29T17:28:19Z","created_by":"dawn","metadata":"{}"},{"issue_id":"nucleus-9ot","depends_on_id":"nucleus-n16","type":"blocks","created_at":"2026-06-29T17:28:02Z","created_by":"dawn","metadata":"{}"},{"issue_id":"nucleus-9ot","depends_on_id":"nucleus-xnz","type":"blocks","created_at":"2026-06-29T17:28:06Z","created_by":"dawn","metadata":"{}"}],"dependency_count":6,"dependent_count":1,"comment_count":0}{"_type":"issue","id":"nucleus-ehl","title":"Refactor GenericTimelineView to use TanStack Virtual","description":"Replace VirtualList in GenericTimelineView.svelte with TanStack Virtual using dynamic height measurement (measureElement)","status":"closed","priority":2,"issue_type":"task","assignee":"dawn","owner":"90008@klbr.net","created_at":"2026-06-29T14:26:53Z","created_by":"dawn","updated_at":"2026-06-29T14:33:25Z","started_at":"2026-06-29T14:33:01Z","closed_at":"2026-06-29T14:33:25Z","close_reason":"refactored GenericTimelineView.svelte to use TanStack Virtual with dynamic height","dependencies":[{"issue_id":"nucleus-ehl","depends_on_id":"nucleus-n16","type":"blocks","created_at":"2026-06-29T17:27:57Z","created_by":"dawn","metadata":"{}"}],"dependency_count":1,"dependent_count":2,"comment_count":0}6 unmodified lines452 unmodified linesExpand all45345445545645745745845946046146246356 unmodified lines452 unmodified linesExpand all bind:position={actionsPos} placement="bottom-end" > {@render dropdownItem('heroicons:link-20-solid', 'copy link to post', () => navigator.clipboard.writeText(`${$settings.socialAppUrl}/profile/${did}/post/${rkey}`) navigator.clipboard.writeText( `${settings.current.socialAppUrl}/profile/${did}/post/${rkey}` ) )} {@render dropdownItem(undefined, 'copy at uri', () => navigator.clipboard.writeText(post.uri) )}56 unmodified lines12 unmodified lines13141516171718192021110 unmodified linesExpand all12 unmodified lines let { selectedFeed = $bindable(), onSelect }: Props = $props();
let isOpen = $state(false);
const savedFeeds = $derived($settings.feeds); const savedFeeds = $derived(settings.current.feeds); const sortedFeeds = $derived( [...savedFeeds].sort((a, b) => (b.pinned ? 1 : 0) - (a.pinned ? 1 : 0)) );
110 unmodified linesExpand all56 unmodified lines5758596061616263646514 unmodified lines56 unmodified lines > {:else if feature.$type === 'app.bsky.richtext.facet#tag'} <a class="text-(--nucleus-accent2)" href={`${$settings.socialAppUrl}/search?q=${encodeURIComponent('#' + feature.tag)}`} href={`${settings.current.socialAppUrl}/search?q=${encodeURIComponent('#' + feature.tag)}`} target="_blank" rel="noopener noreferrer">{@render plainText(text)}</a > {:else}14 unmodified lines1233456720 unmodified lines28293031323332333435363637383940168 unmodified linesExpand all<script lang="ts"> import { needsReload, settings } from '$lib/settings'; import { get } from 'svelte/store';
import Tabs from './Tabs.svelte'; import { portal } from 'svelte-portal'; import { router,20 unmodified lines }
let { tab }: Props = $props();
let localSettings = $state(get(settings)); let hasReloadChanges = $derived(needsReload($settings, localSettings)); let localSettings = $state($state.snapshot(settings.current)); let hasReloadChanges = $derived(needsReload(settings.current, localSettings));
$effect(() => { $settings.theme = localSettings.theme; settings.current = { ...settings.current, theme: localSettings.theme }; });
const handleSave = () => { settings.set(localSettings);168 unmodified linesExpand all1123430 unmodified lines35363738404142394041424344454445464748464748504950515253545253545556575855565760615859606162636465666768697071727374636465666775767778796970717273747576777879808182838485868780818283848586878889899091929394959697989910010110210392931041051061076 unmodified linesimport { writable } from 'svelte/store';import { defaultTheme, type Theme } from './theme';import type { FeedGenerator } from './at/feeds';
export type ApiEndpoints = Record<string, string> & {30 unmodified lines socialAppUrl: 'https://bsky.app', feeds: []};
const createSettingsStore = () => { // Prevent SSR crash if localStorage is missing const stored = typeof localStorage !== 'undefined' ? localStorage.getItem('settings') : null;const applyThemeToDocument = (theme: Theme) => { if (typeof document === 'undefined') return; document.documentElement.style.setProperty('--nucleus-bg', theme.bg); document.documentElement.style.setProperty('--nucleus-fg', theme.fg); document.documentElement.style.setProperty('--nucleus-accent', theme.accent); document.documentElement.style.setProperty('--nucleus-accent2', theme.accent2);
const initial: Partial<Settings> = stored ? JSON.parse(stored) : defaultSettings; initial.endpoints = { ...defaultSettings.endpoints, ...initial.endpoints }; initial.theme = { ...defaultSettings.theme, ...initial.theme }; initial.socialAppUrl = initial.socialAppUrl ?? defaultSettings.socialAppUrl; initial.feeds = initial.feeds ?? defaultSettings.feeds; const oldMeta = document.querySelector('meta[name="theme-color"]'); if (oldMeta) oldMeta.remove();
const { subscribe, set, update } = writable<Settings>(initial as Settings); const metaThemeColor = document.createElement('meta'); metaThemeColor.setAttribute('name', 'theme-color'); metaThemeColor.setAttribute('content', theme.bg); document.head.appendChild(metaThemeColor);};
subscribe((settings) => { if (typeof document === 'undefined') return; const theme = settings.theme; document.documentElement.style.setProperty('--nucleus-bg', theme.bg); document.documentElement.style.setProperty('--nucleus-fg', theme.fg); document.documentElement.style.setProperty('--nucleus-accent', theme.accent); document.documentElement.style.setProperty('--nucleus-accent2', theme.accent2);// Private reactive statelet _settings = $state<Settings>(defaultSettings);
const oldMeta = document.querySelector('meta[name="theme-color"]'); if (oldMeta) oldMeta.remove();if (typeof localStorage !== 'undefined') { const stored = localStorage.getItem('settings'); if (stored) { try { const parsed = JSON.parse(stored); _settings = { endpoints: { ...defaultSettings.endpoints, ...parsed.endpoints }, theme: { ...defaultSettings.theme, ...parsed.theme }, socialAppUrl: parsed.socialAppUrl ?? defaultSettings.socialAppUrl, feeds: parsed.feeds ?? defaultSettings.feeds }; } catch (e) { console.error('Failed to parse settings from localStorage', e); } }}
const metaThemeColor = document.createElement('meta'); metaThemeColor.setAttribute('name', 'theme-color'); metaThemeColor.setAttribute('content', theme.bg); document.head.appendChild(metaThemeColor); });// Apply theme initially if in browserif (typeof document !== 'undefined') { applyThemeToDocument(_settings.theme);}
return { subscribe, set: (value: Settings) => { if (typeof localStorage !== 'undefined') localStorage.setItem('settings', JSON.stringify(value)); set(value); }, update: (fn: (value: Settings) => Settings) => { update((value) => { const newValue = fn(value); if (typeof localStorage !== 'undefined') localStorage.setItem('settings', JSON.stringify(newValue)); return newValue; }); }, reset: () => { if (typeof localStorage !== 'undefined') localStorage.setItem('settings', JSON.stringify(defaultSettings)); set(defaultSettings);export const settings = { get current() { return _settings; }, set current(value: Settings) { _settings = value; if (typeof localStorage !== 'undefined') { localStorage.setItem('settings', JSON.stringify(value)); } }; applyThemeToDocument(value.theme); }, // Compatibility wrapper for settings.set set(value: Settings) { this.current = value; }, // Compatibility wrapper for settings.update update(fn: (value: Settings) => Settings) { this.current = fn(this.current); }, reset() { this.current = defaultSettings; }};
export const settings = createSettingsStore();
export const needsReload = (current: Settings, other: Settings): boolean => { return ( current.endpoints.slingshot !== other.endpoints.slingshot || current.endpoints.spacedust !== other.endpoints.spacedust ||6 unmodified lines1113 unmodified linesExpand all11141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511201186118711881189119011911192119311941195119611971198119912001113 unmodified linesExpand all else handleBacklink(event); }};
export const gcPosts = () => { const activeUris = new SvelteSet<ResourceUri>();
// 1. Collect all URIs from timelines for (const timeline of timelines.values()) { for (const uri of timeline) { activeUris.add(uri); } }
// 2. Collect all URIs from feedTimelines for (const feedMap of feedTimelines.values()) { for (const feedUris of feedMap.values()) { for (const uri of feedUris) { activeUris.add(uri); } } }
// 3. Collect all URIs from followingFeed for (const feed of followingFeed.values()) { for (const uri of feed) { activeUris.add(uri); } }
// 4. Collect all URIs from followingBuffer for (const buffer of followingBuffer.values()) { for (const uri of buffer) { activeUris.add(uri); } }
// 5. Clean up allPosts let prunedCount = 0; for (const uri of allPosts.keys()) { if (!activeUris.has(uri)) { allPosts.delete(uri); prunedCount++;
// remove from other index maps postsByDid.forEach((set) => set.delete(uri)); postsByRootUri.forEach((set) => set.delete(uri)); replyIndex.forEach((set) => set.delete(uri)); deletedPosts.delete(uri); } }
// 6. Clean up empty sets/maps in index maps to prevent memory leak for (const [did, set] of postsByDid) { if (set.size === 0) postsByDid.delete(did); } for (const [rootUri, set] of postsByRootUri) { if (set.size === 0) postsByRootUri.delete(rootUri); } for (const [did, set] of replyIndex) { if (set.size === 0) replyIndex.delete(did); }
if (prunedCount > 0) { console.log( `[GC] Pruned ${prunedCount} unused posts from cache. Active posts: ${allPosts.size}` ); }};
export const currentTime = new SvelteDate();
if (typeof window !== 'undefined')if (typeof window !== 'undefined') { setInterval(() => { currentTime.setTime(Date.now()); }, 1000);
// Run post cache garbage collection every 3 minutes setInterval( () => { gcPosts(); }, 1000 * 60 * 3 );}
export const router = new Router();12335678945678910import { settings } from '$lib/settings';import type { Did } from '@atcute/lexicons';import { get } from 'svelte/store';
export const slingshotUrl: URL = new URL(get(settings).endpoints.slingshot);export const spacedustUrl: URL = new URL(get(settings).endpoints.spacedust);export const constellationUrl: URL = new URL(get(settings).endpoints.constellation);export const pocketUrl: URL = new URL(get(settings).endpoints.pocket);export const cdnUrl: URL = new URL(get(settings).endpoints.cdn);export const slingshotUrl: URL = new URL(settings.current.endpoints.slingshot);export const spacedustUrl: URL = new URL(settings.current.endpoints.spacedust);export const constellationUrl: URL = new URL(settings.current.endpoints.constellation);export const pocketUrl: URL = new URL(settings.current.endpoints.pocket);export const cdnUrl: URL = new URL(settings.current.endpoints.cdn);
export const httpToDidWeb = (url: string): Did => `did:web:${new URL(url).hostname}`;123 unmodified linesExpand all124125126127128128129130130131132133134249 unmodified linesExpand all123 unmodified linesExpand all 'app.bsky.graph.block:subject' ); });
console.log(`creating jetstream subscription to ${$settings.endpoints.jetstream}`); console.log(`creating jetstream subscription to ${settings.current.endpoints.jetstream}`); const jetstreamSub = new JetstreamSubscription({ url: $settings.endpoints.jetstream, url: settings.current.endpoints.jetstream, wantedCollections: ['app.bsky.feed.post'], // this is here because if wantedDids is zero jetstream will send all events wantedDids: ['did:web:guestbook.gaze.systems'] });249 unmodified linesExpand all