Bump welshman

This commit is contained in:
Jon Staab 2026-09-01 10:29:14 -07:00
parent d974b98c1a
commit f2b13c6ab9
3 changed files with 75 additions and 36 deletions

View file

@ -78,16 +78,16 @@
"@types/throttle-debounce": "^5.0.2", "@types/throttle-debounce": "^5.0.2",
"@vite-pwa/assets-generator": "^1.0.2", "@vite-pwa/assets-generator": "^1.0.2",
"@vite-pwa/sveltekit": "^1.1.0", "@vite-pwa/sveltekit": "^1.1.0",
"@welshman/app": "^0.9.7", "@welshman/app": "^0.9.8",
"@welshman/content": "^0.9.7", "@welshman/content": "^0.9.8",
"@welshman/domain": "^0.9.7", "@welshman/domain": "^0.9.8",
"@welshman/editor": "^0.9.7", "@welshman/editor": "^0.9.8",
"@welshman/feeds": "^0.9.7", "@welshman/feeds": "^0.9.8",
"@welshman/lib": "^0.9.7", "@welshman/lib": "^0.9.8",
"@welshman/net": "^0.9.7", "@welshman/net": "^0.9.8",
"@welshman/signer": "^0.9.7", "@welshman/signer": "^0.9.8",
"@welshman/store": "^0.9.7", "@welshman/store": "^0.9.8",
"@welshman/util": "^0.9.7", "@welshman/util": "^0.9.8",
"cheerio": "^1.2.0", "cheerio": "^1.2.0",
"compressorjs-next": "^1.1.2", "compressorjs-next": "^1.1.2",
"dompurify": "^3.4.13", "dompurify": "^3.4.13",

Binary file not shown.

View file

@ -1,6 +1,6 @@
import {derived, get, readable, writable} from "svelte/store" import {derived, get, readable, writable} from "svelte/store"
import type {Readable, Writable} from "svelte/store" import type {Readable, Writable} from "svelte/store"
import {batch, call, int, now, on, sortBy, uniqBy, MONTH, YEAR} from "@welshman/lib" import {batch, call, int, ms, now, on, sleep, sortBy, uniqBy, MONTH, YEAR} from "@welshman/lib"
import { import {
COMMENT, COMMENT,
DELETE, DELETE,
@ -324,6 +324,11 @@ export type FeedLoadState =
| {status: "searching"} | {status: "searching"}
| {status: "exhausted"} | {status: "exhausted"}
// One span of a feed's timeline. `complete` is whether the relays actually answered for it: a
// request the socket dropped reports nothing found, which is not the same thing as a span with
// nothing in it, and the two have to move the window differently.
export type FeedSpan = {found: number; complete: boolean; exhausted: boolean}
// Empty spans to walk per trigger. Enough to cross a gap; not enough to reach the end of the // Empty spans to walk per trigger. Enough to cross a gap; not enough to reach the end of the
// history on a single request. // history on a single request.
const SPANS_PER_TRIGGER = 3 const SPANS_PER_TRIGGER = 3
@ -334,7 +339,7 @@ const SPANS_PER_TRIGGER = 3
export const isFeedLoading = (state: Maybe<FeedLoadState>) => export const isFeedLoading = (state: Maybe<FeedLoadState>) =>
state?.status === "loading" || state?.status === "searching" state?.status === "loading" || state?.status === "searching"
const makeFeedLoader = (load: () => Promise<{found: number; exhausted: boolean}>) => { const makeFeedLoader = (load: () => Promise<FeedSpan>) => {
const state = writable<FeedLoadState>({status: "idle"}) const state = writable<FeedLoadState>({status: "idle"})
let running = false let running = false
@ -348,17 +353,21 @@ const makeFeedLoader = (load: () => Promise<{found: number; exhausted: boolean}>
for (let span = 0; span < SPANS_PER_TRIGGER; span++) { for (let span = 0; span < SPANS_PER_TRIGGER; span++) {
state.set({status: span > 0 ? "searching" : "loading"}) state.set({status: span > 0 ? "searching" : "loading"})
const {found, exhausted} = await load() const {found, complete, exhausted} = await load()
if (exhausted) { if (exhausted) {
state.set({status: "exhausted"}) state.set({status: "exhausted"})
return return
} }
if (found > 0) { // A span nobody answered for hasn't moved the window, so hold here rather than walking
state.set({status: "idle"}) // past it, and give the socket a moment before the scroller comes back around
return if (!complete) {
await sleep(ms(3))
break
} }
if (found > 0) break
} }
state.set({status: "idle"}) state.set({status: "idle"})
@ -376,7 +385,7 @@ const makeFeedLoader = (load: () => Promise<{found: number; exhausted: boolean}>
// ordinary feed scrolls toward the end of its own content. // ordinary feed scrolls toward the end of its own content.
export const makeScrollLoader = ( export const makeScrollLoader = (
element: HTMLElement, element: HTMLElement,
load: () => Promise<{found: number; exhausted: boolean}>, load: () => Promise<FeedSpan>,
options: Partial<ScrollerOpts> = {}, options: Partial<ScrollerOpts> = {},
) => { ) => {
const loader = makeFeedLoader(load) const loader = makeFeedLoader(load)
@ -452,41 +461,58 @@ export const makeFeed = ({
const unsubscribers = syncFeed({relays, filters, events, seen, insertEvents}) const unsubscribers = syncFeed({relays, filters, events, seen, insertEvents})
const loadTimeframe = async (since: number, until: number) => { const loadTimeframe = async (since: number, until: number) => {
let complete = false
const found = await network.get().request({ const found = await network.get().request({
relays, relays,
autoClose: true, autoClose: true,
signal: controller.signal, signal: controller.signal,
filters: filters.map(filter => ({...filter, since, until})), filters: filters.map(filter => ({...filter, since, until})),
onEose: () => {
complete = true
},
}) })
// A span that turns up nothing widens the next one, so walking a sparse history doesn't // A span that turns up nothing widens the next one, so walking a sparse history doesn't
// take dozens of round trips // take dozens of round trips
interval = found.length > 0 ? int(MONTH) : Math.round(interval * 1.5) if (complete) {
interval = found.length > 0 ? int(MONTH) : Math.round(interval * 1.5)
}
return found.length return {found: found.length, complete}
} }
// Ask for the next span in each direction. A span that comes back empty is normal while // Ask for the next span in each direction. A span that comes back empty is normal while
// walking a sparse history, so the count is reported separately from whether there is any // walking a sparse history, so the count is reported separately from whether there is any
// history left — a caller watching its list for changes would never hear about an empty one. // history left — a caller watching its list for changes would never hear about an empty one.
const loadOlder = async () => { // The window only moves once the relays have answered: a request the socket dropped looks
if (oldest < now() - int(2, YEAR)) return {found: 0, exhausted: true} // exactly like an empty span, and walking past it would leave a hole nothing goes back for.
const loadOlder = async (): Promise<FeedSpan> => {
if (oldest < now() - int(2, YEAR)) return {found: 0, complete: true, exhausted: true}
const until = oldest const until = oldest
const since = until - interval
const {found, complete} = await loadTimeframe(since, until)
oldest = until - interval if (complete) {
oldest = since
}
return {found: await loadTimeframe(oldest, until), exhausted: false} return {found, complete, exhausted: false}
} }
const loadNewer = async () => { const loadNewer = async (): Promise<FeedSpan> => {
if (newest >= now()) return {found: 0, exhausted: true} if (newest >= now()) return {found: 0, complete: true, exhausted: true}
const since = newest const since = newest
const until = Math.min(now(), since + interval)
const {found, complete} = await loadTimeframe(since, until)
newest = Math.min(now(), since + interval) if (complete) {
newest = until
}
return {found: await loadTimeframe(since, newest), exhausted: false} return {found, complete, exhausted: false}
} }
for (const url of relays) { for (const url of relays) {
@ -584,34 +610,47 @@ export const makeCalendarFeed = ({
}) })
const loadTimeframe = async (since: number, until: number) => { const loadTimeframe = async (since: number, until: number) => {
let complete = false
const found = await network.get().request({ const found = await network.get().request({
relays, relays,
autoClose: true, autoClose: true,
signal: controller.signal, signal: controller.signal,
filters: [{kinds: [EVENT_TIME], "#D": daysBetween(since, until).map(String)}], filters: [{kinds: [EVENT_TIME], "#D": daysBetween(since, until).map(String)}],
onEose: () => {
complete = true
},
}) })
return found.length return {found: found.length, complete}
} }
const loadOlder = async () => { const loadOlder = async (): Promise<FeedSpan> => {
if (oldest < now() - int(2, YEAR)) return {found: 0, exhausted: true} if (oldest < now() - int(2, YEAR)) return {found: 0, complete: true, exhausted: true}
const until = oldest const until = oldest
const since = until - interval
const {found, complete} = await loadTimeframe(since, until)
oldest = until - interval if (complete) {
oldest = since
}
return {found: await loadTimeframe(oldest, until), exhausted: false} return {found, complete, exhausted: false}
} }
const loadNewer = async () => { const loadNewer = async (): Promise<FeedSpan> => {
if (newest > now() + int(2, YEAR)) return {found: 0, exhausted: true} if (newest > now() + int(2, YEAR)) return {found: 0, complete: true, exhausted: true}
const since = newest const since = newest
const until = since + interval
const {found, complete} = await loadTimeframe(since, until)
newest = since + interval if (complete) {
newest = until
}
return {found: await loadTimeframe(since, newest), exhausted: false} return {found, complete, exhausted: false}
} }
return { return {