diff --git a/.agents/skills/flotilla-architecture/SKILL.md b/.agents/skills/flotilla-architecture/SKILL.md index b5de1524..7508882e 100644 --- a/.agents/skills/flotilla-architecture/SKILL.md +++ b/.agents/skills/flotilla-architecture/SKILL.md @@ -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. diff --git a/.agents/skills/flotilla-model/SKILL.md b/.agents/skills/flotilla-model/SKILL.md index fc1c53e5..756ab176 100644 --- a/.agents/skills/flotilla-model/SKILL.md +++ b/.agents/skills/flotilla-model/SKILL.md @@ -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. diff --git a/.agents/skills/flotilla-state/SKILL.md b/.agents/skills/flotilla-state/SKILL.md index 1028beb2..caf82151 100644 --- a/.agents/skills/flotilla-state/SKILL.md +++ b/.agents/skills/flotilla-state/SKILL.md @@ -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 diff --git a/.agents/skills/flotilla-views/SKILL.md b/.agents/skills/flotilla-views/SKILL.md index 3d70000d..3907ec4a 100644 --- a/.agents/skills/flotilla-views/SKILL.md +++ b/.agents/skills/flotilla-views/SKILL.md @@ -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: @@ -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` diff --git a/src/app/actionItems.ts b/src/app/actionItems.ts index 379ba029..c85c2072 100644 --- a/src/app/actionItems.ts +++ b/src/app/actionItems.ts @@ -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), ], diff --git a/src/app/calendar.ts b/src/app/calendar.ts index eee59e5c..76566224 100644 --- a/src/app/calendar.ts +++ b/src/app/calendar.ts @@ -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" diff --git a/src/app/call.ts b/src/app/call.ts index f9da559e..d2ba972b 100644 --- a/src/app/call.ts +++ b/src/app/call.ts @@ -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)), ), ], diff --git a/src/app/components/ArticleActions.svelte b/src/app/components/ArticleActions.svelte index 8ce9ab89..85c0f311 100644 --- a/src/app/components/ArticleActions.svelte +++ b/src/app/components/ArticleActions.svelte @@ -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}) diff --git a/src/app/components/ArticleAuthorSidebar.svelte b/src/app/components/ArticleAuthorSidebar.svelte index fa9db59e..1f935f9c 100644 --- a/src/app/components/ArticleAuthorSidebar.svelte +++ b/src/app/components/ArticleAuthorSidebar.svelte @@ -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), ) diff --git a/src/app/components/ContentQuote.svelte b/src/app/components/ContentQuote.svelte index ca0ff1ee..9768a7e7 100644 --- a/src/app/components/ContentQuote.svelte +++ b/src/app/components/ContentQuote.svelte @@ -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 diff --git a/src/app/components/EventComments.svelte b/src/app/components/EventComments.svelte index 7fc9d143..173c6ca6 100644 --- a/src/app/components/EventComments.svelte +++ b/src/app/components/EventComments.svelte @@ -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)) diff --git a/src/app/components/NoteContentMinimalPoll.svelte b/src/app/components/NoteContentMinimalPoll.svelte index 7de6b942..1bdb1957 100644 --- a/src/app/components/NoteContentMinimalPoll.svelte +++ b/src/app/components/NoteContentMinimalPoll.svelte @@ -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 = $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) diff --git a/src/app/components/PinContentEvent.svelte b/src/app/components/PinContentEvent.svelte index 5a3f5159..44fa98b6 100644 --- a/src/app/components/PinContentEvent.svelte +++ b/src/app/components/PinContentEvent.svelte @@ -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) diff --git a/src/app/components/PinboardSelect.svelte b/src/app/components/PinboardSelect.svelte index 226739e1..7cf17b49 100644 --- a/src/app/components/PinboardSelect.svelte +++ b/src/app/components/PinboardSelect.svelte @@ -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) diff --git a/src/app/components/PollVotes.svelte b/src/app/components/PollVotes.svelte index 30355ed2..253b16be 100644 --- a/src/app/components/PollVotes.svelte +++ b/src/app/components/PollVotes.svelte @@ -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 diff --git a/src/app/components/ReportDetails.svelte b/src/app/components/ReportDetails.svelte index f84f6653..23b6530d 100644 --- a/src/app/components/ReportDetails.svelte +++ b/src/app/components/ReportDetails.svelte @@ -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() diff --git a/src/app/components/RoomItem.svelte b/src/app/components/RoomItem.svelte index aa7becca..764aeec3 100644 --- a/src/app/components/RoomItem.svelte +++ b/src/app/components/RoomItem.svelte @@ -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), diff --git a/src/app/components/SearchBody.svelte b/src/app/components/SearchBody.svelte index 3c1eca16..057fd736 100644 --- a/src/app/components/SearchBody.svelte +++ b/src/app/components/SearchBody.svelte @@ -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()), ), ) diff --git a/src/app/components/SpaceMembersSummary.svelte b/src/app/components/SpaceMembersSummary.svelte index 92eadc9b..cbfc8525 100644 --- a/src/app/components/SpaceMembersSummary.svelte +++ b/src/app/components/SpaceMembersSummary.svelte @@ -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] : []) diff --git a/src/app/components/SpaceMenuNavItems.svelte b/src/app/components/SpaceMenuNavItems.svelte index ef30fea3..002f1019 100644 --- a/src/app/components/SpaceMenuNavItems.svelte +++ b/src/app/components/SpaceMenuNavItems.svelte @@ -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. diff --git a/src/app/components/SpaceReports.svelte b/src/app/components/SpaceReports.svelte index 9d9af14e..54d045ea 100644 --- a/src/app/components/SpaceReports.svelte +++ b/src/app/components/SpaceReports.svelte @@ -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() diff --git a/src/app/core.ts b/src/app/core.ts index 6ee77bae..9a5ef98d 100644 --- a/src/app/core.ts +++ b/src/app/core.ts @@ -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) diff --git a/src/app/featured.ts b/src/app/featured.ts index d081282d..46ef50f0 100644 --- a/src/app/featured.ts +++ b/src/app/featured.ts @@ -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", ] 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)) diff --git a/src/app/feeds.ts b/src/app/feeds.ts index 40b95f93..ea53027a 100644 --- a/src/app/feeds.ts +++ b/src/app/feeds.ts @@ -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), ) diff --git a/src/app/management.ts b/src/app/management.ts index 31eca984..6422c3bf 100644 --- a/src/app/management.ts +++ b/src/app/management.ts @@ -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]) => diff --git a/src/app/notifications.ts b/src/app/notifications.ts index 722206d9..0cb2d821 100644 --- a/src/app/notifications.ts +++ b/src/app/notifications.ts @@ -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, diff --git a/src/app/pins.ts b/src/app/pins.ts index fd9fe9a2..6aa3979d 100644 --- a/src/app/pins.ts +++ b/src/app/pins.ts @@ -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 => derived( @@ -22,7 +21,7 @@ export const derivePinnedEvents = (pubkey: string): Readable => 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() diff --git a/src/app/repository.ts b/src/app/repository.ts index 85611cc6..a7061133 100644 --- a/src/app/repository.ts +++ b/src/app/repository.ts @@ -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. diff --git a/src/app/roomPins.ts b/src/app/roomPins.ts index 3d04e1d1..57ea5617 100644 --- a/src/app/roomPins.ts +++ b/src/app/roomPins.ts @@ -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 => readable([], set => { @@ -24,22 +23,25 @@ export const deriveRoomPinnedEvents = (url: string, h: string): Readable { - 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) diff --git a/src/routes/spaces/[relay]/articles/[address]/+page.svelte b/src/routes/spaces/[relay]/articles/[address]/+page.svelte index 41c009c4..abd42136 100644 --- a/src/routes/spaces/[relay]/articles/[address]/+page.svelte +++ b/src/routes/spaces/[relay]/articles/[address]/+page.svelte @@ -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() ?? [])) diff --git a/src/routes/spaces/[relay]/calendar/[address]/+page.svelte b/src/routes/spaces/[relay]/calendar/[address]/+page.svelte index 349544af..1a8b7cb1 100644 --- a/src/routes/spaces/[relay]/calendar/[address]/+page.svelte +++ b/src/routes/spaces/[relay]/calendar/[address]/+page.svelte @@ -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([])) const people = $derived(getRsvpsByStatus($rsvps)) diff --git a/src/routes/spaces/[relay]/classifieds/[address]/+page.svelte b/src/routes/spaces/[relay]/classifieds/[address]/+page.svelte index 813020d1..6f6ac8d0 100644 --- a/src/routes/spaces/[relay]/classifieds/[address]/+page.svelte +++ b/src/routes/spaces/[relay]/classifieds/[address]/+page.svelte @@ -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() diff --git a/src/routes/spaces/[relay]/goals/[id]/+page.svelte b/src/routes/spaces/[relay]/goals/[id]/+page.svelte index b8f1866f..d49c8314 100644 --- a/src/routes/spaces/[relay]/goals/[id]/+page.svelte +++ b/src/routes/spaces/[relay]/goals/[id]/+page.svelte @@ -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) diff --git a/src/routes/spaces/[relay]/library/+page.svelte b/src/routes/spaces/[relay]/library/+page.svelte index 8df067e8..abb7025a 100644 --- a/src/routes/spaces/[relay]/library/+page.svelte +++ b/src/routes/spaces/[relay]/library/+page.svelte @@ -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") ?? "") diff --git a/src/routes/spaces/[relay]/polls/[id]/+page.svelte b/src/routes/spaces/[relay]/polls/[id]/+page.svelte index d89f2a99..85a7d869 100644 --- a/src/routes/spaces/[relay]/polls/[id]/+page.svelte +++ b/src/routes/spaces/[relay]/polls/[id]/+page.svelte @@ -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() diff --git a/src/routes/spaces/[relay]/threads/[id]/+page.svelte b/src/routes/spaces/[relay]/threads/[id]/+page.svelte index 47954235..53ed27c8 100644 --- a/src/routes/spaces/[relay]/threads/[id]/+page.svelte +++ b/src/routes/spaces/[relay]/threads/[id]/+page.svelte @@ -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"})