Query events through welshman's Events plugin and delete the repository.ts wrappers around it

This commit is contained in:
Jon Staab 2026-09-25 16:11:59 -07:00
parent f10dc96c43
commit 964a1bb6fa
36 changed files with 123 additions and 193 deletions

View file

@ -324,7 +324,7 @@ card, and `NoteContentClassified` renders a listing wherever `NoteContent` is us
**Routes.** `src/routes/spaces/[relay]/classifieds/+page.svelte` loads listings and their comments
with `makeFeed` and filters them with the `classifieds.ts` helpers. `[address]/+page.svelte` reads
one listing with `deriveEvent(address, [url])`.
one listing with `$events.one(address, [url]).$`.
Articles are composed on a full page (`spaces/[relay]/articles/create`, built by
`makeArticleCreatePath`) instead of in a modal.

View file

@ -54,8 +54,8 @@ members and pins, the space member list, and roles. Anyone can publish events of
readers must check the author. The welshman collections do: `Rooms` and every
`RelaySignedDerivedPlugin` (`RelayMemberLists`, `RelayRoles`, `RoomPinLists`) drop events whose
author isn't the relay's `self`, and re-check when NIP-11 loads. For a relay-authored kind with no
plugin, filter `deriveEventsForUrl(url, filters)` on that `self` key yourself rather than reading a
bare `deriveEventsForUrl`.
plugin, read `$events.relaySignedForUrl(url, filters)`, which keeps only events signed by that
`self` key, rather than a bare `$events.forUrl`.
The app signs everything it publishes with the user's own key. Space-wide content with no author of
its own belongs to the space's owner, the pubkey NIP-11 names: featured content
@ -235,7 +235,7 @@ space it lists or used to list, so each relay hears about joins and leaves. Kind
so `RoomChat` and `publishRoomQuote` publish raw templates through `Thunks`.
Reads are scoped the same way. Request from the space relay (`relays: [url]`, plus `"#h": [h]` for
a room) and read results with `deriveEventsForUrl(url, filters)`, which uses the tracker to keep
a room) and read results with `$events.forUrl(url, filters)`, which uses the tracker to keep
only events seen on that relay. A repository-wide query would mix rooms that share an `h` across
relays. `src/app/sync.ts` pulls each space's room state, membership and recent content in the
background; see flotilla-state.

View file

@ -36,7 +36,7 @@ The app is therefore stable for as long as anything under the login gate is moun
| `session` | the persisted `Session` | `undefined` |
| `user` | `User.require($app)`, derived | subscribing or `.get()` throws |
| `usePlugin(Plugin)` | a store holding `$app.use(Plugin)` for the current app | safe |
| `profiles`, `rooms`, `relays`, `thunks`, … | `usePlugin` for 27 welshman plugins | safe |
| `profiles`, `rooms`, `relays`, `events`, `thunks`, … | `usePlugin` for 28 welshman plugins | safe |
| `fromApp(read)` | a store that re-reads `read($app)` when the app changes | safe |
| `deriveUserItem(Plugin)` | the signed-in user's entry in a keyed plugin | `undefined` |
| `userSearchRelayUrls` | the user's search relays, or `DEFAULT_SEARCH_RELAYS` | the defaults |
@ -175,16 +175,19 @@ Events enter `app.repository` from `ingestPolicy` (which calls `tracker.track`,
optimistically. The tracker records which relays each event was seen on, which is what lets
space content be keyed by relay.
`src/app/repository.ts` wraps the `@welshman/store` derivations in `fromApp`:
Raw event queries go through welshman's `Events` plugin, exported from `core.ts` as `events`.
Every method returns a projection over the current app's repository:
- `deriveEvent`, `deriveEvents`, `deriveEventsById`, `deriveIsDeleted`
- relay-scoped: `deriveEventsForUrl`, `deriveEventsByIdForUrl`, `deriveEventsByIdByUrl`,
`getEventsForUrl`
- `deriveLatestEvent`
- `one(idOrAddress, relays)` (loads when nothing local matches, keeps deleted events), `byId`,
`all`, `asc`, `desc`, `isDeleted`
- relay-scoped through the tracker: `forUrl`, `byIdForUrl`, `byIdByUrl`, and
`relaySignedForUrl`, which keeps only events signed by the relay's NIP-11 `self`
Use these for raw event queries. Welshman's `Events` plugin has the same surface returning
projections, and flotilla does not use it. Plugin reads (`get`, `one`, `load`, `index`) are
documented in welshman-app.
Components bind `$events.forUrl(url, filters).$`. A `derive*` function called from gated code
binds `events.get().forUrl(...).$` at call time, and a module-scope store wraps it in
`fromApp($app => $app.use(Events).byIdByUrl(filters).$)`, as `latestActivityByPath` does.
`src/app/repository.ts` holds only `deriveLatestEvent`. Plugin reads (`get`, `one`, `load`,
`index`) are documented in welshman-app.
### Free functions in an app module
@ -196,7 +199,7 @@ projections and repository derivations:
export const deriveSpaceActionItems = (url: string) =>
derived(
[
deriveEventsForUrl(url, [{kinds: [REPORT]}]),
events.get().forUrl(url, [{kinds: [REPORT]}]).$,
rooms.get().pendingJoins(url).$,
deriveSpacePermissions(url),
],
@ -424,7 +427,7 @@ store, consumed in components with `$store`.
Take the first answer that fits:
1. **It is an event, or derived from events.** It is already in the repository, or should be.
Read it with a plugin or an `@app/repository` derivation, and put the domain logic in a
Read it with a plugin or the `events` projections, and put the domain logic in a
`derive*` function in the owning app module (`deriveUserRooms`). Don't copy it into a
writable.
2. **It is a keyed collection of one kind, loaded by key.** Write a plugin. A generic kind goes

View file

@ -254,7 +254,7 @@ stores, so a component takes the key and looks up the rest: `RoomName` takes `{u
`$rooms.forRoom(url, h)`, `ProfileName` takes `pubkey` and reads `$profiles.display`.
Events are the exception, and 66 components take `event: TrustedEvent`, since the parent already
holds the event from a feed or a `deriveEvent`. An event prop usually travels with `url`, the
holds the event from a feed or `$events.one`. An event prop usually travels with `url`, the
relay it lives on, which routes its replies and reactions, and with `context: FeedContext`, the
shared loader below. A component takes a pointer instead only when it has to load the event
itself, as `ContentQuote` does. Anything with neither a store nor an event is passed as its
@ -281,7 +281,7 @@ param change and lists key by id. A prop that does change while mounted needs `$
## Reading state in a component
Components read plugins through the `usePlugin` stores exported from `src/app/core.ts`
(`$profiles`, `$relays`, `$rooms`, `$roomLists`, `$network`, `$thunks`, `$deletes`,
(`$profiles`, `$relays`, `$rooms`, `$roomLists`, `$events`, `$network`, `$thunks`, `$deletes`,
`$relayManagement`, …). `$app.use(X)` covers plugins with no export, such as `Zappers`,
`Pinboards` and `Feeds`; flotilla-state has the rules about which to use where.
@ -291,6 +291,7 @@ How you bind depends on what the method returns:
<script lang="ts">
const room = $rooms.forRoom(url, h) // Readable: one, forRoom
const display = $profiles.display(pubkey, removeUndefined([url])).$ // Projection: take .$
const replies = $events.asc(filters).$ // raw repository queries
const shouldProtect = $relays.hasNip(url, 70) // Promise: load, hasNip
</script>
@ -298,7 +299,7 @@ How you bind depends on what the method returns:
```
In a handler, call `.get()` on a projection instead of subscribing. The app modules add `derive*`
factories over the same data: `deriveEvent` and `deriveEventsById` in `src/app/repository.ts`,
factories over the same data: `deriveSpaceActionItems` in `src/app/actionItems.ts`,
`deriveSpaceSupportedMethods` in `src/app/management.ts`, `deriveRelayAuthError` in
`src/app/access.ts`. Call them at the top of the script with fixed arguments, and wrap one in
`$derived` only when its arguments change, as the thread page does for filters that wait on the
@ -316,7 +317,7 @@ data every page needs lives in `src/app/sync.ts`.
`people/[npub]/+page.svelte` does for the profile, relay list, follow, pin, room and messaging
lists before loading the author's outbox with `$network.loadLenient`.
**A detail page:** `deriveEvent(address, [url])` loads when nothing local matches. While it is
**A detail page:** `$events.one(address, [url]).$` loads when nothing local matches. While it is
empty, seven pages show a spinner that turns into a failure message:
```svelte
@ -328,7 +329,7 @@ empty, seven pages show a spinner that turns into a failure message:
```
**Related events:** request them, abort on teardown, and read them back from the repository with
`deriveEventsAsc(deriveEventsById(filters))`.
`$events.asc(filters).$`.
```typescript
onMount(() => {
@ -460,8 +461,8 @@ Rich text uses `makeEditor` from `src/app/editor` with `EditorContent`.
- `flotilla-architecture`: layer rules and exceptions, the `src/app` modules, boot, placement
- `flotilla-state`: plugin stores, `derive*` stores, drafts, settings, thunks and commands
- `flotilla-model`: spaces, rooms, NIP-43/29/86, content kinds, and where events are published
- `welshman-app`: plugins, projections, `Command`, thunks, `Feeds`
- `welshman-store`: `deriveEventsById`, `deriveEventsAsc`, the other repository stores
- `welshman-app`: plugins, projections, `Command`, thunks, `Events`, `Feeds`
- `welshman-store`: `deriveEventsById`, `deriveEventsAsc`, the repository stores under `Events`
- `welshman-domain`: the readers and writers behind `reader` and `writer`
- `welshman-feeds`: `FeedController`, still used by `ProfileFeed`
- `welshman-editor`: the composer behind `makeEditor`

View file

@ -1,8 +1,7 @@
import {derived} from "svelte/store"
import {REPORT, sortEventsDesc} from "@welshman/util"
import {rooms} from "@app/core"
import {events, rooms} from "@app/core"
import {deriveSpacePermissions} from "@app/management"
import {deriveEventsForUrl} from "@app/repository"
// Action items (admin review queue)
@ -10,7 +9,7 @@ import {deriveEventsForUrl} from "@app/repository"
export const deriveSpaceActionItems = (url: string) =>
derived(
[
deriveEventsForUrl(url, [{kinds: [REPORT]}]),
events.get().forUrl(url, [{kinds: [REPORT]}]).$,
rooms.get().pendingJoins(url).$,
deriveSpacePermissions(url),
],

View file

@ -14,8 +14,7 @@ import type {TrustedEvent} from "@welshman/util"
import {CalendarRsvp, TimeEvent} from "@welshman/domain"
import type {RsvpStatus} from "@welshman/domain"
import {synced} from "@welshman/store"
import {deletes, reader, relays, thunks, writer} from "@app/core"
import {deriveEvents} from "@app/repository"
import {deletes, events, reader, relays, thunks, writer} from "@app/core"
import {toSpace} from "@app/rooms"
import {kv} from "@app/storage"
@ -237,7 +236,7 @@ export const makeRsvpFilter = (event: TrustedEvent) => ({
"#a": [getAddress(event)],
})
export const deriveRsvps = (event: TrustedEvent) => deriveEvents([makeRsvpFilter(event)])
export const deriveRsvps = (event: TrustedEvent) => events.get().desc([makeRsvpFilter(event)]).$
export const isAttending = (status?: RsvpStatus) => status === "accepted" || status === "tentative"

View file

@ -5,8 +5,7 @@ import {deriveDeduplicated} from "@welshman/store"
import {makeRoomKey} from "@welshman/app"
import type {Room} from "@welshman/app"
import type {Room as LiveKitRoom} from "livekit-client"
import {network} from "@app/core"
import {deriveEventsForUrl} from "@app/repository"
import {events, network} from "@app/core"
export const LIVEKIT_PARTICIPANTS = 39004
@ -304,7 +303,7 @@ export const deriveCallParticipants = (url: string, h: string) =>
participantMediaState,
callTargetRoom,
deriveDeduplicated(
deriveEventsForUrl(url, [{kinds: [LIVEKIT_PARTICIPANTS], "#d": [h]}]),
events.get().forUrl(url, [{kinds: [LIVEKIT_PARTICIPANTS], "#d": [h]}]).$,
events => first(sortEventsDesc(events)),
),
],

View file

@ -11,8 +11,7 @@
import EventActivity from "@app/components/EventActivity.svelte"
import EventActions from "@app/components/EventActions.svelte"
import EventActionBar from "@app/components/EventActionBar.svelte"
import {reader} from "@app/core"
import {deriveIsDeleted} from "@app/repository"
import {events, reader} from "@app/core"
import {makeSpacePath} from "@app/routes"
type Props = {
@ -30,7 +29,7 @@
const article = $derived(reader(Article)(event))
const h = $derived(article.room())
const topics = $derived(article.topics())
const deleted = $derived(deriveIsDeleted(event))
const deleted = $derived($events.isDeleted(event).$)
const deleteReaction = (reaction: TrustedEvent) => retractReaction(reaction, {url, h})

View file

@ -9,8 +9,7 @@
import Profile from "@app/components/Profile.svelte"
import ProfileName from "@app/components/ProfileName.svelte"
import ProfileAbout from "@app/components/ProfileAbout.svelte"
import {network, reader} from "@app/core"
import {deriveEventsForUrl} from "@app/repository"
import {events, network, reader} from "@app/core"
import {displayReadingTime} from "@app/articles"
import {makeArticlePath} from "@app/routes"
@ -24,15 +23,15 @@
const MAX_ARTICLES = 6
const filters = [{kinds: [LONG_FORM], authors: [event.pubkey], limit: 20}]
const events = deriveEventsForUrl(url, filters)
const articles = $events.forUrl(url, filters).$
const address = getAddress(event)
const articleCount = $derived($events.length)
const articleCount = $derived($articles.length)
const others = $derived(
sortBy(
e => -reader(Article)(e).publishedAt(),
$events.filter(e => getAddress(e) !== address),
$articles.filter(e => getAddress(e) !== address),
).slice(0, MAX_ARTICLES),
)

View file

@ -17,8 +17,7 @@
import NoteCard from "@app/components/NoteCard.svelte"
import NoteContent from "@app/components/NoteContent.svelte"
import NoteContentMinimal from "@app/components/NoteContentMinimal.svelte"
import {network, router} from "@app/core"
import {deriveEvent} from "@app/repository"
import {events, network, router} from "@app/core"
import {entityLink} from "@app/env"
import {goToEvent} from "@app/routes"
@ -39,7 +38,7 @@
const mergedRelays = writable(hints)
const quote = deriveEvent(idOrAddress, hints)
const quote = $events.one(idOrAddress, hints).$
// Whoever quoted the event must have seen it, so the quoting event's relays are asked too.
$router.resolver

View file

@ -3,15 +3,13 @@
import {removeUndefined} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util"
import {getCommentFiltersForRoot} from "@welshman/util"
import {deriveEventsAsc} from "@welshman/store"
import Reply from "@assets/icons/reply-2.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import CommentTree from "@app/components/CommentTree.svelte"
import type {FeedContext} from "@app/feeds"
import CommentCompose from "@app/components/CommentCompose.svelte"
import {network} from "@app/core"
import {deriveEventsById} from "@app/repository"
import {events, network} from "@app/core"
import {buildCommentTree} from "@app/social"
type Props = {
@ -24,7 +22,7 @@
const relays = removeUndefined([url])
const filters = getCommentFiltersForRoot([event])
const comments = deriveEventsAsc(deriveEventsById(filters))
const comments = $events.asc(filters).$
const nodes = $derived(buildCommentTree(event, $comments))

View file

@ -3,12 +3,11 @@
import {POLL_RESPONSE} from "@welshman/util"
import {Poll} from "@welshman/domain"
import ContentMinimal from "@app/components/ContentMinimal.svelte"
import {reader} from "@app/core"
import {deriveEvents} from "@app/repository"
import {events, reader} from "@app/core"
const props: ComponentProps<typeof ContentMinimal> = $props()
const responses = deriveEvents([{kinds: [POLL_RESPONSE], "#e": [props.event.id]}])
const responses = $events.desc([{kinds: [POLL_RESPONSE], "#e": [props.event.id]}]).$
const voters = $derived(reader(Poll)(props.event).results($responses).voters)
</script>

View file

@ -2,8 +2,8 @@
import {onDestroy} from "svelte"
import Spinner from "@lib/components/Spinner.svelte"
import NoteItem from "@app/components/NoteItem.svelte"
import {events} from "@app/core"
import {makeFeedContext} from "@app/feeds"
import {deriveEvent} from "@app/repository"
type Props = {
url?: string
@ -13,7 +13,7 @@
const {url, value, relays}: Props = $props()
const event = deriveEvent(value, relays)
const event = $events.one(value, relays).$
const context = makeFeedContext({relays})
onDestroy(context.cleanup)

View file

@ -15,9 +15,8 @@
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
import RelayName from "@app/components/RelayName.svelte"
import PinAdd from "@app/components/PinAdd.svelte"
import {reader} from "@app/core"
import {events, reader} from "@app/core"
import {eventToReference} from "@app/pinboards"
import {deriveEventsForUrl} from "@app/repository"
import {makeSpacePath} from "@app/routes"
import {pushModal} from "@app/modal"
@ -28,8 +27,8 @@
const {url, event}: Props = $props()
const boards = derived(deriveEventsForUrl(url, [{kinds: [PINBOARD]}]), $events =>
$events.map(reader(Pinboard)),
const boards = derived($events.forUrl(url, [{kinds: [PINBOARD]}]).$, $pinboards =>
$pinboards.map(reader(Pinboard)),
)
const reference = eventToReference(event)

View file

@ -6,8 +6,7 @@
import {Poll, PollResponse} from "@welshman/domain"
import type {Thunk} from "@welshman/app"
import PollOption from "@app/components/PollOption.svelte"
import {command, reader, thunks, user, writer} from "@app/core"
import {deriveEvents} from "@app/repository"
import {command, events, reader, thunks, user, writer} from "@app/core"
type Props = {
url: string
@ -16,7 +15,7 @@
const {url, event}: Props = $props()
const responses = deriveEvents([{kinds: [POLL_RESPONSE], "#e": [event.id]}])
const responses = $events.desc([{kinds: [POLL_RESPONSE], "#e": [event.id]}]).$
const getOwnResponse = (responses: TrustedEvent[]) => {
let latest: TrustedEvent | undefined

View file

@ -8,8 +8,8 @@
import ModalBody from "@lib/components/ModalBody.svelte"
import Button from "@lib/components/Button.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
import {deriveEventsById} from "@app/repository"
import ReportItem from "@app/components/ReportItem.svelte"
import {events} from "@app/core"
type Props = {
url: string
@ -18,7 +18,7 @@
const {url, event}: Props = $props()
const reports = deriveEventsById([{kinds: [REPORT], "#e": [event.id]}])
const reports = $events.byId([{kinds: [REPORT], "#e": [event.id]}]).$
const back = () => history.back()

View file

@ -32,12 +32,11 @@
import RoomItemMenuButton from "@app/components/RoomItemMenuButton.svelte"
import RoomItemMenuMobile from "@app/components/RoomItemMenuMobile.svelte"
import RoomItemContent from "@app/components/RoomItemContent.svelte"
import {profiles, thunks, user} from "@app/core"
import {events, profiles, thunks, user} from "@app/core"
import {noThunks, thunksByEventId} from "@app/thunks"
import {colorFor} from "@app/theme"
import {ENABLE_ZAPS} from "@app/env"
import type {FeedContext} from "@app/feeds"
import {deriveEvent, deriveEventsForUrl} from "@app/repository"
import {highlightedEvent, makeContentPath, makeThreadPath} from "@app/routes"
import {pushModal} from "@app/modal"
@ -70,16 +69,16 @@
// A message that is nothing but a reference renders as the event it quotes.
const isQuoteOnly = event.content.trim().split(/\s/).length === 1
const quote = isQuoteOnly ? getContentQuote(event.content) : undefined
const innerEvent = quote ? deriveEvent(quote.value, [url]) : readable(undefined)
const innerEvent = quote ? $events.one(quote.value, [url]).$ : readable(undefined)
const innerComments = $derived(
$innerEvent ? deriveEventsForUrl(url, getCommentFiltersForParent([$innerEvent])) : readable([]),
$innerEvent ? $events.forUrl(url, getCommentFiltersForParent([$innerEvent])).$ : readable([]),
)
const path = $derived(
$innerEvent && makeContentPath(url, $innerEvent.kind, getIdOrAddress($innerEvent)),
)
const threads = deriveEventsForUrl(url, [{kinds: [THREAD], "#q": [event.id]}])
const threads = $events.forUrl(url, [{kinds: [THREAD], "#q": [event.id]}]).$
const commenterDisplays = $derived(
deriveDisplaysByPubkey(uniq($innerComments.map(e => e.pubkey)), url),

View file

@ -14,8 +14,7 @@
import PeopleItem from "@app/components/PeopleItem.svelte"
import NoteCard from "@app/components/NoteCard.svelte"
import NoteContentMinimal from "@app/components/NoteContentMinimal.svelte"
import {getEventsForUrl} from "@app/repository"
import {network, profiles} from "@app/core"
import {events, network, profiles} from "@app/core"
import {popModal} from "@app/modal"
import {pushToast} from "@app/toast"
import {goToEvent} from "@app/routes"
@ -52,7 +51,7 @@
results = sortEventsDesc(
uniqBy(
(e: TrustedEvent) => e.id,
relays.flatMap(relay => Array.from(getEventsForUrl(relay, filters))),
relays.flatMap(relay => events.get().forUrl(relay, filters).get()),
),
)

View file

@ -6,8 +6,7 @@
import Icon from "@lib/components/Icon.svelte"
import Link from "@lib/components/Link.svelte"
import Profile from "@app/components/Profile.svelte"
import {relayMemberLists, relays} from "@app/core"
import {deriveEventsForUrl} from "@app/repository"
import {events, relayMemberLists, relays} from "@app/core"
import {makeSpacePath} from "@app/routes"
type Props = {
@ -18,7 +17,7 @@
const relay = $relays.one(url)
const members = $relayMemberLists.forUrl(url)
const memberEvents = deriveEventsForUrl(url, [{kinds: [RELAY_ADD_MEMBER, RELAY_JOIN]}])
const memberEvents = $events.forUrl(url, [{kinds: [RELAY_ADD_MEMBER, RELAY_JOIN]}]).$
const admins = $derived($relay?.pubkey ? [$relay.pubkey] : [])

View file

@ -17,10 +17,9 @@
import Icon from "@lib/components/Icon.svelte"
import SecondaryNavItem from "@lib/components/SecondaryNavItem.svelte"
import SpaceSearch from "@app/components/SpaceSearch.svelte"
import {relays} from "@app/core"
import {events, relays} from "@app/core"
import {ENABLE_ZAPS} from "@app/env"
import {CONTENT_KINDS} from "@app/content"
import {deriveEventsForUrl} from "@app/repository"
import {makeSpacePath} from "@app/routes"
import {allNotifications, notifications} from "@app/notifications"
import {pushModal} from "@app/modal"
@ -46,8 +45,8 @@
const cachedKinds: number[] = getJson(spaceKindsKey) ?? []
const spaceKinds = deriveDeduplicatedByValue(
deriveEventsForUrl(url, [{kinds: CONTENT_KINDS}]),
$events => new Set([...cachedKinds, ...$events.map(e => e.kind)]),
$events.forUrl(url, [{kinds: CONTENT_KINDS}]).$,
$content => new Set([...cachedKinds, ...$content.map(e => e.kind)]),
)
// A comment can name content this space doesn't have, and it counts toward the space either way.

View file

@ -11,7 +11,7 @@
import ModalFooter from "@lib/components/ModalFooter.svelte"
import ReportItem from "@app/components/ReportItem.svelte"
import RelayName from "@app/components/RelayName.svelte"
import {deriveEventsForUrl} from "@app/repository"
import {events} from "@app/core"
interface Props {
url: string
@ -19,7 +19,7 @@
const {url}: Props = $props()
const reports = deriveEventsForUrl(url, [{kinds: [REPORT]}])
const reports = $events.forUrl(url, [{kinds: [REPORT]}]).$
const back = () => history.back()

View file

@ -10,6 +10,7 @@ import {
BlossomServerLists,
Deletes,
Domain,
Events,
FollowLists,
Handles,
Logger,
@ -130,6 +131,7 @@ export const blockedRelayLists = usePlugin(BlockedRelayLists)
export const blossomServerLists = usePlugin(BlossomServerLists)
export const deletes = usePlugin(Deletes)
export const domain = usePlugin(Domain)
export const events = usePlugin(Events)
export const followLists = usePlugin(FollowLists)
export const handles = usePlugin(Handles)
export const logger = usePlugin(Logger)

View file

@ -2,8 +2,7 @@ import {derived} from "svelte/store"
import {APP_DATA, sortEventsDesc, tagSpec, tagValues} from "@welshman/util"
import {AppData} from "@welshman/domain"
import {Relays, publish} from "@welshman/app"
import {command, fromApp, writer} from "@app/core"
import {deriveEventsForUrl} from "@app/repository"
import {command, events, fromApp, writer} from "@app/core"
import {toSpace} from "@app/rooms"
// NIP-78 app data from the pubkey the space's NIP-11 names, one ["content", <value>] tag per entry.
@ -13,7 +12,7 @@ export const deriveFeaturedContent = (url: string) =>
derived(
[
fromApp($app => $app.use(Relays).one(url)),
deriveEventsForUrl(url, [{kinds: [APP_DATA], "#d": [FEATURED_CONTENT_D]}]),
events.get().forUrl(url, [{kinds: [APP_DATA], "#d": [FEATURED_CONTENT_D]}]).$,
],
([$relay, $events]) => {
const [event] = sortEventsDesc($events.filter(e => e.pubkey === $relay?.pubkey))

View file

@ -30,6 +30,7 @@ import {
tagValues,
} from "@welshman/util"
import {Comment, TimeEvent} from "@welshman/domain"
import {Events} from "@welshman/app"
import type {Maybe} from "@welshman/lib"
import type {Filter, TrustedEvent} from "@welshman/util"
import {mergeRepositoryUpdates} from "@welshman/net"
@ -39,7 +40,6 @@ import type {ScrollerOpts} from "@lib/html"
import {daysBetween} from "@lib/util"
import {EVENT_CONTEXT_KINDS, REACTION_KINDS} from "@app/content"
import {app, network, reader} from "@app/core"
import {getEventsForUrl} from "@app/repository"
const noEvents: TrustedEvent[] = []
@ -661,7 +661,7 @@ export const makeFeed = ({
// What the repository already holds is an answer too, so a page of it renders before any relay replies.
const stored = relays.map(url =>
Array.from(getEventsForUrl(url, filters)).sort(compareEventsAsc).slice(-PAGE_SIZE),
app.get().use(Events).forUrl(url, filters).get().sort(compareEventsAsc).slice(-PAGE_SIZE),
)
if (stored.every(page => page.length > 0)) {
@ -712,7 +712,7 @@ export const makeCalendarFeed = ({
const events = writable(
uniqBy(
e => e.id,
relays.flatMap(url => Array.from(getEventsForUrl(url, filters))),
relays.flatMap(url => app.get().use(Events).forUrl(url, filters).get()),
).sort(compareByStart),
)

View file

@ -3,8 +3,7 @@ import {ago, MINUTE, now, simpleCache} from "@welshman/lib"
import {ROOM_CREATE_PERMISSION, hexTags, tagValues} from "@welshman/util"
import {RelayManagement, Relays} from "@welshman/app"
import type {IApp} from "@welshman/app"
import {fromApp, usePlugin, user} from "@app/core"
import {deriveEventsForUrl} from "@app/repository"
import {events, fromApp, usePlugin, user} from "@app/core"
// `supportedmethods` answers for the pubkey that signed the request, so this catalog is the client's.
export const MANAGEMENT_METHOD_GROUPS = [
@ -221,7 +220,7 @@ export const deriveUserCanCreateRoom = (url: string) =>
derived(
[
user,
deriveEventsForUrl(url, [{kinds: [ROOM_CREATE_PERMISSION]}]),
events.get().forUrl(url, [{kinds: [ROOM_CREATE_PERMISSION]}]).$,
deriveUserIsSpaceStaff(url),
],
([$user, $events, $isStaff]) =>

View file

@ -15,8 +15,7 @@ import {
} from "@welshman/util"
import {Comment} from "@welshman/domain"
import {synced, throttled, withGetter} from "@welshman/store"
import {Relays, RoomLists} from "@welshman/app"
import {deriveEventsByIdByUrl} from "@app/repository"
import {Events, Relays, RoomLists} from "@welshman/app"
import {app, fromApp, reader} from "@app/core"
import {makeRoomPath, makeSpaceChatPath, makeChatPath, makeContentPath} from "@app/routes"
import {CONTENT_KINDS, makeCommentFilter} from "@app/content"
@ -236,10 +235,13 @@ export const latestActivityByPath = derived(
chatsById,
fromApp($app => $app.use(Relays).index.$),
fromApp($app => $app.use(RoomLists).index.$),
deriveEventsByIdByUrl([
{kinds: [MESSAGE, ...CONTENT_KINDS]},
makeCommentFilter(CONTENT_KINDS),
]),
fromApp(
$app =>
$app
.use(Events)
.byIdByUrl([{kinds: [MESSAGE, ...CONTENT_KINDS]}, makeCommentFilter(CONTENT_KINDS)])
.$,
),
userSettingsValues,
],
identity,

View file

@ -2,8 +2,7 @@ import {derived} from "svelte/store"
import type {Readable} from "svelte/store"
import {getIdFilters, outbox} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util"
import {network, pinLists, router} from "@app/core"
import {deriveEvents} from "@app/repository"
import {events, network, pinLists, router} from "@app/core"
export const derivePinnedEvents = (pubkey: string): Readable<TrustedEvent[]> =>
derived(
@ -22,7 +21,7 @@ export const derivePinnedEvents = (pubkey: string): Readable<TrustedEvent[]> =>
network.get().loadLenient({relays: $relays, filters, signal: controller.signal}),
)
const unsubscribe = deriveEvents(filters).subscribe(set)
const unsubscribe = events.get().desc(filters).$.subscribe(set)
return () => {
controller.abort()

View file

@ -3,61 +3,8 @@ import type {Unsubscriber} from "svelte/store"
import {first, on} from "@welshman/lib"
import type {Maybe} from "@welshman/lib"
import {sortEventsDesc} from "@welshman/util"
import type {Filter, TrustedEvent} from "@welshman/util"
import * as store from "@welshman/store"
import {Network} from "@welshman/app"
import {app, fromApp} from "@app/core"
// Events
export const deriveEvent = (idOrAddress: string, relays: string[] = []) =>
fromApp($app =>
store.makeDeriveEvent({
repository: $app.repository,
includeDeleted: true,
onDerive: (filters: Filter[], hints: string[]) =>
$app.use(Network).loadLenient({filters, relays: hints}),
})(idOrAddress, relays),
)
export const deriveEventsById = (filters: Filter[] = [{}]) =>
fromApp($app => store.deriveEventsById({repository: $app.repository, filters}))
export const deriveEvents = (filters: Filter[] = [{}]) =>
store.deriveEventsDesc(deriveEventsById(filters))
export const deriveIsDeleted = (event: TrustedEvent) =>
fromApp($app => store.deriveIsDeleted($app.repository, event))
// Events on a relay
export const getEventsForUrl = (url: string, filters: Filter[] = [{}]) =>
store
.getEventsByIdForUrl({
url,
filters,
tracker: app.get().tracker,
repository: app.get().repository,
})
.values()
export const deriveEventsByIdForUrl = (url: string, filters: Filter[] = [{}]) =>
fromApp($app =>
store.deriveEventsByIdForUrl({
url,
filters,
tracker: $app.tracker,
repository: $app.repository,
}),
)
export const deriveEventsForUrl = (url: string, filters: Filter[] = [{}]) =>
store.deriveArray(deriveEventsByIdForUrl(url, filters))
export const deriveEventsByIdByUrl = (filters: Filter[] = [{}]) =>
fromApp($app =>
store.deriveEventsByIdByUrl({filters, tracker: $app.tracker, repository: $app.repository}),
)
import type {TrustedEvent} from "@welshman/util"
import {app} from "@app/core"
// The most recent event held from one author.

View file

@ -2,8 +2,7 @@ import {derived, readable} from "svelte/store"
import type {Readable} from "svelte/store"
import {getIdFilters, getIdOrAddress} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util"
import {network, roomPinLists} from "@app/core"
import {deriveEventsForUrl} from "@app/repository"
import {events, network, roomPinLists} from "@app/core"
export const deriveRoomPinnedEvents = (url: string, h: string): Readable<TrustedEvent[]> =>
readable<TrustedEvent[]>([], set => {
@ -24,22 +23,25 @@ export const deriveRoomPinnedEvents = (url: string, h: string): Readable<Trusted
network.get().loadLenient({relays: [url], filters, signal: controller.signal})
return deriveEventsForUrl(url, filters).subscribe($events => {
const byPin = new Map(
$events.flatMap(event => [
[event.id, event],
[getIdOrAddress(event), event],
]),
)
return events
.get()
.forUrl(url, filters)
.$.subscribe($events => {
const byPin = new Map(
$events.flatMap(event => [
[event.id, event],
[getIdOrAddress(event), event],
]),
)
setEvents(
$pins.flatMap(pin => {
const event = byPin.get(pin)
setEvents(
$pins.flatMap(pin => {
const event = byPin.get(pin)
return event ? [event] : []
}),
)
})
return event ? [event] : []
}),
)
})
},
).subscribe(set)

View file

@ -16,8 +16,7 @@
import EventComments from "@app/components/EventComments.svelte"
import EventMenu from "@app/components/EventMenu.svelte"
import ReadingProgress from "@app/components/ReadingProgress.svelte"
import {reader} from "@app/core"
import {deriveEvent} from "@app/repository"
import {events, reader} from "@app/core"
import {makeFeedContext} from "@app/feeds"
import {decodeRelay} from "@app/relays"
import {displayReadingTime} from "@app/articles"
@ -29,7 +28,7 @@
const {relay, address} = params
const url = decodeRelay(relay)
const context = makeFeedContext({relays: [url]})
const event = deriveEvent(address, [url])
const event = $events.one(address, [url]).$
const article = derived(event, $event => ($event ? reader(Article)($event) : undefined))
const h = $derived($article?.room())
const topics = $derived(uniq($article?.topics() ?? []))

View file

@ -23,8 +23,7 @@
import CalendarEventPeople from "@app/components/CalendarEventPeople.svelte"
import CalendarRsvp from "@app/components/CalendarRsvp.svelte"
import {deriveRsvps, getRsvpsByStatus, makeRsvpFilter} from "@app/calendar"
import {network, reader} from "@app/core"
import {deriveEvent, deriveEvents} from "@app/repository"
import {events, network, reader} from "@app/core"
import {makeFeedContext} from "@app/feeds"
import {decodeRelay} from "@app/relays"
import type {PageProps} from "./$types"
@ -34,10 +33,10 @@
const {relay, address} = params
const url = decodeRelay(relay)
const context = makeFeedContext({relays: [url]})
const event = deriveEvent(address, [url])
const event = $events.one(address, [url]).$
const timeEvent = derived(event, $event => ($event ? reader(TimeEvent)($event) : undefined))
const filters = $derived($event ? getCommentFiltersForRoot([$event]) : [])
const replies = $derived(deriveEvents(filters))
const replies = $derived($events.desc(filters).$)
const rsvps = $derived($event ? deriveRsvps($event) : readable<TrustedEvent[]>([]))
const people = $derived(getRsvpsByStatus($rsvps))

View file

@ -10,8 +10,7 @@
import NoteCard from "@app/components/NoteCard.svelte"
import ClassifiedActions from "@app/components/ClassifiedActions.svelte"
import EventComments from "@app/components/EventComments.svelte"
import {reader} from "@app/core"
import {deriveEvent} from "@app/repository"
import {events, reader} from "@app/core"
import {makeFeedContext} from "@app/feeds"
import {decodeRelay} from "@app/relays"
import type {PageProps} from "./$types"
@ -21,7 +20,7 @@
const {relay, address} = params
const url = decodeRelay(relay)
const context = makeFeedContext({relays: [url]})
const event = deriveEvent(address, [url])
const event = $events.one(address, [url]).$
const classified = derived(event, $event => ($event ? reader(Classified)($event) : undefined))
const back = () => history.back()

View file

@ -18,10 +18,9 @@
import GoalStatus from "@app/components/GoalStatus.svelte"
import GoalSupporters from "@app/components/GoalSupporters.svelte"
import EventComments from "@app/components/EventComments.svelte"
import {reader} from "@app/core"
import {events, reader} from "@app/core"
import {ENABLE_ZAPS} from "@app/env"
import {deriveGoalProgress} from "@app/goals"
import {deriveEvent} from "@app/repository"
import {makeFeedContext} from "@app/feeds"
import {decodeRelay} from "@app/relays"
import type {PageProps} from "./$types"
@ -31,7 +30,7 @@
const {relay, id} = params
const url = decodeRelay(relay)
const context = makeFeedContext({relays: [url]})
const event = deriveEvent(id, [url])
const event = $events.one(id, [url]).$
const goal = derived(event, $event => ($event ? reader(ZapGoal)($event) : undefined))
const progress = $derived($event ? deriveGoalProgress($event, url) : undefined)
const remaining = $derived($progress ? Math.max(0, $progress.target - $progress.raised) : 0)

View file

@ -21,9 +21,8 @@
import PinAdd from "@app/components/PinAdd.svelte"
import PinboardItem from "@app/components/PinboardItem.svelte"
import PinboardEdit from "@app/components/PinboardEdit.svelte"
import {app, network, reader} from "@app/core"
import {app, events, network, reader} from "@app/core"
import {decodeRelay} from "@app/relays"
import {deriveEventsForUrl} from "@app/repository"
import {makeLibraryPath} from "@app/routes"
import {pushModal} from "@app/modal"
import type {PageProps} from "./$types"
@ -36,8 +35,8 @@
let loading = $state(true)
// A shelf belongs to the space it was published to rather than to the pubkey that signed it.
const boards = derived(deriveEventsForUrl(url, [{kinds: [PINBOARD]}]), $events =>
$events.map(reader(Pinboard)),
const boards = derived($events.forUrl(url, [{kinds: [PINBOARD]}]).$, $pinboards =>
$pinboards.map(reader(Pinboard)),
)
const address = $derived($page.url.searchParams.get("board") ?? "")

View file

@ -2,7 +2,6 @@
import {onDestroy, onMount} from "svelte"
import {sleep} from "@welshman/lib"
import {POLL, POLL_RESPONSE, getCommentFiltersForRoot} from "@welshman/util"
import {deriveEventsAsc} from "@welshman/store"
import SortVertical from "@assets/icons/sort-vertical.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import PageContent from "@lib/components/PageContent.svelte"
@ -13,8 +12,7 @@
import NoteContent from "@app/components/NoteContent.svelte"
import CommentActions from "@app/components/CommentActions.svelte"
import EventReply from "@app/components/EventReply.svelte"
import {network} from "@app/core"
import {deriveEvent, deriveEventsById} from "@app/repository"
import {events, network} from "@app/core"
import {makeFeedContext} from "@app/feeds"
import {decodeRelay} from "@app/relays"
import type {PageProps} from "./$types"
@ -24,9 +22,9 @@
const {relay, id} = params
const url = decodeRelay(relay)
const context = makeFeedContext({relays: [url]})
const event = deriveEvent(id, [url])
const event = $events.one(id, [url]).$
const filters = $derived($event ? getCommentFiltersForRoot([$event]) : [])
const comments = $derived(deriveEventsAsc(deriveEventsById(filters)))
const comments = $derived($events.asc(filters).$)
const back = () => history.back()

View file

@ -3,7 +3,6 @@
import {page} from "$app/state"
import {sleep, spec} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util"
import {deriveEventsAsc} from "@welshman/store"
import {getCommentFiltersForRoot} from "@welshman/util"
import Reply from "@assets/icons/reply-2.svg?dataurl"
import AltArrowDown from "@assets/icons/alt-arrow-down.svg?dataurl"
@ -15,8 +14,7 @@
import ThreadPost from "@app/components/ThreadPost.svelte"
import ThreadSummaryBar from "@app/components/ThreadSummaryBar.svelte"
import EventReply from "@app/components/EventReply.svelte"
import {deriveEvent, deriveEventsById} from "@app/repository"
import {network} from "@app/core"
import {events, network} from "@app/core"
import {makeFeedContext} from "@app/feeds"
import {getChecked} from "@app/notifications"
import {decodeRelay} from "@app/relays"
@ -29,11 +27,11 @@
const {relay, id} = params
const url = decodeRelay(relay)
const event = deriveEvent(id, [url])
const event = $events.one(id, [url]).$
// Rows register themselves with `related`, so the whole page's reactions load in one batch
const context = makeFeedContext({relays: [url]})
const filters = $derived($event ? getCommentFiltersForRoot([$event]) : [])
const replies = $derived(deriveEventsAsc(deriveEventsById(filters)))
const replies = $derived($events.asc(filters).$)
const scrollToTop = () => element?.scrollTo({top: 0, behavior: "smooth"})