tracks lexicons and how many times they appeared on the jetstream

feat(client): add toggling between showing since stream start vs server init

ptr.pet e8cda421 a4dd7621

verified
+99 -22
+1 -1
client/src/lib/components/BskyToggle.svelte
··· 11 11 <!-- svelte-ignore a11y_no_static_element_interactions --> 12 12 <button 13 13 onclick={onBskyToggle} 14 - class="wsbadge !mt-0 !font-normal bg-yellow-100 hover:bg-yellow-200 border-yellow-300" 14 + class="wsbadge !mt-0 !font-normal bg-blue-100 hover:bg-blue-200 border-blue-300" 15 15 > 16 16 <input checked={dontShowBsky} type="checkbox" /> 17 17 <span class="ml-0.5"> hide app.bsky.* </span>
+4 -4
client/src/lib/components/RefreshControl.svelte
··· 8 8 </script> 9 9 10 10 <div 11 - class="wsbadge !pl-2 !px-1 !mt-0 !font-normal bg-green-100 hover:bg-green-200 border-green-300" 11 + class="wsbadge !pl-2 !px-1 !mt-0 !font-normal bg-lime-100 hover:bg-lime-200 border-lime-300" 12 12 > 13 - <label for="refresh-rate" class="text-green-800 mr-1">refresh:</label> 13 + <label for="refresh-rate" class="text-lime-800 mr-1">refresh:</label> 14 14 <input 15 15 id="refresh-rate" 16 16 value={refreshRate} ··· 24 24 pattern="[0-9]*" 25 25 min="0" 26 26 placeholder="real-time" 27 - class="bg-green-50 text-green-900 placeholder-green-400 border border-green-200 rounded-full px-1 outline-none focus:bg-white focus:border-green-400 min-w-0 w-20" 27 + class="bg-green-50 text-lime-900 placeholder-lime-600 border border-lime-200 rounded-full px-1 outline-none focus:bg-white focus:border-lime-400 min-w-0 w-20" 28 28 /> 29 - <span class="text-green-700">s</span> 29 + <span class="text-lime-700">s</span> 30 30 </div>
+29
client/src/lib/components/ShowControls.svelte
··· 1 + <script lang="ts"> 2 + import type { ShowOption } from "$lib/types"; 3 + 4 + interface Props { 5 + show: ShowOption; 6 + onShowChange: (value: ShowOption) => void; 7 + } 8 + 9 + let { show, onShowChange }: Props = $props(); 10 + 11 + const showOptions: ShowOption[] = ["server init", "stream start"]; 12 + </script> 13 + 14 + <div 15 + class="wsbadge !pl-2 !px-1 !mt-0 !font-normal bg-pink-100 hover:bg-pink-200 border-pink-300" 16 + > 17 + <label for="show" class="text-pink-800 mr-1"> show since: </label> 18 + <select 19 + id="show" 20 + value={show} 21 + onchange={(e) => 22 + onShowChange((e.target as HTMLSelectElement).value as ShowOption)} 23 + class="bg-pink-50 text-pink-900 border border-pink-200 rounded-full px-1 outline-none focus:bg-white focus:border-pink-400 min-w-0" 24 + > 25 + {#each showOptions as option} 26 + <option value={option}>{option}</option> 27 + {/each} 28 + </select> 29 + </div>
+1
client/src/lib/types.ts
··· 18 18 }; 19 19 20 20 export type SortOption = "total" | "created" | "deleted" | "date"; 21 + export type ShowOption = "server init" | "stream start";
+64 -17
client/src/routes/+page.svelte
··· 4 4 EventRecord, 5 5 Events, 6 6 NsidCount, 7 + ShowOption, 7 8 Since, 8 9 SortOption, 9 10 } from "$lib/types"; 10 11 import { onMount, onDestroy } from "svelte"; 11 - import { writable } from "svelte/store"; 12 + import { get, writable } from "svelte/store"; 12 13 import { PUBLIC_API_URL } from "$env/static/public"; 13 14 import { fetchEvents, fetchTrackingSince } from "$lib/api"; 14 15 import { createRegexFilter } from "$lib/filter"; ··· 20 21 import BskyToggle from "$lib/components/BskyToggle.svelte"; 21 22 import RefreshControl from "$lib/components/RefreshControl.svelte"; 22 23 import { formatTimestamp } from "$lib/format"; 24 + import ShowControls from "$lib/components/ShowControls.svelte"; 23 25 24 26 type Props = { 25 27 data: { events: Events; trackingSince: Since }; ··· 30 32 const events = writable( 31 33 new Map<string, EventRecord>(Object.entries(data.events.events)), 32 34 ); 35 + const eventsStart = new Map<string, EventRecord>( 36 + Object.entries(data.events.events), 37 + ); 33 38 const pendingUpdates = new Map<string, EventRecord>(); 34 - let eventsList: NsidCount[] = $state([]); 39 + 35 40 let updateTimer: NodeJS.Timeout | null = null; 36 - events.subscribe((value) => { 37 - eventsList = value 38 - .entries() 39 - .map(([nsid, event]) => ({ 40 - nsid, 41 - ...event, 42 - })) 43 - .toArray(); 44 - }); 45 41 let per_second = $state(data.events.per_second); 46 42 let tracking_since = $state(data.trackingSince.since); 47 43 44 + const diffEvents = ( 45 + oldEvents: Map<string, EventRecord>, 46 + newEvents: Map<string, EventRecord>, 47 + ): NsidCount[] => { 48 + const nsidCounts: NsidCount[] = []; 49 + for (const [nsid, event] of newEvents.entries()) { 50 + const oldEvent = oldEvents.get(nsid); 51 + if (oldEvent) { 52 + const counts = { 53 + nsid, 54 + count: event.count - oldEvent.count, 55 + deleted_count: event.deleted_count - oldEvent.deleted_count, 56 + last_seen: event.last_seen, 57 + }; 58 + if (counts.count > 0 || counts.deleted_count > 0) 59 + nsidCounts.push(counts); 60 + } else { 61 + nsidCounts.push({ 62 + nsid, 63 + ...event, 64 + }); 65 + } 66 + } 67 + return nsidCounts; 68 + }; 48 69 const applyEvents = (newEvents: Record<string, EventRecord>) => { 49 70 events.update((map) => { 50 71 for (const [nsid, event] of Object.entries(newEvents)) { ··· 54 75 }); 55 76 }; 56 77 78 + let error: string | null = $state(null); 79 + let filterRegex = $state(""); 80 + let dontShowBsky = $state(false); 81 + let sortBy: SortOption = $state("total"); 82 + let refreshRate = $state(""); 83 + let changedByUser = $state(false); 84 + let show: ShowOption = $state("server init"); 85 + let eventsList: NsidCount[] = $state([]); 86 + let updateEventsList = $derived((value: Map<string, EventRecord>) => { 87 + switch (show) { 88 + case "server init": 89 + eventsList = value 90 + .entries() 91 + .map(([nsid, event]) => ({ 92 + nsid, 93 + ...event, 94 + })) 95 + .toArray(); 96 + break; 97 + case "stream start": 98 + eventsList = diffEvents(eventsStart, value); 99 + break; 100 + } 101 + }); 102 + events.subscribe((value) => updateEventsList(value)); 57 103 let all: EventRecord = $derived( 58 104 eventsList.reduce( 59 105 (acc, event) => { ··· 73 119 }, 74 120 ), 75 121 ); 76 - let error: string | null = $state(null); 77 - let filterRegex = $state(""); 78 - let dontShowBsky = $state(false); 79 - let sortBy: SortOption = $state("total"); 80 - let refreshRate = $state(""); 81 - let changedByUser = $state(false); 82 122 83 123 let websocket: WebSocket | null = null; 84 124 let isStreamOpen = $state(false); ··· 294 334 refreshRate = "2"; 295 335 else if (refreshRate === "2" && changedByUser === false) 296 336 refreshRate = ""; 337 + }} 338 + /> 339 + <ShowControls 340 + {show} 341 + onShowChange={(value: ShowOption) => { 342 + show = value; 343 + updateEventsList(get(events)); 297 344 }} 298 345 /> 299 346 <RefreshControl