Optimize ThunkStatusOrError
This commit is contained in:
parent
a14f599fcb
commit
3bf3927896
13 changed files with 106 additions and 21 deletions
|
|
@ -47,7 +47,7 @@
|
|||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<ThunkStatusOrDeleted {event}>
|
||||
<ThunkStatusOrDeleted {event} {context}>
|
||||
<ReactionSummary
|
||||
{url}
|
||||
{event}
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@
|
|||
Posted in #<RoomName {h} {url} />
|
||||
</Link>
|
||||
{/if}
|
||||
<ThunkStatusOrDeleted {event}>
|
||||
<ThunkStatusOrDeleted {event} {context}>
|
||||
<ReactionSummary
|
||||
{url}
|
||||
{event}
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@
|
|||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<ThunkStatusOrDeleted {event}>
|
||||
<ThunkStatusOrDeleted {event} {context}>
|
||||
{#snippet status()}
|
||||
<ClassifiedStatus {event} />
|
||||
{/snippet}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@
|
|||
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<div class="flex grow flex-wrap justify-end gap-2">
|
||||
<ThunkStatusOrDeleted {event}>
|
||||
<ThunkStatusOrDeleted {event} {context}>
|
||||
<ReactionSummary
|
||||
{url}
|
||||
{event}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@
|
|||
Posted in #<RoomName {h} {url} />
|
||||
</Link>
|
||||
{/if}
|
||||
<ThunkStatusOrDeleted {event}>
|
||||
<ThunkStatusOrDeleted {event} {context}>
|
||||
<ReactionSummary
|
||||
{url}
|
||||
{event}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@
|
|||
Posted in #<RoomName {h} {url} />
|
||||
</Link>
|
||||
{/if}
|
||||
<ThunkStatusOrDeleted {event}>
|
||||
<ThunkStatusOrDeleted {event} {context}>
|
||||
<ReactionSummary
|
||||
{url}
|
||||
{event}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts">
|
||||
import cx from "classnames"
|
||||
import {get, readable} from "svelte/store"
|
||||
import {readable} from "svelte/store"
|
||||
import {
|
||||
hash,
|
||||
gte,
|
||||
|
|
@ -39,6 +39,7 @@
|
|||
import RoomItemMenuMobile from "@app/components/RoomItemMenuMobile.svelte"
|
||||
import RoomItemContent from "@app/components/RoomItemContent.svelte"
|
||||
import {profiles, thunks, user} from "@app/core"
|
||||
import {noThunks, thunksByEventId} from "@app/thunks"
|
||||
import {colors} from "@app/theme"
|
||||
import {ENABLE_ZAPS} from "@app/env"
|
||||
import type {FeedContext} from "@app/feeds"
|
||||
|
|
@ -69,7 +70,7 @@
|
|||
const h = tagValue(tagSpec("h"), event.tags)
|
||||
const today = formatTimestampAsDate(now())
|
||||
const profileDisplay = $profiles.display(event.pubkey, [url]).$
|
||||
const thunk = $thunks.merge(get($thunks.history).filter(t => t.event.id === event.id))
|
||||
const thunk = $derived($thunks.merge($thunksByEventId.get(event.id) ?? noThunks))
|
||||
const [_, colorValue] = colors[hash(event.pubkey) % colors.length]
|
||||
|
||||
const qTag = matchTag(tagSpec("q"), event.tags)
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@
|
|||
Posted in #<RoomName {h} {url} />
|
||||
</Link>
|
||||
{/if}
|
||||
<ThunkStatusOrDeleted {event}>
|
||||
<ThunkStatusOrDeleted {event} {context}>
|
||||
<ReactionSummary
|
||||
{url}
|
||||
{event}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
|
||||
<div class="flex w-full justify-end px-1 text-xs {restProps.class}">
|
||||
<span class="flex items-center gap-1">
|
||||
<span class="spinner-xs mx-1 h-3 w-3 translate-y-px"></span>
|
||||
<span class="spinner spinner-xs mx-1 translate-y-px"></span>
|
||||
<span class="opacity-50">Sending...</span>
|
||||
<button
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -2,24 +2,22 @@
|
|||
import type {Snippet} from "svelte"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {PublishStatus} from "@welshman/net"
|
||||
import {deriveIsDeleted} from "@app/repository"
|
||||
import ThunkStatus from "@app/components/ThunkStatus.svelte"
|
||||
import {thunks} from "@app/core"
|
||||
import type {FeedContext} from "@app/feeds"
|
||||
import {noThunks, thunksByEventId} from "@app/thunks"
|
||||
|
||||
type Props = {
|
||||
event: TrustedEvent
|
||||
context: FeedContext
|
||||
status?: Snippet
|
||||
children?: Snippet
|
||||
}
|
||||
|
||||
const {event, status, children}: Props = $props()
|
||||
|
||||
// Editing a replaceable event hands this a different event
|
||||
const deleted = $derived(deriveIsDeleted(event))
|
||||
const history = $thunks.history
|
||||
// Subscribed rather than read: a thunk mutates its results in place and notifies, so reading
|
||||
// them off the object would leave this showing a publish that has since finished.
|
||||
const thunk = $derived($thunks.merge($history.filter(t => t.event.id === event.id)))
|
||||
const {event, context, status, children}: Props = $props()
|
||||
const deleted = $derived(context.deleted(event))
|
||||
const pending = $derived($thunksByEventId.get(event.id) ?? noThunks)
|
||||
const thunk = $derived($thunks.merge(pending))
|
||||
</script>
|
||||
|
||||
{#if $deleted}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
addressTags,
|
||||
getAddress,
|
||||
getCommentFiltersForRoot,
|
||||
getIdOrAddress,
|
||||
getReplyFilters,
|
||||
hexTags,
|
||||
isReplaceableKind,
|
||||
|
|
@ -48,6 +49,7 @@ export const makeFeedContext = ({relays}: {relays: string[] | Promise<string[]>}
|
|||
const eventsByTarget = new Map<string, TrustedEvent[]>()
|
||||
const targetsByEventId = new Map<string, string[]>()
|
||||
const subscribersByTarget = new Map<string, Set<(events: TrustedEvent[]) => void>>()
|
||||
const deletedChecks = new Map<string, Set<() => void>>()
|
||||
|
||||
const addEvent = (event: TrustedEvent, touched: Set<string>) => {
|
||||
// An event seen before its target was tracked stays unfiled, so that adding the target
|
||||
|
|
@ -86,6 +88,22 @@ export const makeFeedContext = ({relays}: {relays: string[] | Promise<string[]>}
|
|||
}
|
||||
}
|
||||
|
||||
const notifyDeleted = (added: TrustedEvent[], removed: Set<string>) => {
|
||||
if (deletedChecks.size > 0) {
|
||||
if (removed.size > 0) {
|
||||
for (const checks of deletedChecks.values()) {
|
||||
for (const check of checks) check()
|
||||
}
|
||||
} else {
|
||||
for (const event of added) {
|
||||
for (const check of deletedChecks.get(getIdOrAddress(event)) || []) {
|
||||
check()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const notify = (touched: Set<string>) => {
|
||||
for (const target of touched) {
|
||||
for (const subscriber of subscribersByTarget.get(target) || []) {
|
||||
|
|
@ -146,6 +164,7 @@ export const makeFeedContext = ({relays}: {relays: string[] | Promise<string[]>}
|
|||
}
|
||||
|
||||
notify(touched)
|
||||
notifyDeleted(added, removed)
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
@ -183,6 +202,29 @@ export const makeFeedContext = ({relays}: {relays: string[] | Promise<string[]>}
|
|||
|
||||
return {
|
||||
add,
|
||||
deleted: (event: TrustedEvent) =>
|
||||
readable(repository.isDeleted(event), set => {
|
||||
const key = getIdOrAddress(event)
|
||||
const check = () => set(repository.isDeleted(event))
|
||||
|
||||
let checks = deletedChecks.get(key)
|
||||
|
||||
if (!checks) {
|
||||
checks = new Set()
|
||||
deletedChecks.set(key, checks)
|
||||
}
|
||||
|
||||
checks.add(check)
|
||||
check()
|
||||
|
||||
return () => {
|
||||
checks.delete(check)
|
||||
|
||||
if (checks.size === 0) {
|
||||
deletedChecks.delete(key)
|
||||
}
|
||||
}
|
||||
}),
|
||||
related: (event: TrustedEvent): Readable<TrustedEvent[]> => {
|
||||
add(event)
|
||||
|
||||
|
|
|
|||
43
src/app/thunks.ts
Normal file
43
src/app/thunks.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import {derived} from "svelte/store"
|
||||
import {pushToMapKey} from "@welshman/lib"
|
||||
import {Thunks} from "@welshman/app"
|
||||
import type {Thunk} from "@welshman/app"
|
||||
import {fromApp} from "@app/core"
|
||||
|
||||
let previous = new Map<string, Thunk[]>()
|
||||
|
||||
// Publishes indexed by the event they carry.
|
||||
//
|
||||
// Every row that shows publish status wants the thunks for one event, and each of them filtering
|
||||
// the whole history is O(rows × history) on every publish — history only shrinks when a thunk is
|
||||
// aborted, so it grows for as long as the session lasts. Indexing once leaves each row a lookup.
|
||||
export const thunksByEventId = derived(
|
||||
fromApp($app => $app.use(Thunks).history),
|
||||
$history => {
|
||||
const byId = new Map<string, Thunk[]>()
|
||||
|
||||
for (const thunk of $history) {
|
||||
pushToMapKey(byId, thunk.event.id, thunk)
|
||||
}
|
||||
|
||||
// Hand back the array from last time wherever an event's thunks are unchanged. A row derives
|
||||
// a merged thunk from this, and that merge subscribes to each thunk it holds — rebuilding it
|
||||
// every time anything anywhere publishes churns the object its status components are watching.
|
||||
for (const [id, thunks] of byId) {
|
||||
const before = previous.get(id)
|
||||
|
||||
if (before?.length === thunks.length && before.every((thunk, i) => thunk === thunks[i])) {
|
||||
byId.set(id, before)
|
||||
}
|
||||
}
|
||||
|
||||
previous = byId
|
||||
|
||||
return byId
|
||||
},
|
||||
new Map<string, Thunk[]>(),
|
||||
)
|
||||
|
||||
// Shared, so a row with nothing in flight keeps the same value across every publish and doesn't
|
||||
// rebuild anything downstream
|
||||
export const noThunks: Thunk[] = []
|
||||
|
|
@ -8,8 +8,9 @@
|
|||
animation: spinner-spin 0.7s linear infinite;
|
||||
}
|
||||
|
||||
/* sizes are standalone (share the element with .spinner, not nested under it)
|
||||
— some call sites use spinner-xs on its own, e.g. ThunkPending */
|
||||
/* sizes share the element with .spinner rather than nesting under it, and only override its
|
||||
dimensions — the shape, colour and animation all come from .spinner, so a size on its own
|
||||
renders as a bordered box */
|
||||
.spinner-sm {
|
||||
@apply h-4 w-4 border-1;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue