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>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<ThunkStatusOrDeleted {event}>
|
<ThunkStatusOrDeleted {event} {context}>
|
||||||
<ReactionSummary
|
<ReactionSummary
|
||||||
{url}
|
{url}
|
||||||
{event}
|
{event}
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@
|
||||||
Posted in #<RoomName {h} {url} />
|
Posted in #<RoomName {h} {url} />
|
||||||
</Link>
|
</Link>
|
||||||
{/if}
|
{/if}
|
||||||
<ThunkStatusOrDeleted {event}>
|
<ThunkStatusOrDeleted {event} {context}>
|
||||||
<ReactionSummary
|
<ReactionSummary
|
||||||
{url}
|
{url}
|
||||||
{event}
|
{event}
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<ThunkStatusOrDeleted {event}>
|
<ThunkStatusOrDeleted {event} {context}>
|
||||||
{#snippet status()}
|
{#snippet status()}
|
||||||
<ClassifiedStatus {event} />
|
<ClassifiedStatus {event} />
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||||
<div class="flex grow flex-wrap justify-end gap-2">
|
<div class="flex grow flex-wrap justify-end gap-2">
|
||||||
<ThunkStatusOrDeleted {event}>
|
<ThunkStatusOrDeleted {event} {context}>
|
||||||
<ReactionSummary
|
<ReactionSummary
|
||||||
{url}
|
{url}
|
||||||
{event}
|
{event}
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@
|
||||||
Posted in #<RoomName {h} {url} />
|
Posted in #<RoomName {h} {url} />
|
||||||
</Link>
|
</Link>
|
||||||
{/if}
|
{/if}
|
||||||
<ThunkStatusOrDeleted {event}>
|
<ThunkStatusOrDeleted {event} {context}>
|
||||||
<ReactionSummary
|
<ReactionSummary
|
||||||
{url}
|
{url}
|
||||||
{event}
|
{event}
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@
|
||||||
Posted in #<RoomName {h} {url} />
|
Posted in #<RoomName {h} {url} />
|
||||||
</Link>
|
</Link>
|
||||||
{/if}
|
{/if}
|
||||||
<ThunkStatusOrDeleted {event}>
|
<ThunkStatusOrDeleted {event} {context}>
|
||||||
<ReactionSummary
|
<ReactionSummary
|
||||||
{url}
|
{url}
|
||||||
{event}
|
{event}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import cx from "classnames"
|
import cx from "classnames"
|
||||||
import {get, readable} from "svelte/store"
|
import {readable} from "svelte/store"
|
||||||
import {
|
import {
|
||||||
hash,
|
hash,
|
||||||
gte,
|
gte,
|
||||||
|
|
@ -39,6 +39,7 @@
|
||||||
import RoomItemMenuMobile from "@app/components/RoomItemMenuMobile.svelte"
|
import RoomItemMenuMobile from "@app/components/RoomItemMenuMobile.svelte"
|
||||||
import RoomItemContent from "@app/components/RoomItemContent.svelte"
|
import RoomItemContent from "@app/components/RoomItemContent.svelte"
|
||||||
import {profiles, thunks, user} from "@app/core"
|
import {profiles, thunks, user} from "@app/core"
|
||||||
|
import {noThunks, thunksByEventId} from "@app/thunks"
|
||||||
import {colors} from "@app/theme"
|
import {colors} from "@app/theme"
|
||||||
import {ENABLE_ZAPS} from "@app/env"
|
import {ENABLE_ZAPS} from "@app/env"
|
||||||
import type {FeedContext} from "@app/feeds"
|
import type {FeedContext} from "@app/feeds"
|
||||||
|
|
@ -69,7 +70,7 @@
|
||||||
const h = tagValue(tagSpec("h"), event.tags)
|
const h = tagValue(tagSpec("h"), event.tags)
|
||||||
const today = formatTimestampAsDate(now())
|
const today = formatTimestampAsDate(now())
|
||||||
const profileDisplay = $profiles.display(event.pubkey, [url]).$
|
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 [_, colorValue] = colors[hash(event.pubkey) % colors.length]
|
||||||
|
|
||||||
const qTag = matchTag(tagSpec("q"), event.tags)
|
const qTag = matchTag(tagSpec("q"), event.tags)
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@
|
||||||
Posted in #<RoomName {h} {url} />
|
Posted in #<RoomName {h} {url} />
|
||||||
</Link>
|
</Link>
|
||||||
{/if}
|
{/if}
|
||||||
<ThunkStatusOrDeleted {event}>
|
<ThunkStatusOrDeleted {event} {context}>
|
||||||
<ReactionSummary
|
<ReactionSummary
|
||||||
{url}
|
{url}
|
||||||
{event}
|
{event}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
|
|
||||||
<div class="flex w-full justify-end px-1 text-xs {restProps.class}">
|
<div class="flex w-full justify-end px-1 text-xs {restProps.class}">
|
||||||
<span class="flex items-center gap-1">
|
<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>
|
<span class="opacity-50">Sending...</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
|
||||||
|
|
@ -2,24 +2,22 @@
|
||||||
import type {Snippet} from "svelte"
|
import type {Snippet} from "svelte"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {PublishStatus} from "@welshman/net"
|
import {PublishStatus} from "@welshman/net"
|
||||||
import {deriveIsDeleted} from "@app/repository"
|
|
||||||
import ThunkStatus from "@app/components/ThunkStatus.svelte"
|
import ThunkStatus from "@app/components/ThunkStatus.svelte"
|
||||||
import {thunks} from "@app/core"
|
import {thunks} from "@app/core"
|
||||||
|
import type {FeedContext} from "@app/feeds"
|
||||||
|
import {noThunks, thunksByEventId} from "@app/thunks"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
event: TrustedEvent
|
event: TrustedEvent
|
||||||
|
context: FeedContext
|
||||||
status?: Snippet
|
status?: Snippet
|
||||||
children?: Snippet
|
children?: Snippet
|
||||||
}
|
}
|
||||||
|
|
||||||
const {event, status, children}: Props = $props()
|
const {event, context, status, children}: Props = $props()
|
||||||
|
const deleted = $derived(context.deleted(event))
|
||||||
// Editing a replaceable event hands this a different event
|
const pending = $derived($thunksByEventId.get(event.id) ?? noThunks)
|
||||||
const deleted = $derived(deriveIsDeleted(event))
|
const thunk = $derived($thunks.merge(pending))
|
||||||
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)))
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $deleted}
|
{#if $deleted}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import {
|
||||||
addressTags,
|
addressTags,
|
||||||
getAddress,
|
getAddress,
|
||||||
getCommentFiltersForRoot,
|
getCommentFiltersForRoot,
|
||||||
|
getIdOrAddress,
|
||||||
getReplyFilters,
|
getReplyFilters,
|
||||||
hexTags,
|
hexTags,
|
||||||
isReplaceableKind,
|
isReplaceableKind,
|
||||||
|
|
@ -48,6 +49,7 @@ export const makeFeedContext = ({relays}: {relays: string[] | Promise<string[]>}
|
||||||
const eventsByTarget = new Map<string, TrustedEvent[]>()
|
const eventsByTarget = new Map<string, TrustedEvent[]>()
|
||||||
const targetsByEventId = new Map<string, string[]>()
|
const targetsByEventId = new Map<string, string[]>()
|
||||||
const subscribersByTarget = new Map<string, Set<(events: TrustedEvent[]) => void>>()
|
const subscribersByTarget = new Map<string, Set<(events: TrustedEvent[]) => void>>()
|
||||||
|
const deletedChecks = new Map<string, Set<() => void>>()
|
||||||
|
|
||||||
const addEvent = (event: TrustedEvent, touched: Set<string>) => {
|
const addEvent = (event: TrustedEvent, touched: Set<string>) => {
|
||||||
// An event seen before its target was tracked stays unfiled, so that adding the target
|
// 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>) => {
|
const notify = (touched: Set<string>) => {
|
||||||
for (const target of touched) {
|
for (const target of touched) {
|
||||||
for (const subscriber of subscribersByTarget.get(target) || []) {
|
for (const subscriber of subscribersByTarget.get(target) || []) {
|
||||||
|
|
@ -146,6 +164,7 @@ export const makeFeedContext = ({relays}: {relays: string[] | Promise<string[]>}
|
||||||
}
|
}
|
||||||
|
|
||||||
notify(touched)
|
notify(touched)
|
||||||
|
notifyDeleted(added, removed)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -183,6 +202,29 @@ export const makeFeedContext = ({relays}: {relays: string[] | Promise<string[]>}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
add,
|
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[]> => {
|
related: (event: TrustedEvent): Readable<TrustedEvent[]> => {
|
||||||
add(event)
|
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;
|
animation: spinner-spin 0.7s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* sizes are standalone (share the element with .spinner, not nested under it)
|
/* sizes share the element with .spinner rather than nesting under it, and only override its
|
||||||
— some call sites use spinner-xs on its own, e.g. ThunkPending */
|
dimensions — the shape, colour and animation all come from .spinner, so a size on its own
|
||||||
|
renders as a bordered box */
|
||||||
.spinner-sm {
|
.spinner-sm {
|
||||||
@apply h-4 w-4 border-1;
|
@apply h-4 w-4 border-1;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue